Skip to content

Commit 33d2cc2

Browse files
committed
Add webgl_batch_lod_bvh example page
1 parent b916c16 commit 33d2cc2

File tree

4 files changed

+272
-0
lines changed

4 files changed

+272
-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: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js raycaster - 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="https://github.com/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 4 LODs generated with meshoptimizer. <br>
19+
Frustum culling and raycasting are accelerated by using BVHs (TLAS & BLAS). <br>
20+
See <a href="https://github.com/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+
const instancesCount = 500000;
60+
let stats;
61+
let camera, scene, renderer;
62+
let batchedMesh;
63+
let lastHoveredInstance = null;
64+
const lastHoveredColor = new THREE.Color();
65+
const yellow = new THREE.Color( 'yellow' );
66+
67+
const raycaster = new THREE.Raycaster();
68+
const mouse = new THREE.Vector2( 1, 1 );
69+
const position = new THREE.Vector3();
70+
const quaternion = new THREE.Quaternion();
71+
const scale = new THREE.Vector3( 1, 1, 1 );
72+
const matrix = new THREE.Matrix4();
73+
const color = new THREE.Color();
74+
75+
init();
76+
77+
async function init() {
78+
79+
// environment
80+
camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 0.1, 600 );
81+
camera.position.set( 0, 10, 50 );
82+
83+
scene = new THREE.Scene();
84+
scene.fog = new THREE.Fog( 0x000000, 500, 600 );
85+
86+
const ambient = new THREE.AmbientLight();
87+
scene.add( camera, ambient );
88+
89+
const dirLight = new THREE.DirectionalLight( 'white', 2 );
90+
camera.add( dirLight, dirLight.target );
91+
92+
// renderer
93+
renderer = new THREE.WebGLRenderer( { antialias: true } );
94+
renderer.setPixelRatio( window.devicePixelRatio );
95+
renderer.setSize( window.innerWidth, window.innerHeight );
96+
document.body.appendChild( renderer.domElement );
97+
98+
raycaster.firstHitOnly = true;
99+
100+
stats = new Stats();
101+
document.body.appendChild( stats.dom );
102+
103+
const controls = new MapControls( camera, renderer.domElement );
104+
controls.maxPolarAngle = Math.PI / 2;
105+
106+
const geometries = [
107+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 1 ),
108+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 2 ),
109+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 3 ),
110+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 4 ),
111+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 1, 5 ),
112+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 1 ),
113+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 2, 3 ),
114+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 3, 1 ),
115+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 4, 1 ),
116+
new THREE.TorusKnotGeometry( 1, 0.4, 256, 32, 5, 3 )
117+
];
118+
119+
// generate 4 LODs (Levels of Detail) for each geometry
120+
const geometriesLODArray = await simplifyGeometriesByErrorLOD( geometries, 4, performanceRangeLOD );
121+
122+
// create BatchedMesh
123+
const { vertexCount, indexCount, LODIndexCount } = getBatchedMeshLODCount( geometriesLODArray );
124+
batchedMesh = new THREE.BatchedMesh( instancesCount, vertexCount, indexCount, new THREE.MeshStandardMaterial( { metalness: 0.2, roughness: 0.2 } ) );
125+
126+
// enable radix sort for better performance
127+
batchedMesh.customSort = createRadixSort( batchedMesh );
128+
129+
// add geometries and their LODs to the batched mesh ( all LODs share the same position array )
130+
for ( let i = 0; i < geometriesLODArray.length; i ++ ) {
131+
132+
const geometryLOD = geometriesLODArray[ i ];
133+
const geometryId = batchedMesh.addGeometry( geometryLOD[ 0 ], - 1, LODIndexCount[ i ] );
134+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 1 ], 15 );
135+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 2 ], 75 );
136+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 3 ], 125 );
137+
batchedMesh.addGeometryLOD( geometryId, geometryLOD[ 4 ], 200 );
138+
139+
}
140+
141+
// place instances in a 2D grid with randomized rotation and color
142+
const sqrtCount = Math.ceil( Math.sqrt( instancesCount ) );
143+
const size = 5.5;
144+
const start = ( sqrtCount / - 2 * size ) + ( size / 2 );
145+
146+
for ( let i = 0; i < instancesCount; i ++ ) {
147+
148+
const r = Math.floor( i / sqrtCount );
149+
const c = i % sqrtCount;
150+
const id = batchedMesh.addInstance( Math.floor( Math.random() * geometriesLODArray.length ) );
151+
position.set( c * size + start, 0, r * size + start );
152+
quaternion.random();
153+
batchedMesh.setMatrixAt( id, matrix.compose( position, quaternion, scale ) );
154+
batchedMesh.setColorAt( id, color.setHSL( Math.random(), 0.6, 0.5 ) );
155+
156+
}
157+
158+
// compute blas (bottom-level acceleration structure) bvh ( three-mesh-bvh )
159+
batchedMesh.computeBoundsTree();
160+
161+
// compute tlas (top-level acceleration structure) bvh ( @three.ez/batched-mesh-extensions )
162+
batchedMesh.computeBVH( THREE.WebGLCoordinateSystem );
163+
164+
scene.add( batchedMesh );
165+
166+
// set up gui
167+
const config = {
168+
useBVH: true,
169+
useLOD: true
170+
};
171+
172+
const bvh = batchedMesh.bvh;
173+
const lods = batchedMesh._geometryInfo.map( x => x.LOD );
174+
175+
const gui = new GUI();
176+
gui.add( batchedMesh, 'instanceCount' ).disable();
177+
178+
const frustumCullingFolder = gui.addFolder( 'Frustum culling & raycasting' );
179+
frustumCullingFolder.add( config, 'useBVH' ).onChange( v => {
180+
181+
batchedMesh.bvh = v ? bvh : null;
182+
183+
} );
184+
185+
const GeometriesFolder = gui.addFolder( 'Geometries' );
186+
GeometriesFolder.add( config, 'useLOD' ).onChange( v => {
187+
188+
const geometryInfo = batchedMesh._geometryInfo;
189+
for ( let i = 0; i < geometryInfo.length; i ++ ) {
190+
191+
geometryInfo[ i ].LOD = v ? lods[ i ] : null;
192+
193+
}
194+
195+
} );
196+
197+
document.addEventListener( 'mousemove', onMouseMove );
198+
window.addEventListener( 'resize', onWindowResize );
199+
onWindowResize();
200+
201+
renderer.setAnimationLoop( animate );
202+
203+
}
204+
205+
206+
function onMouseMove( event ) {
207+
208+
event.preventDefault();
209+
210+
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
211+
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
212+
213+
raycast();
214+
215+
}
216+
217+
218+
function onWindowResize() {
219+
220+
camera.aspect = window.innerWidth / window.innerHeight;
221+
camera.updateProjectionMatrix();
222+
223+
renderer.setSize( window.innerWidth, window.innerHeight );
224+
225+
}
226+
227+
function raycast() {
228+
229+
raycaster.setFromCamera( mouse, camera );
230+
const intersection = raycaster.intersectObject( batchedMesh );
231+
232+
if ( intersection.length > 0 ) {
233+
234+
const batchId = intersection[ 0 ].batchId;
235+
236+
if ( lastHoveredInstance === batchId ) return;
237+
238+
if ( lastHoveredInstance ) {
239+
240+
batchedMesh.setColorAt( lastHoveredInstance, lastHoveredColor );
241+
242+
}
243+
244+
lastHoveredInstance = batchId;
245+
batchedMesh.getColorAt( lastHoveredInstance, lastHoveredColor );
246+
batchedMesh.setColorAt( lastHoveredInstance, yellow );
247+
248+
} else if ( lastHoveredInstance ) {
249+
250+
batchedMesh.setColorAt( lastHoveredInstance, lastHoveredColor );
251+
lastHoveredInstance = null;
252+
253+
}
254+
255+
}
256+
257+
function animate() {
258+
259+
stats.begin();
260+
261+
renderer.render( scene, camera );
262+
263+
stats.end();
264+
265+
}
266+
267+
</script>
268+
269+
</body>
270+
</html>

0 commit comments

Comments
 (0)