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效果
网格合并
多数情况下使用组可以很容易地操纵和管理大量网格。但是当对象的数量非常多时,性能就会成为一个瓶颈。使用组,每个对象还是独立的,仍然需要对它们分别进行处理和渲染。通过
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浏览器:
- 要想GPU加速文本的功能,不仅仅要下载最新的nightlyBuild火狐(Minefield)之外,还要通过以下方法操作才能开启该功能:
- 进入about:config配置页面并搜索gfx.font
- 双击gfx.font_rendering.directwrite.enabled打开这项功能;
- 点右键新建一个integer,命名为mozilla.widget.render-mode;
- 为该integer赋值为6;
- 重启浏览器。
Edge(win10)浏览器:
- 使用 Windows + I 快捷键打开「Windows 设置」——导航到「系统」——「显示」选项页
- 点击「多显示器设置」下的「图形设置」链接,打开「图形设置」专属配置页面
- 在「图形性能首选项」的下拉列表中选择「通用应用」——再在「选择应用」下拉列表中添加 Microsoft Edge 浏览器。
- 添加好之后点击已添加的 Microsoft Edge,再点击「选项」按钮
- 在弹出的「图形规格」选项卡中可以看到当前系统中的所有显卡,选择「高性能」并「保存」即可指定 Microsoft Edge 永久使用使用性能最高的 GPU。
- 完成上述操作步骤后,再重新启动下 Microsoft Edge,它现在就应该会使用 PC 的独立显卡进行渲染任务了。
tips:用threejs做大分辨率下的显示应用时,需要考虑3D渲染的显卡性能以及显卡最大分辨率与显示屏分辨率的对比情况,如果在做了相关优化之后GPU的占用率仍然偏高,页面动效卡顿,三维效果不理想,甚至出现有时候因GPU超负荷而是电脑卡死的情况,这时候就需要考虑升级显卡配置了。
————————————————
版权声明:本文为CSDN博主「仙魁XAN」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014361280/article/details/124285654
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?