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

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

目录

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

一、简单介绍

二、优化方向

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

2、合理执行渲染方法.render()

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

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

5、共享几何体和材质

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

7、网格合并

8、尽量重用Material和Geometry

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

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

11、Instance、Merge 性能对比

参考博文


一、简单介绍

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

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

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

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

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

二、优化方向

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

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

2、合理执行渲染方法.render()

Threejs渲染器的.render()方法每次执行都需要调用大量的CPU、GPU等硬件资源,所以为了提高渲染性能,可以考虑尽量减少.render()的执行次数,这里说的尽量减少.render()的执行次数,不是简单地说越少越好,而是要在考虑渲染效果的基础上减少.render()的执行次数。如果场景有动画效果,就必须周期性执行.render()更新canvas画布图像,如果场景默认是静态的,没有动画,比如展示一个产品、建筑或机械零件的三维模型,只需要在鼠标旋转缩放三维模型,触发.render()执行即可,在没有发生鼠标事件的时候,可以不执行.render()

不控制Threejs渲染器渲染帧率,通过浏览器提供的requestAnimationFrame()函数实现周期性渲染,理想的情况下requestAnimationFrame()可以实现渲染帧率60FPS,如果threejs需要渲染的场景的比较复杂或者说浏览器所在设备硬件性能不好,可能默认执行效果达不到60FBS。

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
}
render();

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

对于大多数一般处于静态的三维场景,可以不一直周期性执行threejs渲染器方法.render(),根据需要执行.render(),比如通过鼠标旋转模型,就通过鼠标事件触发.render()执行,或者在某个时间段出现动画,就在这个时间段周期性执行.render(),过了这个时间段,就恢复原来状态。

比如鼠标控件OrbitControls,当通过OrbitControls控件旋转缩放三维模型的时候,触发渲染器进行渲染。

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

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

threejs会通过requestAnimationFrame()周期性执行一个渲染函数render(),在渲染函数中除了渲染器.render()方法,其它的尽量放在渲染函数外面,如果必须放在里面的,可以加上if判断尽量加上,不要每次执行render函数的时候,每次都执行没必要执行的代码。

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

Threejs渲染场景中网格模型Mesh的时候,如果网格模型Mesh几何体的三角形面数数量或者说顶点数量越多,那么需要的CPU和GPU的运算量就越大,几何体顶点数据占用的内存就多,这时候对于Threejs每次执行渲染.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)使用代码片段

this.controls.addEventListener('change', () => {
      this.enableRender()
    })

    window.addEventListener('keydown', (e: KeyboardEvent) => {
      // can also check which key is pressed...
      this.enableRender()
    }

      this.renderer.domElement.addEventListener('mousedown', (e) => {
        this.enableRender()
      })
      this.renderer.domElement.addEventListener('mousemove', (e) => {
        if (/* we can add more constraints here */) {
          this.enableRender()
        }
      })
      this.renderer.domElement.addEventListener('mouseup', (e) => {
        !this.mouseMoved && this.selectHandler(e)
        this.enableRender()
      })

2)其他 参考 封装类

/**
 * This class implemented setTimeout and setInterval using RequestAnimationFrame
 */
export default class RafHelper {
  readonly TIMEOUT = 'timeout'
  readonly INTERVAL = 'interval'
  private timeoutMap: any = {} // timeout map, key is symbol
  private intervalMap: any = {} // interval map

  private run (type = this.INTERVAL, cb: () => void, interval = 16.7) {
    const now = Date.now
    let startTime = now()
    let endTime = startTime
    const timerSymbol = Symbol('')
    const loop = () => {
      this.setIdMap(timerSymbol, type, loop)
      endTime = now()
      if (endTime - startTime >= interval) {
        if (type === this.intervalMap) {
          startTime = now()
          endTime = startTime
        }
        cb()
        if (type === this.TIMEOUT) {
          this.clearTimeout(timerSymbol)
        }
      }
    }
    this.setIdMap(timerSymbol, type, loop)
    return timerSymbol
  }

  private setIdMap (timerSymbol: symbol, type: string, loop: (time: number) => void) {
    const id = requestAnimationFrame(loop)
    if (type === this.INTERVAL) {
      this.intervalMap[timerSymbol] = id
    } else if (type === this.TIMEOUT) {
      this.timeoutMap[timerSymbol] = id
    }
  }

  public setTimeout (cb: () => void, interval: number) {
    return this.run(this.TIMEOUT, cb, interval)
  }

  public clearTimeout (timer: symbol) {
    cancelAnimationFrame(this.timeoutMap[timer])
  }

  public setInterval (cb: () => void, interval: number) {
    return this.run(this.INTERVAL, cb, interval)
  }

  public clearInterval (timer: symbol) {
    cancelAnimationFrame(this.intervalMap[timer])
  }
}

7、网格合并

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

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

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

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


网格合并的优缺点

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

8、尽量重用Material和Geometry

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

for (var i = 0; i < 100; i++) {
    var material = new THREE.MeshBasicMaterial();
    var geometry = new THREE.BoxGeometry(10, 10, 10);
    var mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
}


尽量替换为

var material = new THREE.MeshBasicMaterial();
var geometry = new THREE.BoxGeometry(10, 10, 10);
for (var i = 0; i < 100; i++) {
    var mesh = new THREE.Mesh(geometry, material);
    scene.add(mesh);
}

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

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

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

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

几何体:
geometry.verticesNeedUpdate = true; //顶点发生了修改
geometry.elementsNeedUpdate = true; //面发生了修改
geometry.morphTargetsNeedUpdate = true; //变形目标发生了修改
geometry.uvsNeedUpdate = true; //uv映射发生了修改
geometry.normalsNeedUpdate = true; //法向发生了修改
geometry.colorsNeedUpdate = true; //顶点颜色发生的修改

材质:
material.needsUpdate = true

纹理:
texture.needsUpdate = true;

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

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

var renderEnabled;
function animate() {
if (renderEnabled) {
    renderer.render(scene, camera);
}
    requestAnimationFrame(animate);
}
animate();

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

//调用一次可以渲染三秒
let timeOut = null;
function timeRender() {
//设置为可渲染状态
    renderEnabled = true;
//清除上次的延迟器
if (timeOut) {
    clearTimeout(timeOut);
}
    timeOut = setTimeout(function () {
    renderEnabled = false;
}, 3000);
}

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

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

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

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

scene.add(mesh);
timeRender();

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

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

11、Instance、Merge 性能对比

1)Instance 多实例化几何体

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

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

2)Merge 合并几何体

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

 let geometries = [];
 let transform = new THREE.Object3D();
 for (let index = 0; index < total; index++) {
     let geometry = new THREE.BoxBufferGeometry(Math.random() * 50 + 50, Math.random() * 50 + 50, Math.random() * 50 + 50);
     transform.position.set(Math.random() * 2000, Math.random() * 2000, Math.random() * 2000);
     transform.updateMatrix();
     geometry.applyMatrix4(transform.matrix);
     geometries.push(geometry);
 }
 let mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);
 let mergedMesh = new THREE.Mesh(mergedGeometry, material2);
 scene.add(mergedMesh);

参考博文

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

2、Three.js渲染性能优化

3、ThreeJS的性能优化方面

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

猜你喜欢

转载自blog.csdn.net/u014361280/article/details/124285654