Loading...
Loading...
WebGPU rigid-body/soft-body physics engine based on the AVBD (Augmented Vertex Block Descent) solver
npx skill4agent add aradotso/trending-skills webphysics-avbd-engineSkill by ara.so — Daily 2026 Skills collection.
webphysicsBrowser support: Chrome only (requires WebGPU). This is an experimental proof-of-concept, not a production library.
git clone https://github.com/jure/webphysics.git
cd webphysics
npm install
npm run dev # development server
npm run build # production buildhttp://localhost:5173src/
├── physics/
│ ├── PhysicsEngine.ts # Main orchestration: substep loop, init, step
│ └── gpu/
│ ├── avbdState.ts # Primal/dual solve, coloring, velocity finalization
│ ├── broadPhase.ts # LBVH broad-phase candidate generation
│ ├── contactGeneration.ts # Narrow-phase manifolds, per-body constraint lists
│ ├── contactRecord.ts # Warm-start state persistence
│ └── avbdState.ts # Inertial targets, primal init, iteration
├── lvbh/
│ └── GPULBVHBuilder.ts # GPU LBVH construction
└── ...import { PhysicsEngine } from './src/physics/PhysicsEngine';
// Requires an existing GPUDevice
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const engine = new PhysicsEngine(device);
await engine.init();// Add a static ground plane
engine.addBody({
type: 'box',
position: [0, -1, 0],
rotation: [0, 0, 0, 1], // quaternion [x, y, z, w]
halfExtents: [10, 0.5, 10],
mass: 0, // 0 = static/infinite mass
restitution: 0.3,
friction: 0.5,
});
// Add a dynamic rigid box
engine.addBody({
type: 'box',
position: [0, 5, 0],
rotation: [0, 0, 0, 1],
halfExtents: [0.5, 0.5, 0.5],
mass: 1.0,
restitution: 0.2,
friction: 0.6,
});const TIMESTEP = 1 / 60;
const SUBSTEPS = 10;
function gameLoop(dt: number) {
engine.step(dt, SUBSTEPS);
// Read back positions for rendering
const bodyStates = engine.getBodyStates();
renderBodies(bodyStates);
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);// After engine.step(), retrieve updated transforms
const states = engine.getBodyStates();
for (const state of states) {
const { position, rotation, bodyIndex } = state;
// position: [x, y, z]
// rotation: quaternion [x, y, z, w]
updateMeshTransform(bodyIndex, position, rotation);
}// Distance joint between two bodies
engine.addJoint({
type: 'distance',
bodyA: 0,
bodyB: 1,
anchorA: [0, 0.5, 0], // local-space anchor on body A
anchorB: [0, -0.5, 0], // local-space anchor on body B
restLength: 1.0,
stiffness: 1e4,
});engine.addSpring({
bodyA: 2,
bodyB: 3,
anchorA: [0, 0, 0],
anchorB: [0, 0, 0],
restLength: 0.8,
stiffness: 500,
damping: 10,
});1. collision detection (x^t)
↓
2. broad phase (LBVH) → src/lvbh/GPULBVHBuilder.ts
↓
3. narrow phase + warm start → src/physics/gpu/contactGeneration.ts
↓
4. per-body constraint lists → src/physics/gpu/avbdState.ts
↓
5. graph coloring → src/physics/gpu/avbdState.ts
↓
6. inertial target y, primal init, warm-start α/γ
↓
7. [loop] colored primal body solve (approx Hessian)
↓
8. [loop] dual + stiffness update
↓
9. finalize velocities| Stage | File |
|---|---|
| Orchestration | |
| Broad phase | |
| Narrow phase | |
| Contact records | |
| AVBD solve | |
| LBVH builder | |
// Passed during engine construction or step
engine.step(dt, substeps, {
gravity: [0, -9.81, 0],
iterations: 10, // AVBD inner iterations per substep
restitutionThreshold: 1.0,
});substepsiterationsmass: 0stiffnessrestitution: 0frictionconst groundIndex = engine.addBody({
type: 'box',
position: [0, 0, 0],
halfExtents: [5, 0.25, 5],
mass: 0,
friction: 0.7,
restitution: 0.1,
});
for (let i = 0; i < 8; i++) {
engine.addBody({
type: 'box',
position: [0, 0.5 + i * 1.05, 0],
halfExtents: [0.5, 0.5, 0.5],
mass: 1.0,
friction: 0.5,
restitution: 0.1,
});
}let prevIndex = engine.addBody({
type: 'box', position: [0, 5, 0],
halfExtents: [0.1, 0.1, 0.1], mass: 0,
friction: 0, restitution: 0,
});
for (let i = 1; i <= 5; i++) {
const curr = engine.addBody({
type: 'box', position: [0, 5 - i, 0],
halfExtents: [0.15, 0.15, 0.15], mass: 1.0,
friction: 0.1, restitution: 0,
});
engine.addJoint({
type: 'distance',
bodyA: prevIndex, bodyB: curr,
anchorA: [0, -0.15, 0], anchorB: [0, 0.15, 0],
restLength: 0.7,
stiffness: 1e5,
});
prevIndex = curr;
}import * as THREE from 'three';
const meshes: THREE.Mesh[] = [];
function syncPhysicsToRender() {
const states = engine.getBodyStates();
states.forEach((state, i) => {
if (!meshes[i]) return;
meshes[i].position.set(...state.position);
meshes[i].quaternion.set(
state.rotation[0], state.rotation[1],
state.rotation[2], state.rotation[3]
);
});
}
function animate() {
engine.step(1 / 60, 10);
syncPhysicsToRender();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}Error: navigator.gpu is undefinedchrome://flags/#enable-unsafe-webgpusubstepsstiffnessmass: 0halfExtentsiterationssubstepschrome://gpu# Ensure Node.js >= 18
node --version
# Clear cache
rm -rf node_modules dist
npm install
npm run buildavbdState.ts