Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ name: "CodeQL config"

paths-ignore:
- "node_modules"
- "examples"
- "examples"
- "tests"
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Tests

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run unit tests
run: npm test

- name: Install Playwright browsers
run: npx playwright install --with-deps webkit

- name: Run E2E tests
run: npm run test:e2e

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
node_modules
dist

# Tests
.last-run.json
test-results/
playwright-report/
coverage/
118 changes: 118 additions & 0 deletions examples/shadowLOD-webgpu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { AmbientLight, BoxGeometry, DirectionalLight, Mesh, PerspectiveCamera, PlaneGeometry, Scene, SphereGeometry, TorusGeometry, TorusKnotGeometry } from 'three';
import { WebGPURenderer, MeshBasicNodeMaterial, MeshPhongNodeMaterial, MeshNormalNodeMaterial } from 'three/webgpu';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { InstancedMesh2 } from '../src/index.webgpu.js';
import Stats from 'stats-gl';

// NOTE: WebGPU buffer approach has a UBO limit of ~1000 instances
// For larger counts, texture-based instancing is needed (not yet implemented)
const count = 10;
const terrainSize = 10;

async function init(): Promise<void> {
// Create WebGPU renderer
const renderer = new WebGPURenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

// Initialize WebGPU
await renderer.init();

// Initialize stats-gl with GPU tracking for WebGPU
const stats = new Stats({
trackGPU: true,
trackHz: true,
trackCPT: false,
logsPerSecond: 4,
graphsPerSecond: 30,
samplesLog: 40,
samplesGraph: 10,
precision: 2,
horizontal: true,
minimal: false,
mode: 0
});
document.body.appendChild(stats.dom);
stats.init(renderer);

const camera = new PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 10000);
camera.position.set(100, 100, 100);

const scene = new Scene();

// Use NodeMaterials for WebGPU compatibility
const materials = [
new MeshBasicNodeMaterial({ color: 0xff0000 }),
new MeshNormalNodeMaterial(),
new MeshNormalNodeMaterial(),
new MeshNormalNodeMaterial(),
new MeshNormalNodeMaterial(),
new MeshNormalNodeMaterial()
];

const instancedMesh = new InstancedMesh2(new BoxGeometry(3,3,3), materials, { capacity: count, renderer });
instancedMesh.castShadow = true;

// Add LOD levels with NodeMaterials
instancedMesh.addLOD(new SphereGeometry(1, 8, 4), new MeshPhongNodeMaterial({ color: 0x00e6e6 }), 100);
// instancedMesh.addShadowLOD(new BoxGeometry(2, 2, 2));
// instancedMesh.addShadowLOD(new TorusKnotGeometry(0.8, 0.2, 32, 8), 80);
// instancedMesh.addShadowLOD(new TorusGeometry(0.8, 0.2, 32, 8), 110);

instancedMesh.addInstances(count, (obj, index) => {
// obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);

// Deterministic placement: simple XY grid, spread evenly
const gridSize = Math.ceil(Math.sqrt(count));
const spacing = terrainSize / gridSize;
const x = (index % gridSize) * spacing - terrainSize / 2 + spacing / 2;
const z = Math.floor(index / gridSize) * spacing - terrainSize / 2 + spacing / 2;
obj.position.set(x, 0, z);

});

instancedMesh.computeBVH();

const ground = new Mesh(new PlaneGeometry(terrainSize, terrainSize, 10, 10), new MeshPhongNodeMaterial({ color: 0x888888 }));
ground.receiveShadow = true;
ground.translateY(-1);
ground.rotateX(Math.PI / -2);

const dirLight = new DirectionalLight();
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.left = -100;
dirLight.shadow.camera.right = 100;
dirLight.shadow.camera.top = 100;
dirLight.shadow.camera.bottom = -100;
dirLight.shadow.camera.updateProjectionMatrix();
dirLight.position.set(0, 30, 50);

scene.add(instancedMesh, ground, new AmbientLight(), dirLight);

const controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = Math.PI / 2.1;
controls.minDistance = 10;
controls.maxDistance = 200;
controls.target.set(0, 0, 0);
controls.update();
// controls.autoRotate = true;

// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});

// Animation loop
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
stats.update();
});
}

init().catch(console.error);
13 changes: 11 additions & 2 deletions examples/shadowLOD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ const materials = [
const instancedMesh = new InstancedMesh2(new BoxGeometry(), materials, { capacity: count });
instancedMesh.castShadow = true;

instancedMesh.addLOD(new SphereGeometry(1, 8, 4), new MeshPhongMaterial({ color: 0x00e6e6 }), 100);
// instancedMesh.addLOD(new SphereGeometry(1, 8, 4), new MeshPhongMaterial({ color: 0x00e6e6 }), 100);
instancedMesh.addLOD(new TorusKnotGeometry(0.8, 0.2, 32, 8), new MeshPhongMaterial({ color: 0x00e6e6 }), 100);
instancedMesh.addShadowLOD(new BoxGeometry(2, 2, 2));
instancedMesh.addShadowLOD(new TorusKnotGeometry(0.8, 0.2, 32, 8), 80);
instancedMesh.addShadowLOD(new TorusGeometry(0.8, 0.2, 32, 8), 110);

instancedMesh.addInstances(count, (obj, index) => {
obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);
// obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);

// Deterministic placement: simple XY grid, spread evenly
const gridSize = Math.ceil(Math.sqrt(count));
const spacing = terrainSize / gridSize;
const x = (index % gridSize) * spacing - terrainSize / 2 + spacing / 2;
const z = Math.floor(index / gridSize) * spacing - terrainSize / 2 + spacing / 2;
obj.position.set(x, 0, z);

});

instancedMesh.computeBVH();
Expand Down
40 changes: 40 additions & 0 deletions index-webgpu.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InstancedMesh2 WebGPU example</title>
<style>
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
overscroll-behavior: none;
}

canvas {
width: 100%;
height: 100%;
display: block;
touch-action: none;
user-select: none;
}

.info {
position: fixed;
top: 40px;
right: 40px;
}
</style>
</head>

<body>
<script type="module" src="./examples/shadowLOD-webgpu.ts"></script>
<span class="info" id="count"></span>
</body>

</html>


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/overrideMaterial.ts"></script>
<script type="module" src="./examples/shadowLOD.ts"></script>
<span class="info" id="count"></span>
</body>

Expand Down
Loading