有这么一种场景:需要渲染一座桥,桥有很多桥柱,桥柱除了位置与倾斜角度不完全相同外,其他均相同,由于桥柱数量很大,使用three.js绘制较为卡顿,如何优化?注意,要求后续能选中某个桥柱
three.js官方教程里提到,大量对象的优化 - three.js manual (threejs.org),使用合并几何体
为什么合并几何体能优化绘制大量对象时的性能呢?
这得引出一个概念:绘制调用(draw call)
绘制调用(draw call)是指渲染引擎向GPU发送绘制命令的过程,每个绘制调用都会告诉GPU绘制一个或多个三维物体或几何体
在图形渲染中,绘制调用的数量对性能有很大影响,较少的绘制调用通常意味着更高的性能,因为GPU在处理绘制调用时需要切换上下文和状态,这会导致一定的开销
在three.js中,由于绘制一个几何体需要调用一次draw call,绘制很多几何体就很消耗性能,所以合并多个几何体为一个几何体能减少draw call,从而实现绘制性能优化
合并几何体会有一个突出的问题:无法单独选择其中某个几何体
由于多个几何体合并为一个几何体,所以已经无法选择原来的某个几何体,即无法拾取单个几何体
考虑到后续需要能选中桥柱,这个方案舍弃
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来实现绘制大量几何体的性能优化
初始情况下使用多个几何体来加载桥柱,其实就是多个圆柱体,数量为10980
示例代码如下:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
html, |
|
body, |
|
canvas { |
|
height: 100%; |
|
width: 100%; |
|
margin: 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
{ |
|
"imports": { |
|
"three": "https://unpkg.com/three/build/three.module.js", |
|
"three/addons/": "https://unpkg.com/three/examples/jsm/" |
|
} |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
|
结果如下:
在笔者的电脑上只有20FPS,拾取功能(选择单个柱子)正常
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 = 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); |
在笔者的电脑上有60FPS,拾取功能(选择单个柱子)正常