ENGINEERING NOTE · 2026-10
给 3D 小镇做质感:一场持续两周的测量战争
项目:日常小镇 3D(Three.js 等距小镇,纯静态、无构建、跑在手机浏览器上)
一、需求只有一句话,但它是不可测的
用户对画面提了一个要求,原话是:
「不是气氛啊,是质感。是细节,是建模的精细度。」
这句话的麻烦在于:它不可测。调个色调、加个雾、换个光照,都能交出一张"看起来不一样"的截图,但都不是他要的东西。项目早期连续三批"加质感"的改动全部失败,复盘时发现根本原因不是审美,而是:改了,但证明不了有没有变。
于是先停下来造尺子。
二、尺子:texel 密度,而不是"有没有贴图"
第一版的判据很自然:材质对象上有没有 map。跑出来一片绿,看起来全城建筑都有贴图。
但这是误报。小镇的建筑外观来自 Kenney 套件(CC0 素材),它的美术实现是一张 512×512 的调色板:一个颜色格子摊满一整面墙。map 存在,墙面上却没有任何细节。人眼看到的"平"是真的平。
换成连续量:每米世界距离摊到多少个贴图像素:
texelsPerMeter = mapWidth × map.repeat.x × sqrt(Auv / A3)
Auv = UV 空间三角形面积和
A3 = 世界空间三角形面积和(顶点必须乘 matrixWorld)
量具合格线里最重要的一条是阳性对照:尺子必须被一个已知真值校准过。地面铺装是现成的锚:一格石砖 1.11×0.73 m、512 贴图,期望值约 461 texel/m。实测地面类 171~2353 texel/m,尺子读数与手算吻合。
然后拿它量建筑:
这个 0 也顺带证伪了一条当时正打算走的路:给地标外墙加贴图。量具显示几何在运行时被换皮管线整个 remove + dispose 掉了,改的地方根本不是屏幕上的东西。如果没造尺子,这一批又会是白干。
三、五条被证据判死刑的路线
找到病灶后,接下来的两周里以下方向全部被 A/B 证伪。记录它们比记录成功值钱:
| 路线 | 死因 |
|---|---|
| 给地面加法线/粗糙度扰动 | 判据读数逐位相同,粗糙度只差 ±0.02,改动小到不存在 |
| UV 旋转 90° 换采样方向 | 各向异性直接把格子拉成 2× 变形,糊了,还丢了棱角 |
| 换一条石缝游走源 | 规律性噪声和像素变化同源,两条验收一起挂 |
| 调阴影固有色当质感杠杆 | 同参两跑对比:饱和度 +0.0、材质指标 −0.2,效应量 < 3× 抖动 |
| 写实 PBR 门板/脊瓦 | 技术上全部成立,风格上被真机一票否决:「门板画错了」 |
第五条最值得说:前四条是量具救的,第五条量具救不了。美学裁决权在用户,机器只能证伪。所有对照图必须"一眼看出差别",看不出差别就不算完成。这条写进了验收标准。
还有一条决策记录:地面纹理的周期性,三次尝试都无法消除,最后选择接受。改动本身会暴露石缝格,继续投入是负收益。证伪清单里"接受"也是一种合法终态。
四、真正的转折:换掉"运行时真相"
既然运行时全城建筑 = 套件几何 + 调色板材质,那要质感就得换这条供应链本身。方向:Blender 自建模写实 PBR 建筑,按材质 3×3 分格 UV、烘 PBR、单材质 GLB 导出,且包围盒/脚底/朝向与对应原模型逐位一致,场景布局一行不用改。
中间还揪出一个更阴的 bug:换皮管线的贴图重解码里 flipY=true 是错的,多翻了一次 V 轴。Kenney 的表现是白墙变粉、绿顶变灰("还像房子",所以两轮都没人发现);自建模的表现是墙面纯黑。修完之后,同机位对照出图第一次出现 40% 的像素变化。
然后卡顿来了。真机帧率掉到不能玩。显存账一算:14 栋背景房每栋 1024² 三张图,光贴图 226MB。手机 GPU 不是这么用的。两刀降档:
- 全部 1024² → 512²:226MB → 94MB
- 贴图按角色分档(地标保 1024、背景房 512、远景 256):94MB → 41MB
五、最后一步:证明"细节真的变多了",而且是信号不是运气
收尾批次给 18 栋建筑换上真细节贴图(法线从一张 618 字节的近全平占位图升级到 512² 真法线,粗糙度图 128²→512²)。三角形数逐栋不变,闸门全绿,但"改完了"不等于"变好了"。
在带 GPU 的机器上跑同机位 A/B(相机坐标逐位一致:[-11.333, 8.36, 23.078]),用 4 邻域拉普拉斯度量画面高频能量:
| 样本 | 高频均值 |
|---|---|
| 旧版 noon 第 1 跑 | 23.0228 |
| 旧版 noon 第 2 跑(测噪声) | 23.0596 |
| 新版 noon 第 1 跑 | 23.2936 |
| 新版 noon 第 2 跑(测噪声) | 23.3422 |
噪声底取同版本两跑之差的最大值 = 0.0487;版本效应 = 0.2708。效应/噪声 = 5.56×,过了"≥3× 且来自两组独立配对"的判据。夜景的 draw call / 三角形 / 纹理数两版逐位相同(145 / 150522 / 91),无回归。
中途还差点被一个假信号带偏:正午场景两帧总账差了 24.6K 三角形,第一反应是"新模型有问题"。拿逐对象分桶一看,两边的读数在彼此身上都出现过:是场景成熟度的采样时刻错位,不是回归。单帧总账不能当回归判据,这条也进了纪律清单。
最后用那把最初的尺子复测:
六、这套流程里真正可复用的东西
小镇本身不重要,重要的是这五步全部可以搬走:
- 把形容词翻译成连续量。"质感"不可测,"每米贴图像素"可测。翻译不了就先别动手。
- 尺子先过阳性对照。用已知真值校准过的尺子才有资格判案,否则"一片绿"和"一片红"都可能是假的。
- 证伪比证实便宜。每条路线先问"什么读数能判它死刑",判不了死刑的改动不值得排队。
- 效果量必须对着噪声底报。没有同版本两跑做参照的任何"提升"都是叙事。
- 裁决权分层。机器判"有没有变",人判"是不是要的"。两层混用的项目,要么白干要么自嗨。
下一步的开放问题也照实说:新贴图为 +20.4MB 显存买单,和 41MB 红线正面冲突,降档方案(粗糙度回 256)还没做真机复核;程序生成的表面细节层和新的真法线是否互相打架,待量具复测。这条线没完,但每一步都可复跑、可证伪。