This guide helps you configure realistic physics simulations by choosing appropriate material properties, simulation parameters, and troubleshooting common issues.
Density determines an object's mass and inertia based on its volume. Higher density = heavier object = harder to move.
| Material | Density (kg/m³) | Use Cases |
|---|---|---|
| Air | 1.2 | Balloons, airships |
| Wood (Balsa) | 160 | Lightweight props |
| Wood (Pine) | 500-600 | Furniture, crates |
| Wood (Oak) | 700-900 | Heavy furniture, beams |
| Ice | 917 | Frozen objects |
| Water | 1000 | Reference value, liquids |
| Concrete | 2400 | Buildings, structures |
| Glass | 2500 | Windows, bottles |
| Aluminum | 2700 | Lightweight metal objects |
| Stone (Granite) | 2750 | Rocks, monuments |
| Steel | 7850 | Heavy machinery, I-beams |
| Copper | 8960 | Wires, pipes |
| Lead | 11340 | Very heavy objects |
| Gold | 19320 | Treasure, jewelry |
// Example: Two boxes with same size, different densities
lightBox := actor.NewBox(
transform,
mgl64.Vec3{1, 1, 1}, // 1m x 1m x 1m
500, // Wood density
)
// mass = density * volume = 500 * (1*1*1) = 500 kg
heavyBox := actor.NewBox(
transform,
mgl64.Vec3{1, 1, 1}, // Same size
7850, // Steel density
)
// mass = 7850 * (1*1*1) = 7850 kg
// The steel box is 15.7x heavier!
// - Requires 15.7x more force to accelerate
// - Falls at same speed (gravity affects all masses equally)
// - Has 15.7x more momentum when movingFor realistic simulation: Use real-world values from table above
For gameplay:
- Heavy objects (800-10000 kg/m³): Obstacles, boss enemies, wrecking balls
- Medium objects (100-800 kg/m³): Player, props, crates
- Light objects (10-100 kg/m³): Debris, decorations, pickups
Pro tip: Mass ratio between interacting objects should ideally be <10:1 for stable simulation. If you need a massive object (planet), make it static instead.
Restitution controls "bounciness" - how much kinetic energy is retained after collision.
| Value | Behavior | Real Materials | Game Use |
|---|---|---|---|
| 0.0 | No bounce (perfectly inelastic) | Clay, putty, wet sand | Sticky surfaces, dampening |
| 0.1-0.2 | Minimal bounce | Lead, wet wood | Realistic ground |
| 0.3-0.4 | Slight bounce | Concrete, hard wood | Standard surfaces |
| 0.5-0.6 | Moderate bounce | Hard plastic, stone | Slightly bouncy |
| 0.7-0.8 | High bounce | Rubber, basketballs | Bouncy surfaces |
| 0.9 | Very high bounce | Super balls | Game power-ups |
| 0.95-1.0 | Nearly perfect bounce | Steel on steel | Pinball, special effects |
| >1.0 | Gains energy (unphysical!) | N/A | Avoid (causes instability) |
When two objects collide, Feather combines their restitution values:
// Current implementation: Maximum of the two
combinedRestitution = max(bodyA.Restitution, bodyB.Restitution)
// Alternative approaches (not currently used):
// Average: (bodyA.Restitution + bodyB.Restitution) / 2
// Multiply: bodyA.Restitution * bodyB.RestitutionWhy maximum? A bouncy ball (e=0.9) should bounce on any surface, even clay (e=0.0).
// Example: Dropping a ball from 10m height
// Clay ball (e = 0.0)
ball1 := actor.NewSphere(transform, 0.5, 1000)
ball1.Restitution = 0.0
// Hits ground at ~14 m/s, bounces at 0 m/s → stops dead
// Basketball (e = 0.8)
ball2 := actor.NewSphere(transform, 0.5, 1000)
ball2.Restitution = 0.8
// Hits ground at ~14 m/s, bounces at 11.2 m/s → bounces to 6.4m
// Super ball (e = 0.95)
ball3 := actor.NewSphere(transform, 0.5, 1000)
ball3.Restitution = 0.95
// Hits ground at ~14 m/s, bounces at 13.3 m/s → bounces to 9mFor realism: Use values from table (most materials: 0.2-0.6)
For gameplay:
- Sticky platforms (0.0): Player shouldn't bounce when landing
- Standard ground (0.1-0.3): Slight bounce, feels natural
- Bouncy obstacles (0.7-0.9): Fun gameplay mechanic
- Hyper-bounce (0.95+): Special power-up zones
Pro tip: Extremely high restitution (>0.95) can cause jitter. If objects won't settle, reduce restitution or increase compliance.
Compliance controls constraint "softness" - how much constraints are allowed to violate before being enforced.
Physics Meaning: Compliance = 1 / Stiffness
| Value | Behavior | Visual Effect | Use Cases |
|---|---|---|---|
| 0 | Infinitely stiff (hard constraint) | No penetration, potential jitter | Ideal (but may be unstable) |
| 1e-10 | Extremely stiff | Tiny penetration, may jitter | Very rigid contacts |
| 1e-9 | Very stiff (default) | Barely visible penetration | Standard rigid bodies |
| 1e-8 | Stiff | Slight penetration, smoother | Stable rigid bodies |
| 1e-7 | Moderate | Noticeable soft contact | Slightly squishy objects |
| 1e-6 | Soft | Visible squishing | Soft bodies, cushions |
| 1e-5 | Very soft | Significant deformation | Jello, very soft materials |
Low Compliance (1e-10):
┌────┐
│ │ ← Object barely penetrates surface
└────┘
═══════ Ground
High Compliance (1e-6):
┌────┐
│ │
└─ ┬ ┴┘ ← Object visibly sinks into surface
══╧═══ Ground
Trade-off:
- Lower compliance → Stiffer contacts → Less penetration → More jitter/instability
- Higher compliance → Softer contacts → More penetration → More stable/smooth
- Start with default:
1e-9(very stiff) - If jittery/vibrating: Increase compliance by 10x (
1e-8) - If too much penetration: Decrease compliance by 10x (
1e-10) - If still unstable: Increase substeps or decrease timestep
- Iterate until satisfied
// Current: Compliance is hardcoded in epa/epa.go
// Future: Will be a material property
// Temporary workaround: Modify epa/epa.go constant
const DefaultCompliance = 1e-9 // Adjust this value
// Ideal future API:
material := actor.Material{
Density: 500,
Restitution: 0.3,
Compliance: 1e-8, // Per-material compliance
}
box := actor.NewBoxWithMaterial(transform, halfExtents, material)These parameters affect the global simulation quality and performance.
Timestep is how much simulated time passes per physics update.
| Timestep | FPS Equivalent | Use Case |
|---|---|---|
| 1/30 (0.0333s) | 30 FPS | Slow-paced games, low-end devices |
| 1/60 (0.0167s) | 60 FPS | Standard for most games |
| 1/120 (0.0083s) | 120 FPS | High-precision simulation |
| 1/240 (0.0042s) | 240 FPS | Very fast objects, high accuracy |
Fixed Timestep (Recommended):
const physicsTimestep = 1.0 / 60.0 // 60 FPS
func GameLoop() {
accumulator := 0.0
for {
frameTime := GetFrameTime()
accumulator += frameTime
// Update physics in fixed timesteps
for accumulator >= physicsTimestep {
world.Step(physicsTimestep)
accumulator -= physicsTimestep
}
Render()
}
}Benefits:
- Deterministic (same input → same output)
- Stable (physics tuned for one timestep)
- Prevents physics explosions from frame rate drops
Variable Timestep (Not Recommended):
func GameLoop() {
for {
dt := GetFrameTime() // Variable!
world.Step(dt) // Unstable
Render()
}
}Problems:
- Non-deterministic
- Unstable (large dt can cause explosions)
- Difficult to tune
For most games: 1/60 (60 FPS physics)
Use smaller timestep if:
- Fast-moving objects tunnel through walls
- High restitution causes instability
- Simulation feels "floaty" or imprecise
Trade-off: Smaller timestep = more accurate but more CPU cost
Substeps divide each physics step into smaller internal steps for better accuracy.
func Step(dt float64, substeps int) {
subDt := dt / float64(substeps)
for i := 0; i < substeps; i++ {
// Run full physics pipeline
ApplyForces(subDt)
DetectCollisions()
SolveConstraints(subDt)
IntegrateVelocities(subDt)
}
}
// Example: dt=1/60, substeps=4
// Each substep processes 1/240 of a second
// Reduces tunneling and improves stability| Substeps | Use Case | CPU Cost |
|---|---|---|
| 1 | Standard scenes, medium speeds | 1x (baseline) |
| 2 | Fast-moving objects, bouncy materials | 2x |
| 4 | Very fast projectiles, high stacks | 4x |
| 8+ | Extreme accuracy requirements | 8x+ |
When to increase substeps:
- Fast objects tunnel through thin walls
- Bouncy objects (high restitution) jitter or explode
- Tall stacks collapse unrealistically
- Constraints feel "soft" even with low compliance
// Currently: Substeps are internal to World.Step()
// Check world.go for substep implementation
world := NewWorld()
world.Substeps = 4 // If this property exists
world.Step(1.0 / 60.0)IMPORTANT: XPBD uses substeps with ONE solver iteration per substep, not multiple solver iterations per step.
The key difference from traditional solvers:
- Traditional solvers: 1 step, many iterations
- XPBD: Many substeps, 1 iteration per substep
// XPBD approach (what Feather uses)
func Step(dt float64, substeps int) {
h := dt / float64(substeps)
for i := 0; i < substeps; i++ {
// Apply forces & integrate velocities
// Detect collisions
// Solve constraints - SINGLE iteration only!
for each contact {
SolvePositionConstraint(h)
}
for each contact {
SolveVelocityConstraint(h)
}
// Integrate positions
}
}| Substeps | Quality | Use Case | CPU Cost |
|---|---|---|---|
| 1 | Standard | Most scenes | 1x (baseline) |
| 2 | Better | Fast objects, bouncy | 2x |
| 4 | High | Very fast, tall stacks | 4x |
| 8+ | Very high | Extreme accuracy | 8x+ |
When to increase substeps (NOT iterations):
- Fast-moving objects tunnel through walls
- Tall stacks collapse unrealistically
- High restitution causes instability
- Need more precise integration
Trade-off: More substeps = more accurate but slower
Why this works: Each substep operates on a smaller timestep, improving integration accuracy and constraint stability without needing multiple iterations.
// Current implementation (world.go)
world := NewWorld()
world.Substeps = 2 // 2 substeps, 1 iteration each
world.Step(1.0 / 60.0) // Total: 2 solver passesGoal: Build a stable tower of boxes
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0}, // Normal pointing up
0, // Distance from origin
)
ground.Static = true
ground.Restitution = 0.1 // Minimal bounce
boxes := make([]*actor.RigidBody, 10)
for i := 0; i < 10; i++ {
box := actor.NewBox(
actor.Transform{Position: mgl64.Vec3{0, float64(i)*2 + 1, 0}},
mgl64.Vec3{0.5, 1, 0.5}, // 1m x 2m x 1m boxes
700, // Wood density
)
box.Restitution = 0.1 // Low bounce
boxes[i] = box
world.AddBody(box)
}
// Simulation settings
dt := 1.0 / 60.0
substeps := 2 // XPBD uses substeps, not solver iterations| Problem | Cause | Solution |
|---|---|---|
| Stack collapses immediately | Boxes spawned overlapping | Space boxes further apart initially |
| Stack wobbles and falls | Too few substeps | Increase substeps to 4 |
| Boxes vibrate/jitter | Compliance too low | Increase compliance to 1e-8 |
| Boxes sink into each other | Compliance too high | Decrease compliance to 1e-9 |
| Stack slowly tips over | Numerical drift | Increase substeps to 2-4 |
Goal: Realistic bouncy ball that loses energy gradually
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0},
0,
)
ground.Static = true
ground.Restitution = 0.8 // Bouncy surface
ball := actor.NewSphere(
actor.Transform{Position: mgl64.Vec3{0, 10, 0}}, // 10m high
0.5, // 0.5m radius
1100, // Rubber density
)
ball.Restitution = 0.85 // High restitution
world.AddBody(ground)
world.AddBody(ball)
// Combined restitution: max(0.8, 0.85) = 0.85
// Ball will bounce to ~72% of previous height each bounceDrop height: 10m
Bounce 1: ~7.2m (0.85² ≈ 0.72)
Bounce 2: ~5.2m
Bounce 3: ~3.7m
Bounce 4: ~2.7m
...eventually settles
| Problem | Cause | Solution |
|---|---|---|
| Ball doesn't bounce | Restitution too low | Increase to 0.7-0.9 |
| Ball bounces forever | Restitution too high (>1.0) | Reduce to ≤0.95 |
| Ball bounces higher each time | Restitution >1.0 or solver bug | Check restitution value |
| Ball vibrates on ground | High restitution + low compliance | Increase compliance or reduce restitution |
| Energy loss too fast | Combined restitution low | Increase restitution on both objects |
Goal: Objects at rest shouldn't vibrate or jitter
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0},
0,
)
ground.Static = true
ground.Restitution = 0.0 // No bounce for resting
box := actor.NewBox(
actor.Transform{Position: mgl64.Vec3{0, 1, 0}},
mgl64.Vec3{1, 1, 1},
500, // Wood
)
box.Restitution = 0.0 // No bounce
// Let it fall and settle
// Tuning parameters
compliance := 1e-8 // Slightly soft for stability
velocityThreshold := 0.01 // Sleep threshold (future feature)| Problem | Cause | Solution |
|---|---|---|
| Object vibrates up/down | Compliance too low | Increase compliance to 1e-8 or 1e-7 |
| Object slowly sinks | Compliance too high | Decrease compliance to 1e-9 |
| Object drifts sideways | Friction not implemented | Wait for friction feature, or increase compliance |
| Vibration at high FPS | Timestep too small for compliance | Adjust compliance proportionally |
Goal: Prevent fast projectiles from passing through thin walls
Frame 1: Frame 2:
•
(ball)
| |•
| wall (ball passed through!)
| |
When object moves >1 thickness per frame, it can "teleport" through walls.
Solution 1: Increase Substeps (Recommended)
world.Substeps = 4 // Check collision 4x per frame
// Effective speed limit = wallThickness * substeps / dtSolution 2: Decrease Timestep
dt = 1.0 / 120.0 // 120 FPS physics (2x more CPU)Solution 3: Thicken Walls
wall := actor.NewBox(
transform,
mgl64.Vec3{5, 10, 2}, // 4m thick instead of 0.5m
2400, // Concrete
)Solution 4: Continuous Collision Detection (Future Feature)
bullet.CCD = true // Will detect collision along swept pathMaximum safe speed = wallThickness / (dt / substeps)
Example:
- Wall thickness: 0.5m
- Timestep: 1/60 = 0.0167s
- Substeps: 4
- Safe speed: 0.5 / (0.0167/4) = 120 m/s
For faster speeds, increase substeps or decrease dt
package main
import (
"github.com/akmonengine/feather"
"github.com/akmonengine/feather/actor"
"github.com/go-gl/mathgl/mgl64"
)
func main() {
// Create world with gravity
world := feather.NewWorld()
world.Gravity = mgl64.Vec3{0, -9.81, 0} // Earth gravity
// Create ground plane
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0}, // Normal (up)
0,
)
ground.Static = true
ground.Restitution = 0.3
world.AddBody(ground)
// Create falling box
box := actor.NewBox(
actor.Transform{Position: mgl64.Vec3{0, 10, 0}},
mgl64.Vec3{0.5, 0.5, 0.5}, // 1m cube
700, // Wood density
)
box.Restitution = 0.4
world.AddBody(box)
// Simulation loop
dt := 1.0 / 60.0
for i := 0; i < 600; i++ { // 10 seconds
world.Step(dt)
// Render or log positions
}
}func CreateTower(world *feather.World, height int) {
for i := 0; i < height; i++ {
box := actor.NewBox(
actor.Transform{
Position: mgl64.Vec3{0, float64(i)*2.0 + 1.0, 0},
},
mgl64.Vec3{0.5, 1.0, 0.5}, // 1m x 2m x 1m
700, // Wood
)
box.Restitution = 0.1 // Low bounce for stability
world.AddBody(box)
}
}
func main() {
world := feather.NewWorld()
world.Gravity = mgl64.Vec3{0, -9.81, 0}
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0},
0,
)
ground.Static = true
ground.Restitution = 0.1
world.AddBody(ground)
CreateTower(world, 10) // 10-box tower
// Stable simulation settings
dt := 1.0 / 60.0
substeps := 2 // XPBD: 2 substeps = 2 solver passes total
for i := 0; i < 1200; i++ { // 20 seconds
world.Step(dt)
}
}func main() {
world := feather.NewWorld()
world.Gravity = mgl64.Vec3{0, -9.81, 0}
ground := actor.NewPlane(
actor.Transform{Position: mgl64.Vec3{0, 0, 0}},
mgl64.Vec3{0, 1, 0},
0,
)
ground.Static = true
ground.Restitution = 0.8 // Bouncy floor
world.AddBody(ground)
// Different ball materials
materials := []struct {
name string
restitution float64
density float64
x float64
}{
{"Clay", 0.0, 1500, -3},
{"Wood", 0.3, 700, -1},
{"Rubber", 0.85, 1100, 1},
{"Super Ball", 0.95, 1100, 3},
}
for _, mat := range materials {
ball := actor.NewSphere(
actor.Transform{Position: mgl64.Vec3{mat.x, 10, 0}},
0.5,
mat.density,
)
ball.Restitution = mat.restitution
world.AddBody(ball)
}
// Run simulation and observe different bounce behaviors
dt := 1.0 / 60.0
for i := 0; i < 600; i++ {
world.Step(dt)
}
}func CreateNewtonsCradle(world *feather.World) {
// Five spheres in a row
for i := 0; i < 5; i++ {
sphere := actor.NewSphere(
actor.Transform{
Position: mgl64.Vec3{float64(i) * 1.1, 5, 0},
},
0.5, // Radius
7850, // Steel density
)
sphere.Restitution = 0.95 // Nearly elastic
world.AddBody(sphere)
}
// Pull first sphere back and release
// (requires distance constraint - not yet implemented)
// For now, just give it initial velocity:
spheres := world.GetBodies()
spheres[0].Velocity = mgl64.Vec3{5, 0, 0} // Push rightward
}
// Expected behavior: Energy transfers through chain
// First ball stops, last ball swings out// Define common material presets
type MaterialPreset struct {
Density float64
Restitution float64
}
var Materials = map[string]MaterialPreset{
"Wood": {700, 0.3},
"Stone": {2750, 0.2},
"Steel": {7850, 0.6},
"Rubber": {1100, 0.85},
"Glass": {2500, 0.4},
"Concrete": {2400, 0.2},
"Ice": {917, 0.05},
}
// Usage:
mat := Materials["Wood"]
box := actor.NewBox(transform, halfExtents, mat.Density)
box.Restitution = mat.Restitution| Symptom | Check | Typical Fix |
|---|---|---|
| Objects jitter/vibrate | Compliance | Increase to 1e-8 |
| Objects sink into ground | Compliance | Decrease to 1e-9 |
| Objects bounce forever | Restitution | Reduce to <0.95 |
| No bounce at all | Restitution | Increase to >0.3 |
| Stack collapses | Substeps | Increase to 4 |
| Fast objects tunnel | Substeps | Increase to 4+ |
| Simulation too slow | Substeps | Reduce to 1 |
| Unrealistic movement | Density | Use real-world values |
- Use static bodies for immovable objects (ground, walls)
- Implement sleep system (deactivate resting bodies) - future feature
- Use minimum substeps (usually 1-2 is sufficient)
- Use spatial acceleration for broad phase - future feature
- Optimize contact manifolds (cache between frames) - future feature
Target: 60 FPS (16.67ms per frame)
Physics budget: 5ms
Rough estimates:
- 50 bodies: ~2ms (comfortable)
- 100 bodies: ~5ms (at limit)
- 200 bodies: ~12ms (need optimization)
If over budget:
1. Reduce substeps (2 → 1)
2. Increase timestep (1/60 → 1/30)
3. Implement spatial grid (future)
4. Implement sleep/islands (future)
For architectural decisions and design rationale, see ARCHITECTURE.md. For detailed algorithm explanations, see ALGORITHMS.md.