压缩优化
几何压缩
1. 纯几何体(高模,无不透明裁剪)
-
瓶颈在:顶点着色器(Vertex Shader)和内存带宽。
-
18.6 万面意味着至少有 20 多万个顶点。移动端 GPU(尤其是手机、平板等 TBDR 架构的显卡)对于单体资产的顶点数量非常敏感。几万个顶点和 20 万个顶点在几何计算和坐标转换上的开销是天差地别的。
-
优点:完美享受显卡的 Early-Z(提前深度测试) 技术,遮挡剔除效率极高,没有像素浪费。
2. 不透明裁剪(Alpha Card 插片流)
-
瓶颈在:片元着色器(Fragment/Pixel Shader)和过度绘制(Overdraw)。
-
如果用插片,全树的面数可以轻松压到 5000 面(甚至更低)。顶点着色器的压力直接暴跌 95% 以上。
-
缺点(性能损耗点):Alpha Clip 在材质里使用了
discard(抛弃像素)指令。在移动端 GPU 中,discard会直接打碎 Early-Z 优化。显卡必须老老实实地把这个面片覆盖的所有像素点都计算一遍,无法提前剔除,这就会造成严重的 Overdraw(过度绘制)。
纹理压缩
1. 显存占用(VRAM):1:4 甚至 1:8 的绝杀
-
WebP 的致命伤:WebP 只是传输压缩。图片下载到浏览器后,CPU 必须把它解压成原始的 RGBA 8888 位图(Bitmaps)再塞给 GPU。一张 的 WebP 文件可能只有 500KB,但进到显存里必然死死占用 16MB。
-
KTX2 的优势:KTX2 容器内的数据(如 ETC1S 或 UASTC)是显存格式。现代手机(iOS/Android)和 PC 的 GPU 能够直接读取并渲染这种压缩格式,不需要解压。同样的 纹理,在显存里通常只占 2MB - 4MB。
结果:如果一个数字孪生或 WebGL 场景有 50 张纹理,用 WebP 可能会直接导致手机浏览器显存溢出(OOM)而崩溃/闪退,而用 KTX2 依然稳如泰山。
2. 彻底消灭主线程卡顿(Jank)
-
用 WebP 时,当模型首次进入视野,浏览器主线程要解析 WebP、创建 ImageBitmap、上传显存,这会导致明显的画面掉帧/卡顿。
-
KTX2 允许使用 Web Worker 在后台直接把数据传输给 GPU,传输速度极快,首次加载和渲染如丝般顺滑。
痛点一:有块状/色带等 Artifact(画质受损)
这是因为 KTX2 常用的 ETC1S 模式为了追求极致压缩率,采用了类似色彩调色盘的算法。在遇到渐变色(如天空盒、光滑金属表面的法线贴图)时,就会出现严重的色彩断层(Banding)。
-
解法:在你的插件里,引入 UASTC 模式。
-
ETC1S:适合基础颜色贴图(BaseColor)、粗糙度等,文件极小。
-
UASTC:画质极高,几乎无损,专门用来压法线贴图(Normal Map)和高细节渐变纹理。虽然文件体积会比 ETC1S 大,但完美解决色带问题。
-
痛点二:文件体积大、网络传输慢
KTX2(尤其是 UASTC 模式)的原始文件体积确实经常比 WebP 大。
-
解法:必须开启转码器的 ZSTD(Zstandard)或 BasisLZ 超级压缩(Supercompression)。
-
现代 glTF 工具链(如
gltf-transform)在导出 KTX2 时,会在外部再套一层 ZSTD 压缩。 -
这样可以把网络传输体积压缩到和 WebP 差不多大(甚至更小)。网络下载完这个紧凑的包后,前端页面通过 Web Assembly (Wasm) 的解压器,在几毫秒内就能瞬间还原成可供 GPU 使用的 KTX2 格式。
-
使用KTX2前置
KTX2 的基础压缩算法(如 ETC1S / UASTC)要求图片的宽高必须是 4 的倍数(Multiple of 4)。
- 原因:现代 GPU 纹理压缩算法(包括 PC 的 BC 格式、手机的 ETC/ASTC 格式)都是基于“块压缩(Block Compression)”原理。GPU 会把整张图切成无数个 像素的“微型矩阵”来单独处理。如果不提前处理成 4 的倍数,KTX2 编码器(如
basisu)在遇到非 4 倍数贴图时,要么直接报错罢工,要么会在边缘强行填充(Padding)虚假像素来凑满 的块。这种强行填充不仅浪费显存,还可能在纹理边缘产生不正常的缝隙(Borders)。
更严格的传统规则:2 的幂次方(Power of 2, 简称 POT)
-
现状:在老旧的 WebGL 1.0 时代,非 POT(NPOT)图片不仅无法压缩,连生成 Mipmaps(渐进纹理层级) 和设置某些重复平铺模式(Repeat Wrap)都不支持。
-
2026年的现状:现在的现代显卡、WebGL 2.0 和 WebGPU 已经完全支持非幂次方(NPOT)贴图的渲染和平铺了。但是,为了写 KTX2 压缩插件,依然要确保宽高能被 4 整除。
最终
法线贴图 (Normal Map):强制或推荐使用 KTX2 (UASTC 模式) 基础色彩/固有色 (BaseColor):如果追求极致画质用 KTX2 (UASTC);追求极致加载速度用 KTX2 (ETC1S) + ZSTD。 UI 或不需要高频渲染的非 3D 纹理:保留 WebP 选项。
渲染压力核心
CPU端压力
1. 提交物流单:DrawCall 数量与状态切换(State Changes)
这是最经典的 CPU 瓶颈。
-
工作原理:CPU 准备好一个物体的数据,向 GPU 发送一条“绘制命令”(DrawCall)。每发一次,CPU 都要进行一次上下文切换(Context Switch)。
-
隐形杀手(状态切换):比 DrawCall 数量更卡的是更换材质和纹理。如果物体 A 用材质 1,物体 B 用材质 2……CPU 每次发 DrawCall 前都要大喊一声:“GPU,把当前的纹理换掉!”这种状态切换(SetPass Call)在 CPU 端极耗性能。
-
优化解法:静态批处理(Static Batching)、动态批处理、实例化渲染(Instancing,适合渲染大量重复的树木、建筑)、合并材质球/合并图集。
2. 空间物流筛选:视锥体剔除与裁剪(Culling & Frustum Culling)
CPU 不能把整个数字孪生城市的所有数据都盲目塞给 GPU,它必须先在内存里做“筛选”。
-
工作原理:每一帧,CPU 都要计算摄像机看得到哪些物体(视锥体裁剪),哪些物体被大楼挡住了(遮挡剔除 Occlusion Culling)。
-
瓶颈点:如果你的场景里有 10 万个 独立的 Mesh(哪怕面数极低),CPU 每一帧光是遍历这 10 万个物体的包围盒(Bounding Box)并判断它们在不在镜头里,就会把 CPU 的单核性能直接烧干。
-
优化解法:在场景层级做 Mesh 合并。把一个区域内的几十栋小楼合并成一个大 Mesh,让 CPU 只要做一次包围盒判定。
3. 数据组织与传输:内存到显存的搬运(Host-to-Device Transfer)
-
工作原理:当新场景加载,或者有新的骨骼动画、动态生成的网格时,CPU 需要在内存中把顶点数据(位置、UV、法线)组织好,然后通过 PCIe 总线源源不断地“推”给 GPU 的显存。
-
瓶颈点:如果网格数据没有经过优化(比如没有压缩顶点格式),或者频繁地在运行时更新顶点(比如用 CPU 去逐顶点计算水波纹),PCIe 总线的带宽就会被挤爆,导致 CPU 在那儿死等传输结束。
-
优化解法:使用 Draco/Meshopt 压缩几何,尽量使用 GPU 端的 Vertex Shader(着色器动画)来代替 CPU 算动画。
GPU压力
- 几何与光栅化(Vertex / Geometry Bound):
- **指标**:顶点数(Vertices)、三角面数(Triangles)。
- **压力点**:顶点着色器(Vertex Shader)要在每一帧计算所有顶点的位置。如果一个建筑模型有几百万面,GPU 的几何前端就会过载。
- 纹理与带宽(Texture / Bandwidth Bound):
- **指标**:显存占用(VRAM)、像素采样次数。
- **压力点**:WebP 变成位图后巨大无比,GPU 在高分辨率(比如 4K 屏幕)下采样时,**显存带宽**会被挤爆。
- 片元/像素着色(Fragment / Pixel Shader Bound):
- **指标**:Shader 复杂度、**Overdraw(像素重复绘制)**。
- **压力点**:这是极其致命的一点。即便面数很低、纹理很小,如果你的自定义材质里写了大量的矩阵运算、多层噪声函数,或者场景里有大量透明/半透明物体(如玻璃、特效)叠在一起,导致同一个像素被重复计算了 10 次(Overdraw),GPU 的算力一样会被榨干。
汇总
| 维度 | 瓶颈分类 | 核心核心指标 | 决定渲染总量的关键因素 | 常用优化手段(你的插件方向) |
|---|---|---|---|---|
| CPU 端 (指挥官) | 1. 调度指令 | DrawCall / SetPass | 独立网格数量、材质球数量、频繁切换状态 | 实例化(Instancing)、合并 Mesh、合并图集 |
| 2. 空间裁剪 | Culling Time | 场景中物体节点的总总数量(Hierarchy 过深) | 八叉树场景管理、提前分块合并网格 | |
| 3. 数据搬运 | Bandwidth (PCIe) | 频繁的内存到显存的动态数据提交 | 静态 Buffer 常驻显存、骨骼动画 GPU 化 | |
| GPU 端 (执行者) | 1. 几何与光栅化 | Vertices / Triangles | 视野内的总三角面数、顶点过于密集(像素级微小面) | Draco/Meshopt 压缩、自动生成 LOD、减面 |
| 2. 显存与带宽 | VRAM 占用 / 采样率 | 未压缩的纹理体积(如 WebP 变位图)、超大分辨率 | KTX2(ETC1S / UASTC)+ ZSTD 纹理压缩 | |
| 3. 片元着色 | Overdraw / Shader 复杂度 | 半透明重叠层数、材质 Shader 内的复杂数学运算 | 减少透明物体层数、严格控制 Shader 复杂度 |
“单次渲染总量”怎么看?
在 Web 3D(WebGL / WebGPU)环境中,我们无法像看 DrawCall 那样用一个简单的数字代表“总量”,因为它是多维度的。你需要通过以下三个层级的工具来“看见”它:
1. 基础层:网页引擎自带的 Stats(初筛)
无论你用 Three.js 还是 Babylon.js,它们自带的 Monitor 面板都能给你最直接的反馈:
-
Geometries / Textures Count:当前内存/显存中缓存的几何体和纹理数量。
-
Triangles / Vertices:当前视野内(视锥体剔除后)实际提交渲染的总三角面数。
评判标准:移动端 WebGL 场景,单帧活动面数一般建议控制在 50万 - 100万面 以内;PC 端可以放宽到 数百万面。如果面数超标,说明“几何总量”过大。
2. 进阶层:Spector.js / 浏览器开发者工具(定量分析)
通过 Spector.js 抓取一帧(Capture),你能清晰地看到:
-
每一次 DrawCall 的具体消耗:它会把这一帧拆解成几十甚至几百个步骤。你可以点开任意一步,查看这一次 DrawCall 渲染了多少个顶点(Vertices Count),绑定了哪几张纹理,纹理的分辨率(如 )以及格式。
-
这就能解答单帧GPU量的问题:可以通过它看到每个 DrawCall 的“单次渲染量”。如果发现某个 DrawCall 一下子提交了 30 万个面,或者关联了一张未压缩的超大纹理,它就是导致掉帧的“罪魁祸首”。
3. 终极层:深度硬件分析(RenderDoc / NVIDIA Nsight)
如果想知道 GPU 渲染这个画面到底费不费性能,需要看 GPU 耗时(GPU Time,单位:毫秒 ms)
-
在 Chrome 浏览器中开启 WebGPU 体验,或者利用 RenderDoc 注入浏览器进程抓帧。
-
它能直接给出:这一帧 GPU 跑了多少毫秒。
评判标准:要达到 60 帧满帧(60 FPS),CPU + GPU 总耗时必须小于 16.6 毫秒 ()。如果 GPU 耗时达到了 25ms,哪怕 DrawCall 只有 10 个,画面也一定会卡顿。
怎么评判这个画面“费性能”?(排查口诀)
评价一个画面是否费性能,不能只看画面好不好看,而是要看它有没有产生“无效的算力浪费”。可以从以下四个维度来建立评判标准:
1. 检查“面数密度”与屏幕像素的比例(过密几何)
-
现象:一个非常小的螺丝钉或者远处的建筑,美术做成了数万面的高模。
-
结果:在屏幕上它可能只占了 像素的大小,但 GPU 却要为它处理几万个顶点。这种“高密低效”的几何体极费性能。
-
量化评判:在 Spector.js 里看单个物体的面数,不合理的物体必须做 LOD(细节层次)或用你的插件进行强力压缩(如 Meshopt)。
2. 检查显存(VRAM)是否逼近设备红线
-
现象:画面不卡,但玩着玩着浏览器标签页直接崩溃(闪退),提示“Context Lost”。
-
评判标准:通过 Chrome 的
Performance面板或系统任务管理器观察。移动端微信内置浏览器给 WebGL 的显存限制通常很严(有的 Android 设备超过 500MB-1GB 就会闪退)。如果场景光纹理就占了 800MB,那这个画面就是“性能炸弹”。
3. 检查 Overdraw(过度绘制)
-
现象:场景里有大面积的玻璃幕墙、粒子特效(烟雾、火焰)、或者层层叠加的半透明数字孪生 UI。
-
评判标准:在 Babylon.js/Three.js 中开启 Overdraw 渲染模式(或者将材质临时替换为半透明红色,叠加越深的地方越红)。如果一个像素被反复绘制了 5 次以上,说明片元着色器压力极大。
4. 统计“DrawCall 的含金量”
-
如果 DrawCall 很高(比如 > 1000),但每帧渲染的面数很少(比如才几万面),这叫“轻量级多批次”。说明 CPU 提交成了瓶颈,美术没有做好 Mesh 合并(Batching/Instancing)。
-
如果 DrawCall 很低(比如 < 50),但 GPU 耗时极长,说明单个 DrawCall 的吞吐量太大(比如一个材质里写了太复杂的数学计算,或者单网格面数过载)。
烘焙问题
烘焙光照,法线反了的话,烘焙不出来 玻璃等有透明状态的模型不用烘焙光照贴图,只需要打开投射,在webgl是开启次表面散射
清空对象数据
-
渲染大原则:不要的东西,连通道都不要给它赋值(保持为
null或undefined),或者直接用开关属性关闭。千万不要挂着贴图然后把强度调成 0。 -
烘焙最佳状态:保持全局 0 灯光、0 全局环境,如果玻璃半透明等,只给这几个被选定物体的材质挂载同一个
.env贴图实例。