threejs性能优化

转自:https://blog.csdn.net/qq_26887683/article/details/120842634

three.js是目前国内开发Web3D应用最多的第三方库,它提供了非常多的3D显示功能。在使用的时候,虽然three.js 本身做了优化,但是在较大分辨率下,加载较大或者较多模型时会出现,帧率会越低,给人感觉就越卡,因此性能方面的优化对提高视觉体验有着积极影响。以下是我在项目(vue+threejs)开发结合度娘总结的一些思路,希望能有所帮助。

合理执行渲染方法

因为默认情况下requestAnimationFrame()每秒执行60次,如果在里面加个for循环,代码效率就会严重影响,同时还要减少浮点计算,系统对浮点计算开支比较大,尽量写成小数乘法。

在一些特定的应用中没有必要保持Threejs渲染频率为60FPS,那么可以通过Threejs渲染时间判断来控制Threejs渲染频率,比如设置为30FPS。

下面代码通过时钟对象.Clock的.getDelta()方法获得threejs两帧渲染时间间隔,然后通过时间判断来控制渲染器渲染方法.render()每秒执行次数:

复制代码
// 创建一个时钟对象Clock
var clock = new THREE.Clock();
// 设置渲染频率为30FBS,也就是每秒调用渲染器render方法大约30次
var FPS = 30;
var renderT = 1 / FPS; //单位秒  间隔多长时间渲染渲染一次
// 声明一个变量表示render()函数被多次调用累积时间
// 如果执行一次renderer.render,timeS重新置0
var timeS = 0;
function render() {
  requestAnimationFrame(render);
  //.getDelta()方法获得两帧的时间间隔
  var T = clock.getDelta();
  timeS = timeS + T;
  // requestAnimationFrame默认调用render函数60次,通过时间判断,降低renderer.render执行频率
  if (timeS > renderT) {
    // 控制台查看渲染器渲染方法的调用周期,也就是间隔时间是多少
    console.log(`调用.render时间间隔`,timeS*1000+'毫秒');
    renderer.render(scene, camera); //执行渲染操作
    ...
    //renderer.render每执行一次,timeS置0
    timeS = 0;
  }
}
render();
复制代码

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

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

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

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

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

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

渲染帧率的优化,其实就是合理调用 render ,有实操些代码

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

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

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

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

使用代码片段

复制代码
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()
      })
复制代码

其他 参考 封装类

复制代码
/**
 * 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])
  }
}
复制代码

 

 

共享几何体和材质

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

相同或者相似类型的对象生成时多使用clone()方法,例如生成多个类似的立方体,推荐使用group,结合clone()方法,代码如下

复制代码
const group = new THREE.Group()
const bar = new THREE.Mesh(barGeo, material)
bar.scale.set(0.3, 0.3, 0.3)
for (let i = 0; i < 80; i++) {
    const cBar = bar.clone()
    group.add(cBar)
}

-----------------------------------------------------
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); }
复制代码

 

 

 

 

使用性能检测插件(stats.js)监测页面性能

// 引入stats.js
import Stats from 'three/examples/js/libs/stats.min.js'
const stats = new Stats()
// 设置stats样式
stats.dom.style.position = 'absolute';
stats.dom.style.top = '0px';
document.body.appendChild(stats.dom);

在渲染函数中需要添加如下代码:

复制代码
function Animate() {
    requestAnimationFrame(Animate);
    Render();
}
function Render() {
    // 更新stats
    stats.update();
    render.render(scene,camera);
}
复制代码

对粒子群进行转换,而不是每个粒子

使用THREE.Sprite时,可以更好地控制单个粒子,但是当使用大量的粒子的时候,这个方法的性能会降低,并且会更复杂。此时可以使用THREE.SpriteCloud,可以轻松地管理大量的粒子,进行整体操作,此时对单个粒子的控制能力会减弱。

模型的面越少越好,模型过于细致会增加渲染开销

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

three场景导入模型时,可以在保证最低清晰度的时候,降低模型的复杂度,面越多,模型越大,加载所需开销就越大

分时加载

  • 调查显示100ms内的响应能让用户感觉非常流畅。50ms是 Nicholas 针对 JavaScript 得出的最佳经验值,setTimeout 延时25ms,25ms 保证主流浏览器都顺畅,可以使用类似的方法来优化three.js程序。
  • 初始化方法以及渲染方法可以适当添加延时以分散同时渲染的压力。
  • 当存在多个模型动画时,根据实际情况可以将多个动画拆分,再可以对每个动画requestAnimationFrame分别设置渲染频率。

页面销毁时手动调用dispose方法,清除延时

复制代码
beforeDestroy () {
    clearTimeout()
    try {
        this.scene.clear()
        this.renderer.dispose()
        this.renderer.forceContextLoss()
        this.renderer.content = null
        // cancelAnimationFrame(animationID) // 去除animationFrame
        const gl = this.renderer.domElement.getContext('webgl')
        gl && gl.getExtension('WEBGL_lose_context').loseContext()
    } catch (e) {
        console.log(e)
    }
}
复制代码

一个网格模型Mesh是包含几何体geometry和材质对象Material的,几何体geometry本质上就是顶点数据,Three.js通过WebGL渲染器解析几何体的时候会调用WebGL API创建顶点缓冲区来存储顶点数据。

如果仅仅执行scene.remove(Mesh)只是把网格模型从场景对象的.children属性中删除,解析网格模型Mesh几何体的顶点数据通过WebGL API创建的顶点缓冲区占用的内存并不会释放。

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

从内存中删除对象或者删除几何体时不要忘记调用以下方法,因为可能导致内存泄漏

geometry.dispose() // 删除几何体

material.dispose() // 删除材质

加载/渲染时间长的添加loading效果

 当加载较大模型或者渲染比较复杂的模型时,页面会有较长时间卡顿,影响用户体验。可以添加loading效果,降低用户等待焦虑。

网格合并

多数情况下使用组可以很容易地操纵和管理大量网格。但是当对象的数量非常多时,性能就会成为一个瓶颈。使用组,每个对象还是独立的,仍然需要对它们分别进行处理和渲染。通过
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 函数后,那么合并的方块将被正确定位。

网格合并的优缺点

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

 

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

这里的更新指的是当前的几何体、材质、纹理等发生了修改,需要 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();
};

 

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);
复制代码

渲染3D的显卡建议设置为独立显卡

在性能和功耗方面,集成显卡具有一般性能的特点,但基本可以满足一些日常应用,与独立显卡相比,热功耗低。虽然独立显卡的性能很强,但其热量和功耗都比较高。独立显卡在三维性能上优于集成显卡。

修改浏览器GUP加速相关设置 

Chrome浏览器:
chrome://flags/#enable-gpu-rasterization   GPU rasterization  设置为Enabled
chrome://flags/#ignore-gpu-blocklist  Override software rendering list   设置为Enabled
chrome://flags/#enable-zero-copy    Zero-copy rasterizer   设置为Enabled
Firefox浏览器:
  1. 要想GPU加速文本的功能,不仅仅要下载最新的nightlyBuild火狐(Minefield)之外,还要通过以下方法操作才能开启该功能:
  2. 进入about:config配置页面并搜索gfx.font
  3. 双击gfx.font_rendering.directwrite.enabled打开这项功能;
  4. 点右键新建一个integer,命名为mozilla.widget.render-mode;
  5. 为该integer赋值为6;
  6. 重启浏览器。
Edge(win10)浏览器:
  1. 使用 Windows + I 快捷键打开「Windows 设置」——导航到「系统」——「显示」选项页
  2. 点击「多显示器设置」下的「图形设置」链接,打开「图形设置」专属配置页面
  3. 在「图形性能首选项」的下拉列表中选择「通用应用」——再在「选择应用」下拉列表中添加 Microsoft Edge 浏览器。
  4. 添加好之后点击已添加的 Microsoft Edge,再点击「选项」按钮
  5. 在弹出的「图形规格」选项卡中可以看到当前系统中的所有显卡,选择「高性能」并「保存」即可指定 Microsoft Edge 永久使用使用性能最高的 GPU。
  6. 完成上述操作步骤后,再重新启动下 Microsoft Edge,它现在就应该会使用 PC 的独立显卡进行渲染任务了。

 

tips:用threejs做大分辨率下的显示应用时,需要考虑3D渲染的显卡性能以及显卡最大分辨率与显示屏分辨率的对比情况,如果在做了相关优化之后GPU的占用率仍然偏高,页面动效卡顿,三维效果不理想,甚至出现有时候因GPU超负荷而是电脑卡死的情况,这时候就需要考虑升级显卡配置了。

 

————————————————

版权声明:本文为CSDN博主「仙魁XAN」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014361280/article/details/124285654

 

参考博文

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

2、Three.js渲染性能优化

3、ThreeJS的性能优化方面

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

posted @   SimoonJia  阅读(3179)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示