Three 之 three.js (webgl)性能优化、提高帧率的思路/方向整理
一、简单介绍
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 合并)
版权归原作者 仙魁XAN 所有, 如有侵权,请联系我们删除。