Better

Ethan的博客,欢迎访问交流

three.js 性能优化清单

最近发现我的场景容易出现卡顿的现象,这让很困惑,虽然数据量挺大,但也不至于那么大,而且我发现的奇怪现象是,GPU 的使用率非常低,倒是 CPU 使用了上升的比较快。

诊断

性能原因诊断:如果出现性能问题,你需要检查是 GPU 还是 CPU 瓶颈,使用 scene.overrideMaterial 覆盖所有的材质,如果性能提升了,那就是 GPU 问题,否则是 CPU 问题

Performance can mean many things, and a win in one area might sometimes mean a loss in others.

清单

影响性能的关键是三角面的数量,以及一些效果的计算量,性能优化清单

  • 关注内存释放:对象及模型删除时,不光要从场景中移除,还需要手动释放资源(包括材质及 child 级联的资源)
    • 确定不需要的,记得 dispose
    • 后续可能加回来的,通过 object.visible、material.opacity、light.intensity 控制
  • 减少 draw calls
    • MergedGeometry,缺点:会失去对单个模型的控制
    • InstancedMesh,使用实例化对象渲染大量具有相同几何体与材质,但具有不同世界变换的物体,帮助你减少 draw call 的数量,提升整体性能,缺点:会失去对单个模型的控制
  • 模型轻量化
    • 减面:如减少曲线、曲面的使用
    • 合并几何体,纹理资源合并
    • 模型及资源压缩:选取压缩比较高的模型格式,纯字符编码开启 gzip 压缩
    • 轻量化展示:视锥体剔除(three.js 默认开启),遮挡剔除(three.js 不支持),背面剔除(three.js 默认实现)
    • 多重 LOD:越远模型越粗糙,越近越精细,通常情况下,你会创建多个几何体,比如说三个,一个距离很远(低细节),一个距离适中(中等细节),还有一个距离非常近(高质量)。
    • 批量绘制
  • 少用性能消耗大方案
    • 反射真实的环境,改为 envMap,做个假的环境贴图
    • 阴影等,如果无必要,不添加
    • 粒子群进行转换,而不是每个粒子
    • 纹理图片尺寸一定得是 2 的幂次方,并尽可能的小
    • 使用自定义 ShaderMaterial,多用 GPU 进行计算
    • 使用代价较低的材质:StandardMaterial > PhongMaterial > LambertMaterial > BasicMaterial
    • 使用少的光源,同时避免动态增减光源,这会导致 Renderer 重编译 shader programs,推荐使用过 light.visible = false 或 light.intensity = 0;点光源相比其他阴影类型更加昂贵
    • 关闭抗锯齿:内置的抗锯齿,在使用了 post-processing 会失效(at least in WebGL 1),你需要使用 FXAA 或 SMAA
    • 使视窗尽可能小
  • 其他
    • 复用 geometries, materials, textures 等
    • 仅在需要的时候 render
    • 通过 Layers 优化显示、隐藏、移除、添加操作?
    • 对于静态对象或很少移动的对象,设置 matrixAutoUpdate 为 false,需要时再手动调用 object.updateMatrix
    • 透明对象是很慢的,尽可能使用少的透明对象,或使用 alphatest,而不是标准的透明度,会更快一点
    • 使用缓存缓存模型:IndexDB
    • 启用 renderer.powerPreference 为 high-performance 属性
    • 每个后置处理器需要重新渲染整个场景,考虑是否可以组合需要的处理器进一个自定义的处理器,会获得很好的性能提升
  • 非必要不要开启 preserveDrawingBuffer
    • WebGL 有两个缓冲区,一个用于绘制,一个用于显示,当你需要绘制时,你有两个选型
    • 拷贝 drawing buffer 到 display buffer:这回涉及到缓存的拷贝操作
    • 交换两个 buffer:这个操作时即时的,preserveDrawingBuffer 设置为 false 时,表示直接交换即可
  • Web Workers
    • 开销较大的数据处理或计算任务放到 worker 中
    • 用线程池可减少 worker 频繁创建
    • 超大模型拆分为多份数据,分包发送接收,分包进行解压,通过索引序列保证顺序
    • 大的场景采取分包流式加载,加载解析完成后丢到主线程进行渲染,类似地图的分片加载
    • Worker 数量:navigator.hardwareConcurrency 表示机器最多可并行执行的任务数量,因此 Worker 不大于这个即可
    • 通信开销:默认通过结构化克隆完成,数据量较大时比较耗时,不大时可以通过 JSON.stringify/parse,此外 ArrayBuffer、ImageBitMap 支持按照引用传递,不会有克隆过程
  • 离屏渲染
    • OffscreenCanvas 是一个全局的 Canvas 对象,在窗口环境和 worker 环境均有效
    • Transfer:在 Worker 线程创建一个 OffscreenCanvas 做后台渲染,再把渲染好的缓存区 Transfer 回主线程显示
    • Commit:主线程中当前 DOM 数中的 Canvas 元素产生一个 OffscreenCanvas,发送给 Worker 线程进行渲染,渲染结构直接 Commit 到浏览器 Display Compositor 输出到当前窗口,相当于 Worker 线程直接更新 Canvas 元素的内容
  • WebAssembly
  • GPU 拾取
    • Raycaster:采用包围盒过滤,计算光线与每个三角面是否相交实现
    • GPU 拾取:创建选取材质,创建一个与渲染呈现的 Mesh 位置相同的 pickingMesh,将场景中每个 pickingMesh 模型的材质替换成不同的颜色,读取鼠标位置像素颜色,根据颜色判断是否位置的物体
    • 官方示例:gpu picking

原因

一顿研究后,找到了具体的原因,数据中有很多的单线段,导致我创建了巨多的 Line 对象,这就是为啥 GPU 利用率低的主要原因。

为什么 GPU 利用率低,CPU 利用很高

  • 原因就是太多的 draw calls,draw calls 是 CPU 发送给 GPU 的命令,当你发起一次 draw call 时,CPU 需要准备渲染状态,比如分配内存、绑定缓冲区等
  • 尽管 GPU 渲染很快,但 CPU 在处理 GPU 的绘制调用时很慢,从而导致 CPU 利用率高,GPU 不高的现象,缓慢而陈旧的 CPU 在准备和发送数据时限制了 GPU,CPU 不能足够快地向 GPU 提供信息,这主要是 CPU 的瓶颈
  • 使 draw call 昂贵的不是数据量,让绘制调用变得昂贵的是 GPU 命令的准备,即 CPU 到 GPU 的通信,无论你有一个三角形还是一千个三角形,绘制调用都需要这个开销
  • 合并 geometry 就是从减少 draw call 角度出发,当然针对相同的 geometry,最佳的方式是使用 InstancedGeometry,它节省内存、减少发送给 GPU 的重复数据

我的屌丝电脑性能测试,创建很多球体做性能测试,简单记录下低于 60fps 情况

  • 使用球体时,达到 7000 会导致低于 60fps,6000 时还能稳定在 60fps
  • 使用立方体,到到 10000 会导致低于 60fps

针对大量线段的优化

  • 使用 merge 的方式可以合并面和点,但针对线(Line)会导致首尾相连接,那么如何提高渲染大量不连接且顶点数量不同的线段呢
  • 这时候就是 LineSegment 发挥作用的时候了,将所有线段通过每两个点一条线的方式表示出来,缺点是数据结构不太符合常规,需要处理一下,可以使用 index 复用顶点

相关资料



留言