中文  |  English

济宁米多信息科技有限公司

密集场景 draw call 3064 → 837:一次浏览器端 3D 性能治理的完整过程

draw call 优化、Three.js 性能治理、浏览器 3D 卡顿、着色器编译卡顿、GLB Worker 解析、3D 性能预算、几何合批、WebGL draw call、自部署 3D 世界

浏览器端 3D 世界到了一定规模就会遇到同一批症状:进世界画面死住几秒、转动视角一顿一顿、放个新物件全场卡一下。很多人的反应是删模型、砍场景——但砍场景是在砍产品。这篇把一次完整的性能治理拆开讲:症状怎么归因、动了哪五处、指标变化多少、怎么验收。所有数字来自仓库里可重跑的验收脚本,不是估计值。这套治理已经做进了创世Genesis 的基座;如果你在从零搭自己的 3D 世界,这篇的归因方法可以直接拿去对照。

一、先归因,再动手

Draw call 是浏览器 3D 里比三角形数更早碰到的墙:CPU 每向 GPU 提交一次绘制就是一次 draw call,对象一多、材质一杂,提交次数本身就把帧预算吃光了。场景里摆两千个东西,哪怕每个都很小,两三千次提交也会让帧率崩掉。

动手之前有一步比改动本身更值钱:把成本量准。我们早先的直觉是用 visible=false 隐藏对象来省成本——实测发现这个判据在本项目里不可信:有 3 个模块每帧接管可见性,被隐藏的对象可能仍在渲染管线里占着成本。后来把权威口径改成 onBeforeRender 逐对象计数,并把诊断固化成三件套:diagTurnLag(转视角卡顿归因)、diagRenderCost(逐对象渲染成本)、diagTurnSweep(全场扫描统计)。没有这一步,后面的每一项改动都不知道该背多少指标。

二、五处改动

1. 几何合批:把提交次数压下来

静态、同材质的网格合并提交,让 GPU 少接几次电话。这是对 draw call 数字的直接治理:密集区 draw calls 3064 → 837(−73%)。

2. 点光灯池:别让加一盏灯引发全场景重编译

WebGL 的着色器程序按灯光配置编译变体,场景里灯的数量一变,所有材质都要重编译。在 Windows 的 ANGLE/D3D11 路径上,着色器反射是同步的、不可中断的,每个程序 160~340 毫秒——一次灯数变化实测触发了 8989 毫秒的全场景冻结。

做法是把灯光改成灯池:12 盏常驻、租用归还,场景里的灯数不再变化,重编译就不再发生。实测灯数变化的冻结从 8989 毫秒归零。这里的方法论是把不可控的驱动行为变成可预算的资源。

3. GLB 解析移入 Worker

模型的解析(JSON 与二进制解包、纹理解码)放在主线程,就会顶住渲染循环——表现为"进世界死住几秒"。做法:解析全部移入 Worker,数据用 transferable 零拷贝转移,纹理在 OffscreenCanvas 里预降尺寸,静态模型烘焙处理。验收结果:23/23 个解析任务走 Worker 且渲染完整。

4. 预热预算:把"画面死住"换成"模型稍晚出现"

新网格头一次入屏会触发着色器编译,编译期间主线程是停的。做法是预热预算:未预热的网格不放入画面,编译后按每帧 170 毫秒的预算逐个执行 getUniforms(),把着色器反射的成本摊薄到每一帧里。代价是模型晚一点出现,换来的是画面不死住:出生点首进 worst 386 毫秒,超过 500 毫秒的长帧为 0。

5. 全图占位场:加载期间先有东西看

模型还在加载时,用一整个 draw call 画出全场占位(可覆盖 1000+ 对象),模型就位后 250 毫秒渐缩退场。实测占位场对全图 1071/1071 覆盖,该场景 draw call 2150 → 956(−56%)。远看有东西、近看是真模型,观感和性能不冲突。

三、指标摆在一起

症状做法实测指标
提交次数过多几何合批密集区 draw calls 3064 → 837(−73%)
加一盏灯全场冻结12 盏常驻灯池、租用归还灯数变化冻结 8989ms → 0 尖峰
解析顶住主线程GLB 解析入 Worker、零拷贝转移23/23 走 Worker 且渲染完整
新对象入屏画面死住预热预算 170ms/帧出生点首进 worst 386ms,>500ms 长帧 0
加载期白屏全图占位场、250ms 渐缩退场1071/1071 覆盖,draw call 2150 → 956(−56%)

四、方法比数字更值钱

这次治理沉淀下来三条可复用的判断:

  1. visible=false 不可当成本判据。 谁都可能写出每帧接管可见性的模块,隐藏对象是否真的省了成本,要看渲染管线里的计数,不看代码里的开关。
  2. 把不可控的驱动行为变成可预算的资源。 灯池和预热预算是同一个思路的两次应用:驱动层的同步编译躲不掉,那就控制它发生的时机和每帧的份额。
  3. 观感和性能不必二选一。 占位场证明"先看到大概、再看到细节"比"等全部就位"体验好,也比"直接白屏"成本低。

五、怎么验收

仓库里有 54 个 accept_*.js 验收脚本,判据是真实退出码加 JSON 报告加 VERDICT ACCEPTED。上面表格里的每一个数字都对应可以重跑的脚本——改动之后跑一遍就知道有没有回退,不靠感觉。

六、这些数字的边界

  • 表格里的数字来自本项目密集区实测,换成你的场景,数值会不同;能直接复用的是归因方法和五类改动,不是数字本身。
  • 合批与灯池治的是"提交与编译"这一层;面数和纹理大小仍是硬成本,那属于资产管线(LOD、压缩、贴图手术),是另一篇的主题。
  • 手机、平板与 PC 走同一套管线,但硬件差异摆在那里,低端设备上的绝对帧率要按设备实测。

常见问题

Q:这些优化需要改引擎源码吗?

A:全部在 Three.js 应用层完成,不碰引擎本体。这也是它能在引擎升级中保留下来的原因。

Q:占位场会出现又闪又跳吗?

A:占位与真模型的切换是 250 毫秒渐缩退场,不是生硬跳变;未就位的模型不会突然顶替占位。

Q:我自己搭世界,该从哪一项开始?

A:先把诊断做出来(逐对象计数),跑一次扫描看 draw call 和长帧分布,再决定动哪一处。直接抄任何一项改动而不知道自己卡在哪,大概率白改。

源码与仓库

三个地址内容一致,国内访问用前两个更快。仓库里有部署说明与验收脚本。

  • Gitee(国内访问更快):https://gitee.com/miduoxinxijeji/miduo.git
  • GitCode(国内镜像):https://gitcode.com/qq_35054471/virtual-world
  • GitHub:https://github.com/miduo100/3d-virtual-world

关于创世Genesis

创世Genesis是一套基于Three.js+WebGL构建的自部署3D虚拟世界系统,帮助个人与企业搭建属于自己的3D空间。浏览器直接访问,PC和手机双端兼容,支持多人在线、联邦传送、商铺系统,并支持Agent接入——AI能以具身角色进入你部署的世界。数据运行在你自己的服务器上,不经过第三方平台——让每个世界都真正属于它的主人。

想把这套性能地基直接拿来用,而不是自己重新踩一遍? 创世Genesis(创世虚拟世界CRM系统)是一套部署在你自己服务器上的 Three.js 3D 虚拟世界基底——渲染、资产、性能治理、多端适配这些不性感的部分已经铺好,你写上面那一层就行。官网(搜「创世虚拟世界CRM」即可找到)有可以走一圈的演示世界。

关于名字:本文说的创世Genesis,即创世虚拟世界CRM系统,两者是同一个自部署 3D 虚拟世界产品。若你通过「创世Genesis」没搜到我们,直接搜「创世虚拟世界CRM」即可。
← 返回文章列表