Back to Blog
2026-03-22

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.