Skip to content

Commit

Permalink
InstancedMeshLOD (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
agargaro authored Sep 15, 2024
1 parent 2216054 commit 6e4baa8
Show file tree
Hide file tree
Showing 13 changed files with 596 additions and 79 deletions.
10 changes: 5 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export default [
...tseslint.configs.strict,
{
rules: {
'no-unused-vars': 'warn',
'no-undef': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-empty-object-type': 'warn',
'no-unused-vars': 'off',
'no-undef': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
},
},
];
39 changes: 39 additions & 0 deletions examples/LOD.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Main, PerspectiveCameraAuto } from '@three.ez/main';
import { AmbientLight, DirectionalLight, MeshLambertMaterial, Scene, SphereGeometry } from 'three';
import { OrbitControls } from 'three/examples/jsm/Addons.js';
import { InstancedMeshLOD } from '../src/index.js';
import { PRNG } from './objects/random.js';

const spawnRange = 10000;

const main = new Main();
const random = new PRNG(10000);

const camera = new PerspectiveCameraAuto(70, 0.1, 2000).translateZ(100).translateY(20);
const controls = new OrbitControls(camera, main.renderer.domElement);

const scene = new Scene();

const instancedMeshLOD = new InstancedMeshLOD(main.renderer, 1000000);

instancedMeshLOD.addLevel(new SphereGeometry(5, 30, 15), new MeshLambertMaterial({ color: 'green' }));
instancedMeshLOD.addLevel(new SphereGeometry(5, 20, 10), new MeshLambertMaterial({ color: 'yellow' }), 50);
instancedMeshLOD.addLevel(new SphereGeometry(5, 10, 5), new MeshLambertMaterial({ color: 'orange' }), 500);
instancedMeshLOD.addLevel(new SphereGeometry(5, 5, 3), new MeshLambertMaterial({ color: 'red' }), 1000);

instancedMeshLOD.levels[0].object.geometry.computeBoundingSphere(); // improve

instancedMeshLOD.updateInstances((object, index) => {
object.position.x = random.range(-spawnRange, spawnRange);
object.position.z = random.range(-spawnRange, spawnRange);
});

instancedMeshLOD.computeBVH();

scene.add(instancedMeshLOD, new AmbientLight(), new DirectionalLight().translateZ(3.5));

main.createView({ scene, camera, enabled: false });

scene.on('animate', (e) => {
controls.update(e.delta);
});
8 changes: 5 additions & 3 deletions examples/dynamicBVH.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const config = {
count: 100000,
animatedCount: 2000,
spawnRadius: 75000,
marginBVH: 100
marginBVH: 200
}

const main = new Main();
Expand Down Expand Up @@ -51,10 +51,12 @@ scene.on('animate', (e) => {
camera.getWorldDirection(spotLight.target.position).multiplyScalar(100).add(camera.position);
camera.getWorldDirection(dirLight.target.position).multiplyScalar(100).add(camera.position);

for (let i = 0; i < config.animatedCount; i++) {
const count = Math.min(config.animatedCount, instancedMesh.instancesCount);

for (let i = 0; i < count; i++) { // TODO use update Instances?
const mesh = instancedMesh.instances[i];
mesh.position.setFromSphericalCoords(mesh.r, mesh.phi + e.total * 0.02, mesh.theta + e.total * 0.02);
mesh.updateMatrix();
mesh.updateMatrixPosition();
}
});

Expand Down
103 changes: 81 additions & 22 deletions examples/trees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,98 @@ import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { Sky } from 'three/examples/jsm/objects/Sky.js';
import { InstancedMesh2 } from '../src/index.js';
import { InstancedMeshLOD } from '../src/index.js';
import { PRNG } from './objects/random.js';

const count = 1000000;
const terrainSize = 125000;
const terrainSize = 50000;
const random = new PRNG(10000);

const main = new Main({ rendererParameters: { antialias: true } }); // init renderer and other stuff
main.renderer.toneMapping = ACESFilmicToneMapping;
main.renderer.toneMappingExposure = 0.5;
main.renderer.shadowMap.enabled = true;
main.renderer.shadowMap.type = PCFSoftShadowMap;

const camera = new PerspectiveCameraAuto(70, 0.1, 3000).translateY(100);
const camera = new PerspectiveCameraAuto(70, 0.1, 2000);
const scene = new Scene();

const treeGLTF = (await Asset.load<GLTF>(GLTFLoader, 'tree.glb')).scene.children[0] as Mesh<BufferGeometry, MeshStandardMaterial>;
// const treeGLTF = (await Asset.load<GLTF>(GLTFLoader, 'tree.glb')).scene.children[0] as Mesh<BufferGeometry, MeshStandardMaterial>;

const trees = new InstancedMesh2(main.renderer, count, treeGLTF.geometry, treeGLTF.material);
trees.castShadow = true;
trees.cursor = 'pointer';
// const trees = new InstancedMesh2(main.renderer, count, treeGLTF.geometry, treeGLTF.material);
// trees.castShadow = true;
// trees.cursor = 'pointer';

trees.createInstances((obj, index) => {
obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);
obj.scale.setScalar(Math.random() * 0.1 + 0.1);
obj.rotateY(Math.random() * Math.PI * 2).rotateZ(Math.random() * 0.3 - 0.15);
});
// trees.createInstances((obj, index) => {
// obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);
// obj.scale.setScalar(Math.random() * 0.1 + 0.1);
// obj.rotateY(Math.random() * Math.PI * 2).rotateZ(Math.random() * 0.3 - 0.15);
// });

// trees.computeBVH();

// trees.on('click', (e) => {
// trees.instances[e.intersection.instanceId].visible = false;
// });



const treeHigh = (await Asset.load<GLTF>(GLTFLoader, 'tree.glb')).scene.children[0];
const treeMid = (await Asset.load<GLTF>(GLTFLoader, 'tree_mid.glb')).scene.children[0];
const treeLow = (await Asset.load<GLTF>(GLTFLoader, 'tree_far.glb')).scene.children[0];

const trunkHigh = treeHigh.children[0] as Mesh<BufferGeometry, MeshStandardMaterial>;
const trunkMid = treeMid.children[0] as Mesh<BufferGeometry, MeshStandardMaterial>;
const trunkLow = treeLow.children[0] as Mesh<BufferGeometry, MeshStandardMaterial>;

const leavesHigh = treeHigh.children[1] as Mesh<BufferGeometry, MeshStandardMaterial>;
const leavesMid = treeMid.children[1] as Mesh<BufferGeometry, MeshStandardMaterial>;
const leavesLow = treeLow.children[1] as Mesh<BufferGeometry, MeshStandardMaterial>;

leavesHigh.material.transparent = leavesMid.material.transparent = leavesLow.material.transparent = false;
leavesHigh.material.alphaTest = leavesMid.material.alphaTest = leavesLow.material.alphaTest = 0.2;
leavesHigh.material.depthWrite = leavesMid.material.depthWrite = leavesLow.material.depthWrite = true;

trees.computeBVH();
const trunkLOD = new InstancedMeshLOD(main.renderer, count);
trunkLOD.addLevel(trunkHigh.geometry, trunkHigh.material);
trunkLOD.addLevel(trunkMid.geometry, trunkMid.material, 100);
trunkLOD.addLevel(trunkLow.geometry, trunkLow.material, 200);
trunkLOD.levels[0].object.geometry.computeBoundingSphere(); // improve

trees.on('click', (e) => {
trees.instances[e.intersection.instanceId].visible = false;
const leavesLOD = new InstancedMeshLOD(main.renderer, count);
leavesLOD.addLevel(leavesHigh.geometry, leavesHigh.material);
// leavesLOD.addLevel(leavesMid.geometry, leavesMid.material, 500);
leavesLOD.addLevel(leavesLow.geometry, leavesLow.material, 500);
leavesLOD.levels[0].object.geometry.computeBoundingSphere(); // improve

trunkLOD.levels[0].object.castShadow = true;
trunkLOD.levels[1].object.castShadow = true;
trunkLOD.levels[2].object.castShadow = true;
leavesLOD.levels[0].object.castShadow = true;
leavesLOD.levels[1].object.castShadow = true;
// leavesLOD.levels[2].object.castShadow = true;



trunkLOD.updateInstances((obj, index) => {
obj.position.x = random.range(-terrainSize / 2, terrainSize / 2);
obj.position.z = random.range(-terrainSize / 2, terrainSize / 2);
obj.scale.multiplyScalar(random.range(5, 10));
obj.rotateY(random.range(0, Math.PI * 2)).rotateZ(random.range(-0.1, 0.1));
});

for (let i = 0; i < leavesLOD.maxCount; i++) {
leavesLOD.setMatrixAt(i, trunkLOD.getMatrixAt(i))
}

trunkLOD.computeBVH();
leavesLOD.computeBVH(); // it would be better use only one BVH




const ground = new Mesh(new PlaneGeometry(terrainSize, terrainSize, 10, 10), new MeshLambertMaterial({ color: 0x004622 }));
ground.interceptByRaycaster = false;
// ground.interceptByRaycaster = false;
ground.receiveShadow = true;
ground.rotateX(Math.PI / -2);

Expand Down Expand Up @@ -74,19 +132,20 @@ dirLight.on('animate', (e) => {
dirLight.target.position.copy(camera.position).sub(sunOffset);
});

scene.add(sky, trees, ground, new AmbientLight(), dirLight, dirLight.target);
scene.add(sky, trunkLOD, leavesLOD, ground, new AmbientLight(), dirLight, dirLight.target);

main.createView({ scene, camera, onAfterRender: () => treeCount.updateDisplay() });
// main.createView({ scene, camera, onAfterRender: () => treeCount.updateDisplay() });
main.createView({ scene, camera, enabled: false });

const controls = new MapControls(camera, main.renderer.domElement);
controls.maxPolarAngle = Math.PI / 2.1;
controls.minDistance = 10;
controls.maxDistance = 100;
controls.panSpeed = 10;
controls.target.set(-25, 100, 100);
controls.target.set(-5, 20, 20);
controls.update();

const gui = new GUI();
gui.add(trees.instances as any, 'length').name('instances total').disable();
const treeCount = gui.add(trees, 'count').name('instances rendered').disable();
gui.add(camera, 'far', 2000, 10000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix());
// gui.add(trees.instances as any, 'length').name('instances total').disable();
// const treeCount = gui.add(trees, 'count').name('instances rendered').disable();
gui.add(camera, 'far', 500, 10000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix());
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
</head>

<body>
<script type="module" src="./examples/openWorld.ts"></script>
<script type="module" src="./examples/LOD.ts"></script>
<span class="info" id="count"></span>
</body>

Expand Down
43 changes: 22 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
"build": "vite build",
"clean": "npx rimraf dist",
"lint": "npx eslint --fix",
"test":"echo todo add tests",
"test": "echo todo add tests",
"publish-patch": "npm version patch --git-tag-version false && npm run build && cd dist && npm publish --access public",
"publish-minor": "npm version minor --git-tag-version false && npm run build && cd dist && npm publish --access public",
"publish-major": "npm version major --git-tag-version false && npm run build && cd dist && npm publish --access public"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@three.ez/main": "^0.5.5",
"@eslint/js": "^9.10.0",
"@three.ez/main": "^0.5.7",
"@types/three": "^0.167.1",
"eslint": "^9.9.0",
"typescript": "^5.1.3",
Expand All @@ -49,6 +49,6 @@
"three": ">=0.159.0"
},
"dependencies": {
"bvh.js": "^0.0.8"
"bvh.js": "^0.0.9"
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './shaders/chunks/instanced_pars_vertex.glsl.js';
export * from './shaders/chunks/instanced_vertex.glsl.js';
export * from './utils/createTexture.js';
export * from './utils/createRadixSort.js';
export * from './objects/InstancedMeshLOD.js';

/** @internal */
declare module 'three' {
Expand Down
2 changes: 1 addition & 1 deletion src/objects/InstancedEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class InstancedEntity {
return this.matrix.premultiply(this.owner.matrixWorld);
}

constructor(owner: InstancedMesh2<any, any, any>, index: number) {
constructor(owner: InstancedMesh2, index: number) {
this.id = index;
this.owner = owner;
this.position = new Vector3();
Expand Down
Loading

0 comments on commit 6e4baa8

Please sign in to comment.