@nexus-physics/core
v0.2.1
Published
A headless, domain-agnostic, server-authoritative 3D physics engine (isomorphic Wasm)
Maintainers
Readme
@nexus-physics/core
High-performance, headless 3D physics engine for JavaScript/TypeScript. Powered by Rust + Rapier3D, compiled to WebAssembly with zero-copy memory access and automatic TypeScript definitions.
🚀 Quick Start
npm install @nexus-physics/coreimport init, { WasmPhysicsWorld } from '@nexus-physics/core';
// Initialize the Wasm module
await init();
// Create world with gravity (3 separate args: gx, gy, gz)
const world = new WasmPhysicsWorld(0, -9.81, 0);
// Add static floor (7 args: entity_id, body_type, shape_type, dims, px, py, pz)
world.add_body('floor', 'static', 'cuboid', [10, 0.5, 10], 0, -1, 0);
// Add dynamic ball
world.add_body('ball', 'dynamic', 'ball', [0.5], 0, 5, 0);
// Step the simulation
world.step(1 / 60);
// Get zero-copy snapshot (Float32Array)
const buffer = world.get_snapshot_view();
console.log(`Ball position: (${buffer[1]}, ${buffer[2]}, ${buffer[3]})`);📦 What You Get
- Deterministic Physics: Fixed timestep accumulator ensures reproducible behavior
- Zero-Copy Snapshots: Direct
Float32Arrayviews into Wasm memory—no garbage collection pressure - Type-Safe API: Full TypeScript definitions auto-generated from Rust
- Isomorphic: Works in browser and Node.js from the same package
- Domain-Agnostic: Generic geometry and forces, not tied to games or robotics
- Server-Authoritative Ready: Perfect for multiplayer simulations with client prediction
📚 Core Concepts
Body Types
Pass as lowercase strings to add_body():
"dynamic": Affected by forces and gravity. For moving objects."static": Immovable terrain, walls, platforms."kinematic": Moved programmatically. For elevators, moving platforms.
Shape Types
Pass as lowercase strings with dimension arrays:
"cuboid": Box shape. Dims:[hx, hy, hz](half-extents)"ball": Sphere shape. Dims:[radius]"cylinder": Cylinder shape. Dims:[radius, half_height]
Snapshot Buffer Layout
Each body in a snapshot occupies exactly 8 floats:
[index, x, y, z, qx, qy, qz, qw, index, x, y, z, ...]Access directly without parsing:
const buffer = world.get_snapshot_view();
for (let i = 0; i < buffer.length; i += 8) {
const id = buffer[i + 0]; // Snapshot entry index
const x = buffer[i + 1]; // Position X
const y = buffer[i + 2]; // Position Y
const z = buffer[i + 3]; // Position Z
const qx = buffer[i + 4]; // Quaternion X
const qy = buffer[i + 5]; // Quaternion Y
const qz = buffer[i + 6]; // Quaternion Z
const qw = buffer[i + 7]; // Quaternion W
}🎮 Common Patterns
Multiplayer Game with Client Prediction
const world = new WasmPhysicsWorld(0, -9.81, 0);
// Game loop
function gameLoop(deltaTime) {
// Apply local player input (linear + angular velocity combined)
world.set_velocity('player', vx, vy, vz, ax, ay, az);
// Predict locally
world.step(deltaTime);
// Render from prediction
const buffer = world.get_snapshot_view();
updatePlayerMesh(buffer);
// Periodically reconciliate with server
if (shouldReconciliate()) {
const serverSnapshot = await fetchServerSnapshot();
const pos = serverSnapshot.position;
const rot = serverSnapshot.rotation;
world.apply_impulse('other_player', pos[0], pos[1], pos[2]);
}
}Three.js Integration
import * as THREE from 'three';
import init, { WasmPhysicsWorld } from '@nexus-physics/core';
const world = new WasmPhysicsWorld(0, -9.81, 0);
const meshes = new Map<string, THREE.Mesh>();
function render() {
world.step(1 / 60);
const buffer = world.get_snapshot_view();
// Update all meshes directly from physics buffer
for (let i = 0; i < buffer.length; i += 8) {
const id = buffer[i];
const mesh = meshes.get(String(id));
if (mesh) {
mesh.position.set(buffer[i + 1], buffer[i + 2], buffer[i + 3]);
mesh.quaternion.set(
buffer[i + 4],
buffer[i + 5],
buffer[i + 6],
buffer[i + 7],
);
}
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}Robotics Simulation
import { WasmPhysicsWorld } from '@nexus-physics/core';
const world = new WasmPhysicsWorld(0, -9.81, 0);
// Add robot base and wheels
world.add_body('robot_base', 'dynamic', 'cuboid', [0.3, 0.1, 0.3], 0, 0.5, 0);
// Control via velocity commands (linear + angular)
function applyMotorCommand(lx, ly, lz, ax, ay, az) {
world.set_velocity('robot_base', lx, ly, lz, ax, ay, az);
}
// Simulate sensor reading (e.g., LiDAR)
function readSensors() {
world.step(1 / 60);
const snapshot = world.get_snapshot_view();
// Process snapshot as needed
return snapshot;
}Deterministic Replay
const world = new WasmPhysicsWorld(0, -9.81, 0);
const timeline: Array<{
velocities: Float32Array;
}> = [];
// Record phase
function recordFrame(
lx: number,
ly: number,
lz: number,
ax: number,
ay: number,
az: number,
) {
world.set_velocity('player', lx, ly, lz, ax, ay, az);
world.step(1 / 60);
timeline.push({
velocities: new Float32Array([lx, ly, lz, ax, ay, az]),
});
}
// Replay phase
function replay() {
const newWorld = new WasmPhysicsWorld(0, -9.81, 0);
newWorld.add_body('player', 'dynamic', 'ball', [0.5], 0, 5, 0);
for (const frame of timeline) {
const v = frame.velocities;
newWorld.set_velocity('player', v[0], v[1], v[2], v[3], v[4], v[5]);
newWorld.step(1 / 60);
}
}📖 API Reference
World
class WasmPhysicsWorld {
// Constructor takes 3 separate gravity components (not an array)
constructor(gx: number, gy: number, gz: number);
step(deltaTime: number): void;
body_count(): number;
update_query_pipeline(): void;
// Add body with 7 arguments (not a config object)
add_body(
entity_id: string,
body_type: 'dynamic' | 'static' | 'kinematic', // lowercase!
shape_type: 'cuboid' | 'ball' | 'cylinder', // lowercase!
dims: number[], // [hx,hy,hz] or [radius] or [radius,half_height]
px: number,
py: number,
pz: number,
): void;
remove_body(entity_id: string): void;
// Only snapshot method - returns Float32Array view
get_snapshot_view(): Float32Array;
// Set both linear AND angular velocity in one call
set_velocity(
entity_id: string,
lx: number,
ly: number,
lz: number, // linear velocity
ax: number,
ay: number,
az: number, // angular velocity
): void;
apply_impulse(entity_id: string, ix: number, iy: number, iz: number): void;
get_position(entity_id: string): Float32Array; // length 3
get_rotation(entity_id: string): Float32Array; // length 4 (quaternion)
cast_ray(
ox: number,
oy: number,
oz: number, // origin
dx: number,
dy: number,
dz: number, // direction
max_toi: number,
): number; // Distance to hit, or NaN if no hit
cast_ray_batch(
origins: Float32Array, // [x, y, z, x, y, z, ...]
directions: Float32Array, // [x, y, z, x, y, z, ...]
max_toi: number,
): Float32Array; // [distance, distance, ...] - max_toi on miss
}🎯 When to Use
✅ Ideal for:
- Multiplayer games with server-authoritative physics
- 3D web applications (Three.js, Babylon.js)
- Robotics and physics simulations
- Deterministic replay systems
- Real-time collision detection
- Networked motion synthesis
❌ Not ideal for:
- Softbody physics (cloth, hair)
- Deformable terrain
- Particle systems (use GPU instead)
- Passive rigid body stacking (CPU-bound)
⚡ Performance Tips
- Reuse Float32Array views: Don't create new buffers every frame
- Batch updates: Apply multiple forces before stepping
- Limit body count: 500-1000 bodies per world on modern hardware
- Use appropriate shapes: Spheres are faster than boxes
- Profile first: Use Chrome DevTools to measure bottlenecks
🔧 Advanced: Node.js Usage
The package exports separate distributions for browser and Node.js. Import detection is automatic:
// Node.js automatically gets the Node.js Wasm build
import init, { WasmPhysicsWorld } from '@nexus-physics/core';
// Or explicitly:
import initNode from '@nexus-physics/core/dist/node/nexus_physics_wasm.js';
// Browser sees the browser build via conditional exports🐛 Troubleshooting
Q: "Module not found" errors in bundler
A: Ensure your bundler is configured for Wasm. Vite, Webpack 5+, and esbuild handle this automatically.
Q: Physics feels jittery between frames
A: You're likely stepping with variable delta times. The engine uses a fixed 60Hz timestep internally—pass the actual elapsed time and let the accumulator handle it.
Q: Why is my snapshot empty?
A: Only Dynamic and Kinematic bodies appear in snapshots. Static bodies are implicit in the world geometry.
📚 Further Reading
📄 License
MIT License. See project repository for full license text.
🤝 Contributing
Found a bug? Have a feature request? Open an issue on the GitHub repository.
Built with ❤️ in Rust & WebAssembly
