Three.js使用InstancedMesh实现性能优化

1. 引言

有这么一种场景:需要渲染一座桥,桥有很多桥柱,桥柱除了位置与倾斜角度不完全相同外,其他均相同,由于桥柱数量很大,使用three.js绘制较为卡顿,如何优化?注意,要求后续能选中某个桥柱

2. 概念

2.1 合并几何体

three.js官方教程里提到,大量对象的优化 – three.js manual (threejs.org),使用合并几何体

为什么合并几何体能优化绘制大量对象时的性能呢?

这得引出一个概念:绘制调用(draw call)

绘制调用(draw call)是指渲染引擎向GPU发送绘制命令的过程,每个绘制调用都会告诉GPU绘制一个或多个三维物体或几何体

在图形渲染中,绘制调用的数量对性能有很大影响,较少的绘制调用通常意味着更高的性能,因为GPU在处理绘制调用时需要切换上下文和状态,这会导致一定的开销

在three.js中,由于绘制一个几何体需要调用一次draw call,绘制很多几何体就很消耗性能,所以合并多个几何体为一个几何体能减少draw call,从而实现绘制性能优化

合并几何体会有一个突出的问题:无法单独选择其中某个几何体

由于多个几何体合并为一个几何体,所以已经无法选择原来的某个几何体,即无法拾取单个几何体

考虑到后续需要能选中桥柱,这个方案舍弃

2.2 InstancedMesh

three.js官方API文档是这样解释:

实例化网格(InstancedMesh),一种具有实例化渲染支持的特殊版本的Mesh。你可以使用 InstancedMesh 来渲染大量具有相同几何体与材质、但具有不同世界变换的物体。 使用 InstancedMesh 将帮助你减少 draw call 的数量,从而提升你应用程序的整体渲染性能

桥柱除了位置与倾斜角度不完全相同外,其他均相同,符合InstancedMesh的要求,同时InstancedMesh是可以选择单个物体的,可以参考这个官方示例:three.js examples (threejs.org)

关于InstancedMesh,更为详细的解释可参考官方文档:InstancedMesh – three.js docs (threejs.org)

综上,笔者选用InstancedMesh来进行桥柱渲染优化,本文记述在three.js中使用InstancedMesh来实现绘制大量几何体的性能优化

3. 初始情况

初始情况下使用多个几何体来加载桥柱,其实就是多个圆柱体,数量为10980

示例代码如下:

<!DOCTYPE html><html lang="en"> <head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title>  <style>    html,    body,    canvas {      height: 100%;      width: 100%;      margin: 0;    }  </style> </head> <body>  <canvas id="canvas"></canvas>   <script type="importmap">		{			"imports": {				"three": "https://unpkg.com/three/build/three.module.js",				"three/addons/": "https://unpkg.com/three/examples/jsm/"			}		}	</script>   <script type="module">    import * as THREE from 'three';    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';    import Stats from 'three/addons/libs/stats.module.js'     const scene = new THREE.Scene();     const raycaster = new THREE.Raycaster();    const mouse = new THREE.Vector2(1, 1);    let mesh;    const color = new THREE.Color();    const white = new THREE.Color().setHex(0xffffff);     // 创建性能监视器    let stats = new Stats();    // 将监视器添加到页面中    document.body.appendChild(stats.domElement)     const canvas = document.querySelector('#canvas');    const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 100000);    camera.position.z = 5;    camera.position.y = 60;    camera.position.x = -1500;     const renderer = new THREE.WebGLRenderer({      canvas: document.querySelector('#canvas'),      antialias: true    });    renderer.setSize(window.innerWidth, window.innerHeight, false)     const controls = new OrbitControls(camera, renderer.domElement);     function animate() {      // 更新帧数      stats.update()       if (scene.children.length > 0) {        raycaster.setFromCamera(mouse, camera);        const intersections = raycaster.intersectObject(scene, true);        if (intersections.length > 0) {          // 获取第一个相交的物体          const intersectedObject = intersections[0].object;           // 更新物体的颜色          intersectedObject.material.color.set(0xff0000); // 设置为红色        }      }       requestAnimationFrame(animate);      renderer.render(scene, camera);    }    animate();     let count = 0    let matrixList = []    fetch("./数据.json").then(res => res.json()).then(res => {      const name = Object.keys(res)      for (let index = 0; index < 60; index++) {         name.filter(item => item.includes("直立桩基")).forEach(item => {          res[item].forEach(element => {            const geometry = new THREE.CylinderGeometry(element.diameter / 2000, element.diameter / 2000, (element.height - element.depth) / 1000, 32);            const material = new THREE.MeshBasicMaterial({ color: 0xffffff });            const cylinder = new THREE.Mesh(geometry, material);             const originalHeight = cylinder.geometry.parameters.height;            cylinder.geometry.translate(0, -originalHeight / 2, 0);             cylinder.position.set(element.x / 1000 * Math.random(), (element.z + element.height) / 1000, element.y / 1000)            scene.add(cylinder);            count++          });        })      }      console.log(count)    })     function onMouseMove(event) {      event.preventDefault();      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;      mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;    }    document.addEventListener('mousemove', onMouseMove);  </script></body> </html>

结果如下:

image-20230727171954824

在笔者的电脑上只有20FPS,拾取功能(选择单个柱子)正常

4. InstanceMesh优化

InstanceMesh在概念上可以理解为这是一组几何体,只需根据instance id即可在这一组InstanceMesh上找到这个几何体,所以InstanceMesh的使用方法主要就是根据InstanceMesh和instance id来确定选择的是那个几何体,从而进行位置变换、设置颜色等

更为详细的InstanceMesh使用方法可参考官方文档和示例:

笔者将上述代码修改为使用InstanceMesh的代码,主体代码如下:

import * as THREE from 'three';import { OrbitControls } from 'three/addons/controls/OrbitControls.js';import Stats from 'three/addons/libs/stats.module.js' const scene = new THREE.Scene(); const raycaster = new THREE.Raycaster();const mouse = new THREE.Vector2(1, 1);let mesh;const color = new THREE.Color();const white = new THREE.Color().setHex(0xffffff); // 创建性能监视器let stats = new Stats();// 将监视器添加到页面中document.body.appendChild(stats.domElement) const canvas = document.querySelector('#canvas');const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 100000);camera.position.z = 5;camera.position.y = 60;camera.position.x = -1500; const renderer = new THREE.WebGLRenderer({    canvas: document.querySelector('#canvas'),    antialias: true});renderer.setSize(window.innerWidth, window.innerHeight, false) const controls = new OrbitControls(camera, renderer.domElement); function animate() {    // 更新帧数    stats.update()     if (mesh) {        raycaster.setFromCamera(mouse, camera);         const intersection = raycaster.intersectObject(mesh);         if (intersection.length > 0) {            const instanceId = intersection[0].instanceId;            console.log(instanceId)            mesh.setColorAt(instanceId, new THREE.Color(0xff0000));            mesh.instanceColor.needsUpdate = true;        }    }     requestAnimationFrame(animate);    renderer.render(scene, camera);}animate(); let count = 0let matrixList = []fetch("./数据.json").then(res => res.json()).then(res => {    const name = Object.keys(res)    for (let index = 0; index < 60; index++) {         name.filter(item => item.includes("直立桩基")).forEach(item => {            res[item].forEach(element => {                count++                matrixList.push(new THREE.Matrix4().makeTranslation(element.x / 1000 * Math.random(), (element.z + element.height) / 1000, element.y / 1000))            });        })    }    console.log(count)     const element = {        diameter: 1200,        depth: 72000    }    const geometry = new THREE.CylinderGeometry(element.diameter / 2000, element.diameter / 2000, element.depth / 1000, 32);    const material = new THREE.MeshBasicMaterial({ color: 0xffffff });     mesh = new THREE.InstancedMesh(geometry, material, count);     for (let i = 0; i < count; i++) {        mesh.setColorAt(i, color);        mesh.setMatrixAt(i, matrixList[i]);    }    scene.add(mesh);}) function onMouseMove(event) {    event.preventDefault();    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;    mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;}document.addEventListener('mousemove', onMouseMove);

image-20230727173402116

在笔者的电脑上有60FPS,拾取功能(选择单个柱子)正常

5. 参考

[1] 大量对象的优化 – three.js manual (threejs.org)

[2] three.js 性能优化的几种方法 – 可爱的黑精灵 – 博客园 (cnblogs.com)

[3] InstancedMesh – three.js docs (threejs.org)

[4] three.js/examples/webgl_instancing_raycast.html at master · mrdoob/three.js (github.com)

[5] three.js examples (threejs.org)

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYjson5v' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片