跳转到内容

细节层次(LOD)

一块一百单位外的石头和一块两千单位外的石头,现在用同样多的三角形绘制;而在两千单位外, 它们几乎全部落进同一个像素里。LODGroup 让一个物体在小到没人分辨得出来时,交给 更便宜的网格去画 —— 这是道具密集的场景里最大的一笔节省。

在 Estella 自带的第三人称示例里,一片 35 块石头的场地就用上了它;在一万个道具的规模上, 三角形从 191 万降到 4.2 万,GPU 时间从 3.5 ms 降到 0.27 ms(数字在下面)。

LODGroup 挂在MeshRenderer 同一个实体上。渲染器说这个物体是什么 —— 那个网格就是 level 0,即最精细的一级;组件说的是它变小之后谁可以顶替它:

world.insert(rock, MeshRenderer, { mesh: 'assets/models/rock-lod0.esmesh', lit: true });
world.insert(rock, LODGroup, {
lod1: 'assets/models/rock-lod1.esmesh',
lod2: 'assets/models/rock-lod2.esmesh',
lod1Size: 0.25, // 小于屏幕高度的四分之一 → 换 lod1
lod2Size: 0.11, // 小于八分之一 → 换 lod2
cullSize: 0.02, // 小于五十分之一 → 干脆不画
hysteresis: 0.1,
});

把这个组件去掉,最精细的网格就回来了 —— 渲染器的其他部分一点没变。

lod1Size 和它的兄弟们,是物体包围体覆盖视口高度的比例。1.0 表示物体正好一屏高, 0.02 表示五十分之一屏高。

它刻意不是距离,因为距离在不同尺度、不同镜头下给的是同一个答案:

  • 一百米外的高塔和一颗石子,在屏幕上不是一个大小;
  • 同一个位置,30° 视场看到的物体是 60° 的两倍大;
  • 正交相机的答案根本不随距离改变。

屏幕占比同时覆盖这三件事,而且与分辨率无关 —— 同一个数字在手机上和桌面上都对。

一条只用一个数字划的边界,会因为相机的轻微呼吸而来回翻转;而每帧都在换网格,远比换本身 更显眼。hysteresis 让每条边界变得不对称:物体在阈值处往下交给更粗的网格,但要长回 到超出阈值那么一个比例之后,才会换回精细的一级。

默认 0.1 时,lod1Size = 0.20 的含义是:

  • 缩小穿过 0.20 → level 1 接手;
  • 重新长过 0.22 → level 0 才回来。

把它设成 0 就是一条裸阈值,会闪。它同样作用于 cullSize,所以在可见边缘的物体不会眨眼。

它属于相机,不属于实体。 同一帧里的两个相机 —— 主视角和小地图,或者分屏 —— 看到的是同一个物体的两个尺寸,可以画成两个不同的级别。实体上没有 currentLOD 可读、也没有可写。

它是在整组的包围体上量的。 用来做判断的包围体是所有级别的并集,所以它不随当前 画的是哪一级而改变。若按当前网格取包围体,就会反过来影响挑出它的那个判断,进而振荡。

剔除先跑。 视锥已经拒绝掉的物体根本不会被测量,所以这份开销只花在可能被看见的东西上。

实例化不受影响。 选级只改变一次 draw 引用哪份几何,别的都不变,所以同一级的道具照旧 合并成一次 instanced draw。三个级别的一片场地到达 GPU 时大约是三次 draw,而不是每个道具一次。

  • 槽位按顺序填 —— 先 lod1,再 lod2,再 lod3。留空的槽位就是梯子的终点,它后面的 一律到不了(引擎会告警)。
  • 尺寸必须递减。 lod2Size 小于 lod1Size,依此类推。
  • cullSize: 0 完全关掉剔除,于是这一组永远不会消失。
  • 蒙皮的一组必须共用一副骨架。 蒙皮网格的每一级都要有相同的关节数;对不上的那一级会 画成绑定姿势,引擎会说出来。

在一片纵深排布的道具场地上实测,512×512,WebGL2:

道具数 三角形 GPU
1 000,始终最高精度 195 776 0.505 ms
1 000,带 LODGroup 4 248 0.116 ms
10 000,始终最高精度 1 912 960 3.595 ms
10 000,带 LODGroup 42 392 0.27 ms

每一万个道具的选级大约花掉 0.2–0.3 ms 的 CPU,这就是那笔交易:花 CPU 去挑,省 GPU 去画。 数量越大,汇率越划算 —— 这正是重点,它决定了一万个道具的场地到底能不能发出去。

LODGroup 讲的是同一个物体在不同尺寸下的事。它不是流式加载:每一级的网格都常驻, 不会加载也不会卸载;小到 cullSize 以下的物体只是不画,并没有从世界里移走。 把远处的物体合并成一个代理(HLOD),以及把世界分块加载,都是另外的题目。