Three.js uses InstancedMesh to achieve performance optimization

1 Introduction

There is such a scenario: a bridge needs to be rendered. The bridge has many bridge pillars. Except for the different positions and tilt angles of the bridge pillars, everything else is the same. Due to the large number of bridge pillars, it is relatively slow to draw using three.js. How to optimization? Note that it is required to select a certain bridge pillar later.

2. Concept

2.1 Merge geometry

The three.js official tutorial mentions the optimization of a large number of objects - three.js manual (threejs.org) , using merged geometries

Why does merging geometry optimize performance when drawing large numbers of objects?

This leads to a concept: draw call (draw call)

A draw call refers to the process in which the rendering engine sends drawing commands to the GPU. Each draw call tells the GPU to draw one or more three-dimensional objects or geometries.

In graphics rendering, the number of draw calls has a great impact on performance. Fewer draw calls usually means higher performance, because the GPU needs to switch contexts and states when processing draw calls, which will cause a certain overhead.

In three.js, since drawing a geometry requires a draw call, drawing many geometries consumes performance, so merging multiple geometries into one geometry can reduce draw calls, thereby optimizing drawing performance.

There is a prominent problem with merging geometries: one of the geometries cannot be selected individually.

Since multiple geometries are merged into one geometry, it is no longer possible to select an original geometry, that is, a single geometry cannot be picked up.

Taking into account the subsequent need to be able to select bridge pillars, this plan was abandoned.

2.2 InstancedMesh

The three.js official API documentation explains this:

InstancedMesh, a special version of Mesh with instanced rendering support . You can use InstancedMesh to render a large number of objects with the same geometry and materials, but different world transformations. Using InstancedMesh will help you reduce the number of draw calls, thereby improving the overall rendering performance of your application

Except for the different positions and tilt angles, the bridge columns are all the same, which meets the requirements of InstancedMesh. At the same time, InstancedMesh allows you to select a single object. You can refer to this official example: three.js examples (threejs.org )

Regarding InstancedMesh, a more detailed explanation can be found in the official documentation: InstancedMesh – three.js docs (threejs.org)

In summary, the author chooses InstancedMesh to optimize the rendering of bridge columns. This article describes the use of InstancedMesh in three.js to optimize the performance of drawing a large number of geometries.

3. Initial situation

In the initial case, multiple geometries are used to load the bridge columns, which are actually multiple cylinders, the number is 10980

The sample code is as follows:

 
 
<!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>

The result is as follows:

image-20230727171954824

On my computer, there is only 20FPS, and the picking function (selecting a single pillar) is normal.

4. InstanceMesh optimization

InstanceMesh can be conceptually understood as a group of geometries. You can find the geometry on this group of InstanceMesh simply based on the instance id. Therefore, the main method of using InstanceMesh is to determine which geometry to select based on the InstanceMesh and instance id. To perform position changes, set colors, etc.

For more detailed instructions on how to use InstanceMesh, please refer to the official documentation and examples:

The author modified the above code to use InstanceMesh. The main code is as follows:

 
 
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 = 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 => {
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

There is 60FPS on the author's computer, and the picking function (selecting a single pillar) is normal

Guess you like

Origin blog.csdn.net/2301_78834737/article/details/132004611