Physics
Physics in Viber3D
Viber3D uses an ECS approach for organizing game data. You can integrate a physics engine—like Jolt—by defining traits for physics objects and systems that step the simulation each frame.
This guide covers:
- Overview & Key Concepts
- Initializing a Physics World
- Creating Physics Bodies
- Stepping & Syncing Transforms
- Collision Handling
- Motion Types & Continuous Collision
- Common Patterns & Tips
- Summary & Next Steps
1. Overview & Key Concepts
- PhysicsWorld Trait
A specialized trait storing the engine’s “world” or “physics interface,” so it’s accessible anywhere in ECS. - RigidBody / PhysicsBody Traits
Each entity that should have physics (e.g. a crate, sphere, or player) can hold a trait referencing its underlying physics body in Jolt. - Systems
- A “build bodies” system to create bodies for newly spawned ECS entities.
- A “step physics” system to advance the physics simulation each frame.
- A “sync transforms” system to copy the updated positions/rotations back to Three.js objects (or to ECS
Position
/Rotation
traits).
Why Jolt?
- Speed: Written in C++ and compiled to WebAssembly.
- Feature-rich: Rigid bodies, collisions, advanced shape definitions, etc.
- Optional: You can swap in a different library (e.g., Rapier) by following similar patterns.
2. Initializing a Physics World
Goal: Load the Jolt library once, create a Jolt “world,” and store it in a custom ECS trait.
// traits/jolt-world.ts
import { trait } from 'koota'
import type Jolt from 'jolt-physics'
export const JoltWorld = trait(() => new JoltWorldImpl())
export class JoltWorldImpl {
static JOLT_NATIVE: typeof Jolt
joltInterface: Jolt.JoltInterface = null!
bodyInterface: Jolt.BodyInterface = null!
physicsSystem: Jolt.PhysicsSystem = null!
initialized = false
constructor() {
if (!JoltWorldImpl.JOLT_NATIVE) {
throw new Error("Jolt not loaded yet.")
}
// Create the JoltInterface with default settings
const J = JoltWorldImpl.JOLT_NATIVE
const settings = new J.JoltSettings()
this._setupCollisionFiltering(settings)
const jolt = new J.JoltInterface(settings)
J.destroy(settings)
this.physicsSystem = jolt.GetPhysicsSystem()
this.bodyInterface = this.physicsSystem.GetBodyInterface()
this.joltInterface = jolt
this.initialized = true
}
stepPhysics(dt: number) {
// E.g., do multiple substeps if dt is large
const steps = dt > 1/55 ? 2 : 1
this.joltInterface.Step(dt, steps)
}
_setupCollisionFiltering(settings: Jolt.JoltSettings) {
// your collision layers, etc.
}
}
Initialize the library at startup:
// actions/use-jolt-actions.ts
import { createActions } from 'koota/react'
import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world'
export const useJoltActions = createActions((world) => ({
async initWorld() {
if (world.has(JoltWorld)) {
// already initialized
return
}
// Load the Jolt WASM
const joltInit = await import('jolt-physics')
// Store the reference globally
JoltWorldImpl.JOLT_NATIVE = await joltInit.default()
// Add the JoltWorld trait to ECS
world.add(JoltWorld())
console.log("Jolt world created.")
}
}))
You can call initWorld()
from a React component, or anywhere else in your startup code.
3. Creating Physics Bodies
a) “NeedsJoltBody” Trait
Define a trait that marks an entity as needing a physics body. This can include settings: whether the body is dynamic, static, or kinematic, etc.
// traits/needs-jolt-body.ts
import { trait } from 'koota'
export const NeedsJoltBody = trait({
layer: "moving" as "moving" | "non_moving",
motionType: "dynamic" as "dynamic" | "static" | "kinematic",
buildConvexShape: false,
continuousCollisionMode: false,
})
You might also define a trait JoltBody
referencing the final Jolt body object:
// traits/jolt-body.ts
import { trait } from 'koota'
import type Jolt from 'jolt-physics'
export const JoltBody = trait<null | Jolt.Body>(null)
b) System to Build Bodies
When new entities have NeedsJoltBody
+ a mesh or geometry trait (like MeshRef
), we create an actual Jolt Body
.
// systems/build-jolt-bodies.ts
import { World } from 'koota'
import { NeedsJoltBody } from '../traits/needs-jolt-body'
import { JoltBody } from '../traits/jolt-body'
import { JoltWorld, JoltWorldImpl } from '../traits/jolt-world'
import { MeshRef } from '../traits/mesh-ref'
import { inferInitTransformsFromMesh, createShapeFromGeometry } from '../misc/jolt-helper'
export function BuildJoltBodies({ world }: { world: World }) {
if (!world.has(JoltWorld)) return
const joltWorld = world.get(JoltWorld)
const J = JoltWorldImpl.JOLT_NATIVE
world.query(NeedsJoltBody, MeshRef).updateEach(([needs, meshRef], entity) => {
const { pos, rot } = inferInitTransformsFromMesh(meshRef.ref)
const shape = needs.buildConvexShape
? createShapeFromGeometry(meshRef.ref.geometry)
: /* other shape logic, e.g. BoxShape */ new J.BoxShape(1,1,1)
// create BodyCreationSettings
const layer = needs.layer === 'moving' ? 1 : 0
const motionType = {
dynamic: J.EMotionType_Dynamic,
static: J.EMotionType_Static,
kinematic: J.EMotionType_Kinematic
}[needs.motionType]
const creation = new J.BodyCreationSettings(shape, pos, rot, motionType, layer)
const body = joltWorld.bodyInterface.CreateBody(creation)
J.destroy(creation)
// add the body to jolt
joltWorld.bodyInterface.AddBody(body.GetID(), J.EActivation_Activate)
// handle continuous collision
if (needs.continuousCollisionMode) {
joltWorld.bodyInterface.SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast)
}
// remove the NeedsJoltBody trait (we built the body)
entity.remove(NeedsJoltBody)
// add the JoltBody trait referencing the new body
entity.add(JoltBody(body))
})
}
This approach ensures that each newly spawned entity gets a Jolt body exactly once.
4. Stepping & Syncing Transforms
a) Update the Physics World
A system that calls stepPhysics
each frame:
// systems/update-jolt.ts
import { World } from 'koota'
import { JoltWorld } from '../traits/jolt-world'
export function UpdateJolt({ world, delta }: { world: World; delta: number }) {
if (!world.has(JoltWorld)) return
const joltWorld = world.get(JoltWorld)
joltWorld.stepPhysics(delta)
}
b) Sync to ECS or Three.js
You can store transforms in ECS Position
/ Rotation
traits, or directly sync to a MeshRef
. For example:
// systems/sync-three-to-jolt.ts
import { World } from 'koota'
import { JoltBody } from '../traits/jolt-body'
import { MeshRef } from '../traits/mesh-ref'
import { JoltWorld } from '../traits/jolt-world'
export function SyncThreeToJolt({ world }: { world: World }) {
if (!world.has(JoltWorld)) return
world.query(JoltBody, MeshRef).useStores(([rb, meshRef], entities) => {
for (const e of entities) {
const body = rb[e.id()]
const mesh = meshRef[e.id()]
if (!body || !mesh) continue
// read Jolt transform
const pos = body.GetPosition()
const rot = body.GetRotation()
// apply to Three.js mesh
mesh.position.set(pos.GetX(), pos.GetY(), pos.GetZ())
mesh.quaternion.set(rot.GetX(), rot.GetY(), rot.GetZ(), rot.GetW())
}
})
}
Note:
useStores
bypasses some overhead by accessing data in a more raw manner—useful for performance-critical loops.
5. Collision Handling
a) Basic Approach
Jolt can track collisions or contact points. Depending on your version, you might:
- Read a contact list or events each step (
GetCollisionsThisFrame()
, or similar). - Subscribe to collision callbacks via Jolt’s event system.
Once you have collisions, you can do ECS logic:
function handleCollisions(world: World) {
const joltWorld = world.get(JoltWorld)
// some hypothetical API
const collisions = joltWorld.physicsSystem.GetCollisionEvents()
for (const c of collisions) {
const bodyA = c.bodyA
const bodyB = c.bodyB
// find which entities correspond to bodyA, bodyB, then apply logic
}
}
Because Koota queries are trait-based, you might store a map of BodyID -> Entity
to help quickly find which entity is colliding. You can store that map in your JoltWorldImpl
or a separate trait.
6. Motion Types & Continuous Collision
Jolt supports different motion types:
- Static: For immovable objects (floors, walls).
- Dynamic: Fully simulated by physics (gravity, collisions).
- Kinematic: Driven by user code, but can push dynamic bodies out of the way.
Continuous Collision (CCD) helps fast-moving objects (e.g., bullets) avoid tunneling. In the code, we check continuousCollisionMode
and set SetMotionQuality(body.GetID(), J.EMotionQuality_LinearCast)
.
7. Common Patterns & Tips
- Separation of Concerns
- Keep “build bodies” in one system, “update the world” in another, and “sync transforms” in a third.
- Deferred Body Creation
- If your engine loads slowly or geometry is not ready, a system can wait until necessary data is present.
- Destroying Entities
- Remember to remove or destroy the underlying Jolt body. For example, if an entity has a
DestroyMe
trait, a system can remove its Jolt body from the world.
- Remember to remove or destroy the underlying Jolt body. For example, if an entity has a
- Performance
- Potentially run heavy collision checks at a lower frequency if your game can tolerate it.
- Accessing ECS or React
- In React code, prefer using actions or hooking into ECS queries to do spawns, impulses, etc.
8. Summary & Next Steps
- Initialize Jolt in a single ECS trait (
JoltWorld
), which you can reference anywhere. - Build bodies for new ECS entities using a system that checks a
NeedsJoltBody
trait or similar. - Step the physics in an
UpdateJolt
system each frame, and sync transforms with your ECS or Three.js objects. - For collisions or advanced features (e.g. joints, triggers), adapt the same pattern: define traits, attach them, and process with systems.
Next Steps:
- Batched Mesh: If you have many objects, see Batch Meshes for rendering optimizations.
- Actions: Create ECS actions to spawn, remove, or apply forces to bodies in a centralized, testable manner.
- In-Depth: Explore official Jolt Physics docs for advanced usage: constraints, sleeping, broad-phase tuning, etc.
That’s it! You now have a fully integrated ECS + Jolt physics pipeline in Viber3D.