0


Three 之 three.js (webgl)性能优化、提高帧率的思路/方向整理

Three 之 three.js (webgl)性能优化、提高帧率的思路/方向整理

一、简单介绍

Three js 开发的一些知识整理,方便后期遇到类似的问题,能够及时查阅使用。

本节介绍, three.js (webgl)性能优化方面,整理一下常用的优化方法或者方向,供大家一个优化思考的方向,如果有不足之处,欢迎指出,或者你有更好的方法,欢迎留言。

用ThreeJS加载大模型总要遇到性能问题,性能优化一般包括加载性能优化、渲染帧率优化、内存优化等。

在CPU、显卡等硬件设备性能一定情况下,为了更好的用户体验,一般项目开发中需要对Threejs项目代码进行性能优化,避免卡顿现象,所以下面就对Threejs性能优化几种方式进行简单介绍。

模型面数比较少的情况下,不仅threejs渲染模型的时候运行性能高,通过网络加载面数少的模型,因为文件体积小,加载速度自然也快。

二、优化方向

1、

  1. 创建多量物体时 BufferGeometry (或者InstancedBufferGeometry)创建

如果通过Threejs提供的几何体类,比如球体、圆柱等几何体类创建几何体,最好使用基类是

  1. BufferGeometry

而不是

  1. Geometry

几何体类。

2、合理执行渲染方法

  1. .render()

Threejs渲染器的

  1. .render()

方法每次执行都需要调用大量的CPU、GPU等硬件资源,所以为了提高渲染性能,可以考虑尽量减少

  1. .render()

的执行次数,这里说的尽量减少

  1. .render()

的执行次数,不是简单地说越少越好,而是要在考虑渲染效果的基础上减少

  1. .render()

的执行次数。如果场景有动画效果,就必须周期性执行

  1. .render()

更新canvas画布图像,如果场景默认是静态的,没有动画,比如展示一个产品、建筑或机械零件的三维模型,只需要在鼠标旋转缩放三维模型,触发

  1. .render()

执行即可,在没有发生鼠标事件的时候,可以不执行

  1. .render()

不控制Threejs渲染器渲染帧率,通过浏览器提供的

  1. requestAnimationFrame()

函数实现周期性渲染,理想的情况下

  1. requestAnimationFrame()

可以实现渲染帧率60FPS,如果threejs需要渲染的场景的比较复杂或者说浏览器所在设备硬件性能不好,可能默认执行效果达不到60FBS。

  1. function render() {
  2. requestAnimationFrame(render);
  3. renderer.render(scene, camera);
  4. }
  5. render();

对一些有动画的场景,可以适当控制

  1. requestAnimationFrame()

函数周期性执行渲染的次数,比如把默认60FBS设置为30FBS。具体设置方式可以参考本站发布文章《Three.js控制渲染帧率(FPS)》

对于大多数一般处于静态的三维场景,可以不一直周期性执行threejs渲染器方法

  1. .render()

,根据需要执行

  1. .render()

,比如通过鼠标旋转模型,就通过鼠标事件触发

  1. .render()

执行,或者在某个时间段出现动画,就在这个时间段周期性执行

  1. .render()

,过了这个时间段,就恢复原来状态。

比如鼠标控件

  1. OrbitControls

,当通过

  1. OrbitControls

控件旋转缩放三维模型的时候,触发渲染器进行渲染。

  1. // 渲染函数
  2. function render() {
  3. renderer.render(scene, camera);
  4. }
  5. render();
  6. var controls = new THREE.OrbitControls(camera);
  7. //监听鼠标事件,触发渲染函数,更新canvas画布渲染效果
  8. controls.addEventListener('change', render);

3、减少没必要执行的代码在周期性渲染函数中的执行

threejs会通过

  1. requestAnimationFrame()

周期性执行一个渲染函数render(),在渲染函数中除了渲染器

  1. .render()

方法,其它的尽量放在渲染函数外面,如果必须放在里面的,可以加上if判断尽量加上,不要每次执行render函数的时候,每次都执行没必要执行的代码。

4、减少模型面数,必要可以用法线贴图增加模型细节替代

Threejs渲染场景中网格模型

  1. Mesh

的时候,如果网格模型

  1. Mesh

几何体的三角形面数数量或者说顶点数量越多,那么需要的CPU和GPU的运算量就越大,几何体顶点数据占用的内存就多,这时候对于Threejs每次执行渲染

  1. .render()

,花费的时间就越多,如果三角形面数过多,可能渲染帧率就会下降,鼠标操作三维模型的时候可能就会比较卡顿。

对于项目中使用的三维模型,3D美术往往会进行减面优化,具体减面过程对于程序员而言一般不用关心。

对于曲面而言,减面过多,可能会影响显示效果,所以减面程度要控制好。

对于曲面模型,使用法线贴图可以在不影响显示质量的情况下有效减少模型面数,法线贴图会通过图片像素值记录模型表面的几何细节,只需要3D美术对模型表面很多几何细节进行减面后,然后导出法线贴图,提供给程序员加载即可。简单地说就是通过法线贴图可以表达三维模型表面的丰富几何细节。

5、共享几何体和材质

不同的网格模型如果可以共享几何体或材质,最好采用共享的方式,如果两个网格模型无法共享几何体或材质,自然不需要共享,比如两个网格模型的材质颜色不同,这种情况下,一般要分别为网格模型创建一个材质对象。

6、渲染帧率的优化,其实就是合理调用 render (补充第 2 点),有实操些代码

帧率优化的思路主要是需要时才渲染,无操作时不调用render()。什么时候需要调用渲染呢?主要包含以下情况:

  • scene中object的增、删、改
  • object被选中、反选
  • 相机位置、观察点变化
  • 渲染区域大小变化
  • ...

于是我们需要注意哪些操作会触发这些变化,主要有以下操作:

  • scene.add/remove方法被调用 (当模型被加载、移除等)
  • object material的变化,位置、缩放、旋转、透明度等变化
  • OrbitControls的的变化
  • camera的 'change'事件
  • 鼠标的 mousedown/mouseup/mousemove等事件
  • 键盘的w/a/s/d/up/down/left/right arrow等

1)使用代码片段

  1. this.controls.addEventListener('change', () => {
  2. this.enableRender()
  3. })
  4. window.addEventListener('keydown', (e: KeyboardEvent) => {
  5. // can also check which key is pressed...
  6. this.enableRender()
  7. }
  8. this.renderer.domElement.addEventListener('mousedown', (e) => {
  9. this.enableRender()
  10. })
  11. this.renderer.domElement.addEventListener('mousemove', (e) => {
  12. if (/* we can add more constraints here */) {
  13. this.enableRender()
  14. }
  15. })
  16. this.renderer.domElement.addEventListener('mouseup', (e) => {
  17. !this.mouseMoved && this.selectHandler(e)
  18. this.enableRender()
  19. })

2)其他 参考 封装类

  1. /**
  2. * This class implemented setTimeout and setInterval using RequestAnimationFrame
  3. */
  4. export default class RafHelper {
  5. readonly TIMEOUT = 'timeout'
  6. readonly INTERVAL = 'interval'
  7. private timeoutMap: any = {} // timeout map, key is symbol
  8. private intervalMap: any = {} // interval map
  9. private run (type = this.INTERVAL, cb: () => void, interval = 16.7) {
  10. const now = Date.now
  11. let startTime = now()
  12. let endTime = startTime
  13. const timerSymbol = Symbol('')
  14. const loop = () => {
  15. this.setIdMap(timerSymbol, type, loop)
  16. endTime = now()
  17. if (endTime - startTime >= interval) {
  18. if (type === this.intervalMap) {
  19. startTime = now()
  20. endTime = startTime
  21. }
  22. cb()
  23. if (type === this.TIMEOUT) {
  24. this.clearTimeout(timerSymbol)
  25. }
  26. }
  27. }
  28. this.setIdMap(timerSymbol, type, loop)
  29. return timerSymbol
  30. }
  31. private setIdMap (timerSymbol: symbol, type: string, loop: (time: number) => void) {
  32. const id = requestAnimationFrame(loop)
  33. if (type === this.INTERVAL) {
  34. this.intervalMap[timerSymbol] = id
  35. } else if (type === this.TIMEOUT) {
  36. this.timeoutMap[timerSymbol] = id
  37. }
  38. }
  39. public setTimeout (cb: () => void, interval: number) {
  40. return this.run(this.TIMEOUT, cb, interval)
  41. }
  42. public clearTimeout (timer: symbol) {
  43. cancelAnimationFrame(this.timeoutMap[timer])
  44. }
  45. public setInterval (cb: () => void, interval: number) {
  46. return this.run(this.INTERVAL, cb, interval)
  47. }
  48. public clearInterval (timer: symbol) {
  49. cancelAnimationFrame(this.intervalMap[timer])
  50. }
  51. }

7、网格合并

多数情况下使用组可以很容易地操纵和管理大量网格。但是当对象的数量非常多时,性能就会成为一个瓶颈。使用组,每个对象还是独立的,仍然需要对它们分别进行处理和渲染。通过
THREE.Geometry.merge() 函数,你可以将多个几何体合并起来创建一个联合体。

当我们使用普通组的情况,绘制20000个立方体,帧率在15帧左右,如果我们选择合并以后,再绘制两万,就会发现,我们可以轻松的渲染20000个立方体,而且没有性能的损失。合并的代码如下:

  1. //合并模型,则使用merge方法合并
  2. var geometry = new THREE.Geometry();
  3. //merge方法将两个几何体对象或者Object3D里面的几何体对象合并,(使用对象
  4. 的变换)将几何体的顶点,面,UV分别合并.
  5. //THREE.GeometryUtils: .merge() has been moved to Geometry.
  6. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead.
  7. for(var i=0; i<gui.numberOfObjects; i++){
  8. var cube = addCube();
  9. cube.updateMatrix();
  10. geometry.merge(cube.geometry, cube.matrix);
  11. }
  12. scene.add(new THREE.Mesh(geometry, cubeMaterial));

THREE.GeometryUtils.merge() 已经将此方法移动到了 THREE.Geometry 对象的上面了,我们使用 addCube 方法进行立方体的创建,为了确保能正确的定位和旋转合并的 THREE.Geometry 对象,我们不仅向 merge 函数提供 THREE.Geometry 对象,还提供该对象的变换矩阵。当我们将此矩阵添加到 merge 函数后,那么合并的方块将被正确定位。

网格合并的优缺点

缺点:组能够对每个单独的个体进行操作,而合并网格后则失去对每个对象的单独控制。想要移
动、旋转或缩放某个方块是不可能的。
优点:性能不会有损失。因为将所有的的网格合并成为了一个,性能将大大的增加。如果需要创建大型的、复杂的几何体。我们还可以从外部资源中创建、加载几何体。

8、尽量重用Material和Geometry

这里以Material和Geometry为例(使用比较频繁)

  1. for (var i = 0; i < 100; i++) {
  2. var material = new THREE.MeshBasicMaterial();
  3. var geometry = new THREE.BoxGeometry(10, 10, 10);
  4. var mesh = new THREE.Mesh(geometry, material);
  5. scene.add(mesh);
  6. }
  7. 尽量替换为
  8. var material = new THREE.MeshBasicMaterial();
  9. var geometry = new THREE.BoxGeometry(10, 10, 10);
  10. for (var i = 0; i < 100; i++) {
  11. var mesh = new THREE.Mesh(geometry, material);
  12. scene.add(mesh);
  13. }

9、删除模型时,将材质和几何体从内存中清除

  1. 1. item.geometry.dispose(); //删除几何体
  2. 2. item.material.dispose(); //删除材质

10、在循环渲染中避免使用更新 (真正需要更新才更新,补充 2,含代码 )

这里的更新指的是当前的几何体、材质、纹理等发生了修改,需要 Three.js 重新更新显存的数据,具体包括:

  1. 几何体:
  2. geometry.verticesNeedUpdate = true; //顶点发生了修改
  3. geometry.elementsNeedUpdate = true; //面发生了修改
  4. geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
  5. geometry.uvsNeedUpdate = true; //uv映射发生了修改
  6. geometry.normalsNeedUpdate = true; //法向发生了修改
  7. geometry.colorsNeedUpdate = true; //顶点颜色发生的修改
  8. 材质:
  9. material.needsUpdate = true
  10. 纹理:
  11. texture.needsUpdate = true;

如果它们发生更新,则将其设置为true,Three.js会通过判断,将数据重新传输到显存当中,并将配置项重新修改为false。这是一个很耗运行效率的过程,所以我们尽量只在需要的时候修改,不要放到render()方法当中循环设置。只在需要的时候渲染

如果在没有操作的时候,让循环一直渲染属于浪费资源,接下来我来带给大家一个只在需要时渲染的方法。
首先在循环渲染中加入一个判断,如果判断值为true时,才可以循环渲染:

  1. var renderEnabled;
  2. function animate() {
  3. if (renderEnabled) {
  4. renderer.render(scene, camera);
  5. }
  6. requestAnimationFrame(animate);
  7. }
  8. animate();

然后设置一个延迟器函数,每次调用后,可以将 renderEnabled 设置为 true ,并延迟三秒将其设
置为 false ,这个延迟时间大家可以根据需求来修改:

  1. //调用一次可以渲染三秒
  2. let timeOut = null;
  3. function timeRender() {
  4. //设置为可渲染状态
  5. renderEnabled = true;
  6. //清除上次的延迟器
  7. if (timeOut) {
  8. clearTimeout(timeOut);
  9. }
  10. timeOut = setTimeout(function () {
  11. renderEnabled = false;
  12. }, 3000);
  13. }

接下来,我们在需要的时候调用这个 timeRender() 方法即可,比如在相机控制器更新后的回调
中:

  1. controls.addEventListener('change', function(){
  2. timeRender();
  3. });

如果相机位置发生变化,就会触发回调,开启循环渲染,更新页面显示。

如果我们添加了一个模型到场景中,直接调用一下重新渲染即可:

  1. scene.add(mesh);
  2. timeRender();

最后,一个重点问题,就是材质的纹理由于是异步的,我们需要在图片添加完成后,触发回调。好在
Three.js已经考虑到了这一点,Three.js的静态对象THREE.DefaultLoadingManager的onLoad回调会在
每一个纹理图片加载完成后触发回调,依靠它,我们可以在Three.js的每一个内容发生变更后触发重新
渲染,并且在闲置状态会停止渲染。

  1. //每次材质和纹理更新,触发重新渲染
  2. THREE.DefaultLoadingManager.onLoad = function () {
  3. timeRender();
  4. };

11、Instance、Merge 性能对比

1)Instance 多实例化几何体

同一个Geometry , 同一个 material ,但可以通过索引轻松控制每一个个体大小、位置等

  1. let insGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
  2. //创建具有多个实例的实例化几何体
  3. let insMesh = new THREE.InstancedMesh(insGeometry, material, total);
  4. //修改位置
  5. let transform = new THREE.Object3D();
  6. for (let index = 0; index < total; index++) {
  7. transform.position.set(Math.random() * 2000, Math.random() * 2000, Math.random() * 2000);
  8. transform.scale.set(Math.random() * 50 + 50, Math.random() * 50 + 50, Math.random() * 50 + 50);
  9. transform.updateMatrix();
  10. //修改实例化几何体中的单个实例的矩阵以改变大小、方向、位置等
  11. insMesh.setMatrixAt(index, transform.matrix);
  12. }
  13. scene.add(insMesh);

2)Merge 合并几何体

不同的 Geometry ,同一个 material 没有索引可以使用,合并后变为一个个体 ,难以单独控制

  1. let geometries = [];
  2. let transform = new THREE.Object3D();
  3. for (let index = 0; index < total; index++) {
  4. let geometry = new THREE.BoxBufferGeometry(Math.random() * 50 + 50, Math.random() * 50 + 50, Math.random() * 50 + 50);
  5. transform.position.set(Math.random() * 2000, Math.random() * 2000, Math.random() * 2000);
  6. transform.updateMatrix();
  7. geometry.applyMatrix4(transform.matrix);
  8. geometries.push(geometry);
  9. }
  10. let mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
  11. let mergedMesh = new THREE.Mesh(mergedGeometry, material2);
  12. scene.add(mergedMesh);

参考博文

1、ThreeJS 性能优化 - 渲染帧率优化 - 知乎

2、Three.js渲染性能优化

3、ThreeJS的性能优化方面

4、Threejs 性能优化之(多实例渲染 and 合并)


本文转载自: https://blog.csdn.net/u014361280/article/details/124285654
版权归原作者 仙魁XAN 所有, 如有侵权,请联系我们删除。

“Three 之 three.js (webgl)性能优化、提高帧率的思路/方向整理”的评论:

还没有评论