Physics Simulation in C++ with Omni Physics: Real-Time Rigid Body and Particle Systems
The omni_physics_simulation module brings compiled C++ physics to your AI game pipeline — rigid body dynamics, constraint solvers, and GPU-accelerated particle systems accessible from Python via pybind11.
Physics simulation is where game feel comes from. The way a crate slides when shot, the way cloth drapes over a character, the way particles scatter from an explosion — all of it runs through physics code. And if that code is slow, your game stutters.
The omni_physics_simulation module is a compiled C++ physics engine exposed to Python via pybind11. It handles rigid body dynamics, joint constraints, collision detection, and GPU-accelerated particle systems — and integrates directly with the NEPA AI Game Scene Workspace for procedural level generation with physically accurate object placement.
What the Module Provides
The compiled module exposes four primary subsystems:
import omni_physics_simulation as phys
# Initialize the physics world
world = phys.PhysicsWorld(
gravity=(0.0, -9.81, 0.0),
substeps=4, # solver substeps per frame (higher = more accurate)
time_step=1.0 / 60.0, # 60Hz simulation
solver="TGS" # Temporal Gauss-Seidel (stable for stacking)
)
# Four primary systems
rigid_body_sim = world.rigid_body_system
constraint_system = world.constraint_system
particle_system = world.particle_system
collision_system = world.collision_system
The C++ backend runs the heavy math on CPU (multi-threaded) or GPU (CUDA particle systems). Python just calls into the API — you never write C++.
Rigid Body Dynamics
The most common use case: objects with mass that respond to forces, collisions, and gravity.
import omni_physics_simulation as phys
import numpy as np
world = phys.PhysicsWorld(gravity=(0, -9.81, 0), substeps=4)
rb = world.rigid_body_system
# Create a box (1m × 1m × 1m, 10kg)
box_id = rb.create_box(
half_extents=(0.5, 0.5, 0.5),
mass=10.0,
position=(0.0, 5.0, 0.0),
rotation=(0.0, 0.0, 0.0, 1.0), # quaternion: (x, y, z, w)
restitution=0.3, # bounciness (0=no bounce, 1=perfect)
friction=0.6 # surface friction
)
# Create static floor
floor_id = rb.create_box(
half_extents=(10.0, 0.1, 10.0),
mass=0.0, # mass=0 means static (infinite mass)
position=(0.0, 0.0, 0.0),
rotation=(0.0, 0.0, 0.0, 1.0)
)
# Apply initial impulse (push the box)
rb.apply_impulse(box_id, force=(5.0, 0.0, 2.0), local_point=(0, 0, 0))
# Step the simulation
for frame in range(300): # 5 seconds at 60fps
world.step()
pos = rb.get_position(box_id)
rot = rb.get_rotation(box_id)
if frame % 30 == 0: # print every half second
print(f"Frame {frame}: pos={pos}, rot={rot}")
Constraint System
Joints connect rigid bodies and constrain their relative motion:
# Hinge joint — rotates around one axis (door, wheel)
door_body = rb.create_box(
half_extents=(0.05, 1.0, 0.5),
mass=5.0,
position=(1.0, 1.0, 0.0)
)
wall_anchor = rb.create_box(
half_extents=(0.05, 1.0, 0.05),
mass=0.0,
position=(1.0, 1.0, 0.0)
)
# Create hinge at the edge of the door
hinge = world.constraint_system.create_hinge(
body_a=wall_anchor,
body_b=door_body,
pivot_point=(1.0, 1.0, 0.5), # world position of hinge
axis=(0.0, 1.0, 0.0), # Y-axis rotation
lower_angle=-np.pi / 2, # -90 degrees
upper_angle=np.pi / 2 # +90 degrees
)
# Apply torque to swing the door open
rb.apply_torque(door_body, (0.0, 15.0, 0.0))
# Spring joint — elastic connection between two bodies
spring = world.constraint_system.create_spring(
body_a=box_id,
body_b=door_body,
stiffness=100.0, # N/m
damping=5.0, # dampens oscillation
rest_length=2.0 # natural length in meters
)
GPU Particle Systems
For fire, smoke, sparks, water, and debris — particle systems run best on GPU:
# Create a fire particle emitter
fire_system = world.particle_system.create_emitter(
position=(0.0, 0.5, 0.0),
emission_rate=500, # particles per second
particle_lifetime=(0.5, 2.0), # min/max seconds per particle
initial_velocity_min=(-0.5, 2.0, -0.5),
initial_velocity_max=(0.5, 5.0, 0.5),
gravity_scale=(-0.5), # fire rises slightly against gravity
size_over_life=[(0.0, 0.05), (0.5, 0.08), (1.0, 0.0)], # (t, size) keyframes
color_over_life=[
(0.0, (1.0, 0.8, 0.0, 1.0)), # yellow-orange at birth
(0.5, (1.0, 0.3, 0.0, 0.8)), # orange mid-life
(1.0, (0.2, 0.2, 0.2, 0.0)) # grey smoke at death
],
backend="cuda" # GPU acceleration
)
# Simulate and extract positions for rendering
for frame in range(600): # 10 seconds
world.step()
# Get particle positions for this frame
positions = world.particle_system.get_positions(fire_system)
colors = world.particle_system.get_colors(fire_system)
sizes = world.particle_system.get_sizes(fire_system)
# Pass to your renderer (OpenGL, Blender, Unity via bridge, etc.)
# renderer.draw_particles(positions, colors, sizes)
Collision Detection and Raycasting
collision = world.collision_system
# Raycast — fire a ray and get the first hit
hit = collision.raycast(
origin=(0.0, 10.0, 0.0),
direction=(0.0, -1.0, 0.0),
max_distance=100.0
)
if hit:
print(f"Hit body: {hit.body_id}")
print(f"Hit point: {hit.point}")
print(f"Hit normal: {hit.normal}")
print(f"Distance: {hit.distance}")
# Overlap query — find all bodies within a sphere
nearby = collision.overlap_sphere(
center=(0.0, 0.0, 0.0),
radius=5.0
)
print(f"Bodies within 5m: {nearby}")
# Continuous collision detection for fast-moving objects
rb.set_ccd_enabled(box_id, True) # prevents tunneling at high velocity
Procedural Level Generation with Physics Validation
The killer integration: use the physics sim to validate procedurally generated levels before they ship to the player.
def validate_level_physics(level_data: dict) -> dict:
"""
Simulate a generated level to find physics issues:
- Objects floating or clipping
- Unstable stacks that would immediately collapse
- Unreachable areas (pathfinding via physics)
"""
world = phys.PhysicsWorld(gravity=(0, -9.81, 0), substeps=8)
rb = world.rigid_body_system
body_ids = {}
# Place all level objects
for obj in level_data["objects"]:
if obj["type"] == "static":
body_ids[obj["id"]] = rb.create_box(
half_extents=obj["half_extents"],
mass=0.0,
position=obj["position"],
rotation=obj["rotation"]
)
else:
body_ids[obj["id"]] = rb.create_box(
half_extents=obj["half_extents"],
mass=obj.get("mass", 1.0),
position=obj["position"],
rotation=obj["rotation"]
)
# Simulate for 3 seconds to let things settle
for _ in range(180):
world.step()
# Check for objects that moved too far from their placed position
issues = []
for obj in level_data["objects"]:
if obj["type"] == "static":
continue
final_pos = rb.get_position(body_ids[obj["id"]])
placed_pos = obj["position"]
drift = np.linalg.norm(np.array(final_pos) - np.array(placed_pos))
if drift > 0.5: # moved more than 50cm after settling
issues.append({
"object_id": obj["id"],
"issue": "unstable_placement",
"drift_meters": drift,
"final_position": final_pos
})
return {
"valid": len(issues) == 0,
"issues": issues,
"issue_count": len(issues)
}
# Use in level generation loop
def generate_valid_level():
for attempt in range(10):
level = generate_level_procedurally()
validation = validate_level_physics(level)
if validation["valid"]:
print(f"Level valid after {attempt + 1} attempts")
return level
else:
print(f"Attempt {attempt + 1}: {validation['issue_count']} issues — regenerating")
return None
Performance Benchmarks
On a Ryzen 9 3950X + RTX 3090:
| Scenario | Bodies | Particles | Step Time | |---|---|---|---| | Simple falling objects | 100 | 0 | 0.8ms | | Complex stacking | 500 | 0 | 3.2ms | | Fire effect | 0 | 10,000 | 0.4ms (GPU) | | Full game scene | 200 | 5,000 | 2.1ms |
Well within real-time budget for 60fps (16.7ms/frame). The CUDA particle backend runs particle simulation almost entirely on GPU with minimal CPU overhead.
The NEPA AI Game Scene Workspace integrates omni_physics_simulation directly into its procedural level generation pipeline. Every generated scene is physics-validated before export — no floating crates, no unstable stacks, no clipping geometry.
→ Get the Game Scene Workspace at /shop/game-scene-workspace
Build game scenes that feel real, because they are.