Skip to content

Commit e345468

Browse files
agargaroMugen87
andauthored
Add BatchedMesh LOD and BVH example page (#31239)
* Add webgl_batch_lod_bvh example page * Freeze flag and small refactoring * Update webgl_batch_lod_bvh.html --------- Co-authored-by: Michael Herzog <[email protected]>
1 parent 0a9f81b commit e345468

File tree

4 files changed

+278
-0
lines changed

4 files changed

+278
-0
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"webgl_animation_skinning_morph",
88
"webgl_animation_multiple",
99
"webgl_animation_walk",
10+
"webgl_batch_lod_bvh",
1011
"webgl_camera",
1112
"webgl_camera_array",
1213
"webgl_camera_logarithmicdepthbuffer",
136 KB
Loading

examples/tags.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"physics_rapier_joints": [ "external" ],
1414
"physics_rapier_character_controller": [ "external" ],
1515
"physics_rapier_vehicle_controller": [ "external" ],
16+
"webgl_batch_lod_bvh": [ "external", "accelerate", "performance", "extension", "plugin", "library", "three.ez" ],
1617
"webgl_clipping": [ "solid" ],
1718
"webgl_clipping_advanced": [ "solid" ],
1819
"webgl_clipping_intersection": [ "solid" ],

examples/webgl_batch_lod_bvh.html

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js raycaster - batch - lod - bvh</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
<style>
9+
a {
10+
text-decoration: underline;
11+
}
12+
</style>
13+
</head>
14+
15+
<body>
16+
<div id="info">
17+
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> batch lod bvh - <a href="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/agargaro/batched-mesh-extensions" target="_blank" rel="noopener">@three.ez/batched-mesh-extensions</a><br/>
18+
BatchedMesh with 10 geometries and 500k instances. Each geometry has 5 LODs (4 generated with meshoptimizer). <br>
19+
Frustum culling and raycasting are accelerated by using BVHs (TLAS & BLAS). <br>
20+
See <a href="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/agargaro/batched-mesh-extensions" target="_blank" rel="noopener">main project repository</a> for more information and examples on BatchedMesh extensions.
21+
</div>
22+
23+
<script type="importmap">
24+
{
25+
"imports": {
26+
"three": "../build/three.module.js",
27+
"three/addons/": "./jsm/",
28+
29+
"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/[email protected]/build/index.module.js",
30+
31+
"@three.ez/batched-mesh-extensions": "https://cdn.jsdelivr.net/npm/@three.ez/[email protected]/build/webgl.js",
32+
"bvh.js": "https://cdn.jsdelivr.net/npm/[email protected]/build/index.js",
33+
34+
"@three.ez/simplify-geometry": "https://cdn.jsdelivr.net/npm/@three.ez/[email protected]/build/index.js",
35+
"meshoptimizer": "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
36+
}
37+
}
38+
</script>
39+
40+
<script type="module">
41+
42+
import * as THREE from 'three';
43+
import Stats from 'three/addons/libs/stats.module.js';
44+
import { MapControls } from 'three/addons/controls/MapControls.js';
45+
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
46+
47+
import { acceleratedRaycast, computeBatchedBoundsTree } from 'three-mesh-bvh';
48+
49+
import { createRadixSort, extendBatchedMeshPrototype, getBatchedMeshLODCount } from '@three.ez/batched-mesh-extensions';
50+
import { performanceRangeLOD, simplifyGeometriesByErrorLOD } from '@three.ez/simplify-geometry';
51+
52+
// add and override BatchedMesh methods ( @three.ez/batched-mesh-extensions )
53+
extendBatchedMeshPrototype();
54+
55+
// add the extension functions ( three-mesh-bvh )
56+
THREE.Mesh.prototype.raycast = acceleratedRaycast;
57+
THREE.BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
58+
59+
let stats;
60+
let camera, scene, renderer;
61+
62+
const instancesCount = 500000;
63+
let batchedMesh;
64+
let lastHoveredInstance = null;
65+
const lastHoveredColor = new THREE.Color();
66+
const yellow = new THREE.Color( 'yellow' );
67+
68+
const raycaster = new THREE.Raycaster();
69+
const mouse = new THREE.Vector2( 1, 1 );
70+
const position = new THREE.Vector3();
71+
const quaternion = new THREE.Quaternion();
72+
const scale = new THREE.Vector3( 1, 1, 1 );
73+
const matrix = new THREE.Matrix4();
74+
const color = new THREE.Color();
75+
76+
init();
77+
78+
async function init() {
79+
80+
// environment
81+
camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 600 );
82+
camera.position.set( 0, 10, 50 );
83+
84+
scene = new THREE.Scene();
85+
scene.fog = new THREE.Fog( 0x000000, 500, 600 );
86+
87+
const ambient = new THREE.AmbientLight();
88+
scene.add( camera, ambient );
89+
90+
const dirLight = new THREE.DirectionalLight( 'white', 2 );
91+
camera.add( dirLight, dirLight.target );
92+
93+
// renderer
94+
renderer = new THREE.WebGLRenderer( { antialias: true } );
95+
renderer.setPixelRatio( window.devicePixelRatio );
96+
renderer.setSize( window.innerWidth, window.innerHeight );
97+
document.body.appendChild( renderer.domElement );
98+
99+
raycaster.firstHitOnly = true;
100+
101+
stats = new Stats();
102+
document.body.appendChild( stats.dom );
103+
104+
const controls = new MapControls( camera, renderer.domElement );
105+
controls.maxPolarAngle = Math.PI / 2;
106+
107+
const geometries = [
108+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 1 ),
109+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 2 ),
110+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 3 ),
111+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 4 ),
112+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 5 ),
113+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 1 ),
114+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 3 ),
115+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 3, 1 ),
116+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 4, 1 ),
117+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 5, 3 )
118+
];
119+
120+
// generate 4 LODs (levels of detail) for each geometry
121+
const geometriesLODArray = await simplifyGeometriesByErrorLOD( geometries, 4, performanceRangeLOD );
122+
123+
// create BatchedMesh
124+
const { vertexCount, indexCount, LODIndexCount } = getBatchedMeshLODCount( geometriesLODArray );
125+
batchedMesh = new THREE.BatchedMesh( instancesCount, vertexCount, indexCount, new THREE.MeshStandardMaterial( { metalness: 0.2, roughness: 0.2 } ) );
126+
127+
// enable radix sort for better performance
128+
batchedMesh.customSort = createRadixSort( batchedMesh );
129+
130+
// add geometries and their LODs to the batched mesh ( all LODs share the same position array )
131+
for ( let i = 0; i < geometriesLODArray.length; i ++ ) {
132+
133+
const geometryLOD = geometriesLODArray[ i ];
134+
const geometryId = batchedMesh.addGeometry( geometryLOD[ 0 ], - 1, LODIndexCount[ i ] );
135+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 1 ], 15 );
136+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 2 ], 75 );
137+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 3 ], 125 );
138+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 4 ], 200 );
139+
140+
}
141+
142+
// place instances in a 2D grid with randomized rotation and color
143+
const sqrtCount = Math.ceil( Math.sqrt( instancesCount ) );
144+
const size = 5.5;
145+
const start = ( sqrtCount / - 2 * size ) + ( size / 2 );
146+
147+
for ( let i = 0; i < instancesCount; i ++ ) {
148+
149+
const r = Math.floor( i / sqrtCount );
150+
const c = i % sqrtCount;
151+
const id = batchedMesh.addInstance( Math.floor( Math.random() * geometriesLODArray.length ) );
152+
position.set( c * size + start, 0, r * size + start );
153+
quaternion.random();
154+
batchedMesh.setMatrixAt( id, matrix.compose( position, quaternion, scale ) );
155+
batchedMesh.setColorAt( id, color.setHSL( Math.random(), 0.6, 0.5 ) );
156+
157+
}
158+
159+
// compute blas (bottom-level acceleration structure) bvh ( three-mesh-bvh )
160+
batchedMesh.computeBoundsTree();
161+
162+
// compute tlas (top-level acceleration structure) bvh ( @three.ez/batched-mesh-extensions )
163+
batchedMesh.computeBVH( THREE.WebGLCoordinateSystem );
164+
165+
scene.add( batchedMesh );
166+
167+
// set up gui
168+
const config = {
169+
freeze: false,
170+
useBVH: true,
171+
useLOD: true
172+
};
173+
174+
const bvh = batchedMesh.bvh;
175+
const lods = batchedMesh._geometryInfo.map( x => x.LOD );
176+
const onBeforeRender = batchedMesh.onBeforeRender;
177+
178+
const gui = new GUI();
179+
180+
gui.add( batchedMesh, 'instanceCount' ).disable();
181+
182+
gui.add( config, 'freeze' ).onChange( v => {
183+
184+
batchedMesh.onBeforeRender = v ? () => {} : onBeforeRender;
185+
186+
} );
187+
188+
const frustumCullingFolder = gui.addFolder( 'Frustum culling & raycasting' );
189+
frustumCullingFolder.add( config, 'useBVH' ).onChange( v => {
190+
191+
batchedMesh.bvh = v ? bvh : null;
192+
193+
} );
194+
195+
const geometriesFolder = gui.addFolder( 'Geometries' );
196+
geometriesFolder.add( config, 'useLOD' ).onChange( v => {
197+
198+
const geometryInfo = batchedMesh._geometryInfo;
199+
for ( let i = 0; i < geometryInfo.length; i ++ ) {
200+
201+
geometryInfo[ i ].LOD = v ? lods[ i ] : null;
202+
203+
}
204+
205+
} );
206+
207+
document.addEventListener( 'pointermove', onPointerMove );
208+
window.addEventListener( 'resize', onWindowResize );
209+
onWindowResize();
210+
211+
renderer.setAnimationLoop( animate );
212+
213+
}
214+
215+
216+
function onPointerMove( event ) {
217+
218+
event.preventDefault();
219+
220+
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
221+
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
222+
223+
raycast();
224+
225+
}
226+
227+
228+
function onWindowResize() {
229+
230+
camera.aspect = window.innerWidth / window.innerHeight;
231+
camera.updateProjectionMatrix();
232+
233+
renderer.setSize( window.innerWidth, window.innerHeight );
234+
235+
}
236+
237+
function raycast() {
238+
239+
raycaster.setFromCamera( mouse, camera );
240+
const intersection = raycaster.intersectObject( batchedMesh );
241+
242+
const batchId = intersection.length > 0 ? intersection[ 0 ].batchId : null;
243+
244+
if ( lastHoveredInstance === batchId ) return;
245+
246+
if ( lastHoveredInstance ) {
247+
248+
batchedMesh.setColorAt( lastHoveredInstance, lastHoveredColor );
249+
250+
}
251+
252+
if ( batchId ) {
253+
254+
batchedMesh.getColorAt( batchId, lastHoveredColor );
255+
batchedMesh.setColorAt( batchId, yellow );
256+
257+
}
258+
259+
lastHoveredInstance = batchId;
260+
261+
}
262+
263+
function animate() {
264+
265+
stats.begin();
266+
267+
renderer.render( scene, camera );
268+
269+
stats.end();
270+
271+
}
272+
273+
</script>
274+
275+
</body>
276+
</html>

0 commit comments

Comments
 (0)