Physics Umbrella Runtime#

Physics Umbrella is a framework that allows third-party physics engines to integrate with the Omni Physics ecosystem. It is part of the omni.physics extension system and provides a unified interface for registering custom simulators that can participate in the physics pipeline, receive stage updates, perform scene queries, handle user interactions, and provide benchmarking data.

Overview#

The system is built around the concept of a Simulation structure that contains function pointers organized into five functional groups:

  • SimulationFns - Core simulation lifecycle and physics operations

  • StageUpdateFns - Stage attachment and timeline event handling

  • SceneQueryFns - Raycasts, sweeps, and overlap queries

  • InteractionFns - User interaction handling (picking, reset behavior)

  • BenchmarkFns - Performance profiling and statistics

The Simulation structure serves as the main container for all physics functionality:

struct Simulation
{
    SimulationFns simulationFns;
    StageUpdateFns stageUpdateFns;
    SceneQueryFns sceneQueryFns;
    InteractionFns interactionFns;
    BenchmarkFns benchmarkFns;
};

When a simulation is registered using IPhysics::registerSimulation(), the physics system calls the registered function pointers at appropriate times during the simulation lifecycle.

Registering a Simulation#

The IPhysics interface is the main entry point for registering and managing simulations. It provides:

Registration Management:
  • registerSimulation() - Register a new simulation with the physics system

  • unregisterSimulation() - Remove a simulation from the physics system

Query Functions:
  • getSimulation() - Get simulation structure by ID

  • getSimulationName() - Get simulation name by ID

  • getNumSimulations() - Get count of registered simulations

  • getSimulationIds() - Get array of all registered simulation IDs

Activation Control:
  • activateSimulation() - Enable a simulator to receive updates

  • deactivateSimulation() - Disable a simulator from receiving updates

  • isSimulationActive() - Check if a simulator is currently active

Event Subscriptions:
  • subscribeSimulationRegistryEvents() - Listen for simulation lifecycle events

  • unsubscribeSimulationRegistryEvents() - Stop listening for events

Note

Simulations are automatically active upon registration but do not trigger the ACTIVATED event at that time.

C++ Example - Registering a Simulation#

#include <omni/physics/simulation/IPhysics.h>

using namespace omni::physics;

// Acquire the physics interface
IPhysics* physics = framework->acquireInterface<IPhysics>();

// Create and configure your simulation
Simulation simulation;

// Register with the physics system
const char* simulationName = "MyCustomSimulation";
SimulationId simId = physics->registerSimulation(simulation, simulationName);

if (simId != kInvalidSimulationId)
{
    // Simulation registered successfully
    // It is automatically active after registration

    // Check if it's active
    bool isActive = physics->isSimulationActive(simId);

    // Deactivate if needed
    physics->deactivateSimulation(simId);

    // Reactivate later
    physics->activateSimulation(simId);
}

// When done, unregister
physics->unregisterSimulation(simId);

Python Example - Registering a Simulation#

from omni.physics.core import (
    Simulation,
    get_physics_interface,
    k_invalid_simulation_id
)

# Get the physics interface
physics = get_physics_interface()

# Create a simulation instance
simulation = Simulation()

# Register the simulation
simulation_name = "MyCustomSimulation"
simulation_id = physics.register_simulation(simulation, simulation_name)

if simulation_id != k_invalid_simulation_id:
    # Simulation registered successfully

    # Query simulation state
    is_active = physics.is_simulation_active(simulation_id)

    # Get simulation name
    name = physics.get_simulation_name(simulation_id)

    # Deactivate/activate as needed
    physics.deactivate_simulation(simulation_id)
    physics.activate_simulation(simulation_id)

# Unregister when done
physics.unregister_simulation(simulation_id)

Querying Registered Simulations#

Applications can query information about all registered simulations:

C++ Example - Querying Simulations#

#include <omni/physics/simulation/IPhysics.h>
#include <vector>

using namespace omni::physics;

IPhysics* physics = framework->acquireInterface<IPhysics>();

// Get number of registered simulations
size_t numSimulations = physics->getNumSimulations();
carb::log_info("Number of registered simulations: %zu", numSimulations);

// Get all simulation IDs
std::vector<SimulationId> simulationIds(numSimulations);
size_t actualCount = physics->getSimulationIds(simulationIds.data(), numSimulations);

// Iterate through all registered simulations
for (size_t i = 0; i < actualCount; ++i)
{
    SimulationId simId = simulationIds[i];
    const char* name = physics->getSimulationName(simId);
    bool isActive = physics->isSimulationActive(simId);

    carb::log_info("Simulation[%zu]: %s (ID: %zu, Active: %s)",
        i, name, simId.id, isActive ? "Yes" : "No");

    // Get the simulation structure
    const Simulation* sim = physics->getSimulation(simId);
    if (sim)
    {
        // Access simulation function pointers
        // sim->simulationFns.initialize, etc.
    }
}

Python Example - Querying Simulations#

from omni.physics.core import get_physics_interface

physics = get_physics_interface()

# Get number of registered simulations
num_simulations = physics.get_num_simulations()
print(f"Number of registered simulations: {num_simulations}")

# Get all simulation IDs
simulation_ids = physics.get_simulation_ids()

# Iterate through all registered simulations
for i, sim_id in enumerate(simulation_ids):
    name = physics.get_simulation_name(sim_id)
    is_active = physics.is_simulation_active(sim_id)

    print(f"Simulation[{i}]: {name} (ID: {sim_id}, Active: {is_active})")

    # Get the simulation structure
    sim = physics.get_simulation(sim_id)
    if sim:
        # Access simulation functions
        # sim.simulation_fns.initialize, etc.
        pass

Simulation Registry Events#

Applications can subscribe to events that provide notifications when simulations are registered, unregistered, activated, or deactivated.

Event Types (SimulationRegistryEventType):

  • eSIMULATION_REGISTERED / SIMULATION_REGISTERED - A new simulation has been registered

  • eSIMULATION_UNREGISTERED / SIMULATION_UNREGISTERED - A simulation has been unregistered (simulation ID is no longer valid after this event)

  • eSIMULATION_ACTIVATED / SIMULATION_ACTIVATED - A simulation has been activated (not triggered during initial registration)

  • eSIMULATION_DEACTIVATED / SIMULATION_DEACTIVATED - A simulation has been deactivated

Important

  • Events are triggered after the associated action has been executed

  • Simulations are automatically active upon registration but do not trigger ACTIVATED event

  • After UNREGISTERED event, the simulation ID is no longer valid for API use

C++ Example - Subscribing to Registry Events#

// C++ Example
SubscriptionId subscriptionId = physics->subscribeSimulationRegistryEvents(
    [](SimulationRegistryEventType::Enum eventType,
       const SimulationId& id,
       const char* name,
       void* userData)
    {
        switch (eventType)
        {
            case SimulationRegistryEventType::eSIMULATION_REGISTERED:
                // Handle registration
                break;
            case SimulationRegistryEventType::eSIMULATION_UNREGISTERED:
                // Handle unregistration
                break;
            case SimulationRegistryEventType::eSIMULATION_ACTIVATED:
                // Handle activation
                break;
            case SimulationRegistryEventType::eSIMULATION_DEACTIVATED:
                // Handle deactivation
                break;
        }
    },
    nullptr  // userData
);

// Unsubscribe when done
physics->unsubscribeSimulationRegistryEvents(subscriptionId);
# Python Example
from omni.physics.core import SimulationRegistryEventType

def on_registry_event(event_type, simulation_id, simulation_name):
    if event_type == SimulationRegistryEventType.SIMULATION_REGISTERED:
        print(f"Simulation registered: {simulation_name}")
    elif event_type == SimulationRegistryEventType.SIMULATION_UNREGISTERED:
        print(f"Simulation unregistered: {simulation_name}")
    elif event_type == SimulationRegistryEventType.SIMULATION_ACTIVATED:
        print(f"Simulation activated: {simulation_name}")
    elif event_type == SimulationRegistryEventType.SIMULATION_DEACTIVATED:
        print(f"Simulation deactivated: {simulation_name}")

subscription = physics.subscribe_simulation_registry_events(on_registry_event)

# Set to None to unsubscribe
subscription = None

Core Simulation Functions#

The SimulationFns structure contains the core physics simulation functionality. This structure is defined in the IPhysicsSimulation interface and groups all essential simulation operations.

Key Function Groups#

The SimulationFns structure organizes functions into logical groups:

  • Stage Management - initialize, close, getAttachedStage

  • Simulation Control - simulate, simulateAsync, fetchResults, checkResults, flushChanges

  • Change Tracking - pauseChangeTracking, isChangeTrackingPaused

  • Timing - getSimulationTimeStepsPerSecond, getSimulationTimestamp, getSimulationStepCount

  • Body Control - addForceAtPos, addTorque, wakeUp, putToSleep, isSleeping

  • Capability Checking - isCapableOfSimulating

  • Events - subscribePhysicsContactReportEvents, subscribePhysicsOnStepEvents

For complete function signatures and detailed documentation, refer to the IPhysicsSimulation interface header.

Important Concepts#

Change Tracking

The physics system tracks changes to USD stage through a fabric listener. Change tracking can be paused when making many modifications to avoid unnecessary intermediate updates:

// Pause tracking while making bulk changes
simulation->pauseChangeTracking(true);
// Make many USD modifications...
simulation->pauseChangeTracking(false);
simulation->flushChanges();  // Process all buffered changes

Simulation Step Timing

  • simulate() executes for the exact elapsed time passed synchronously - it waits for results before returning and writes output data as if fetchResults() was called. No substepping occurs.

  • simulateAsync() executes for the exact elapsed time passed asynchronously - it returns immediately and the caller must use fetchResults() (blocking) or checkResults() (non-blocking) to retrieve results. No substepping occurs.

  • Recommended maximum step size is 1/60 second for stability

  • getSimulationStepCount() resets to 0 when a new simulation starts

  • getSimulationTimestamp() increments with every simulation step

Stage Initialization and Teardown

The SimulationFns fields initialize and close replace the previous attachStage and detachStage names:

  • initialize(stageId) - Initializes the simulation with a USD stage. Runs the physics parser and populates simulation objects. Any previously attached stage is closed automatically. Returns true on success.

  • close() - Closes the simulation and removes all objects.

  • getAttachedStage() - Returns the currently attached USD stage ID (0 means no stage).

simulate vs simulateAsync

The simulation provides two modes for stepping physics:

  • simulate(elapsedTime, currentTime) runs the physics step synchronously. It blocks until the simulation is complete, then writes output data automatically (equivalent to calling fetchResults() after the step). This is the simplest option when you do not need to overlap computation with the physics step.

  • simulateAsync(elapsedTime, currentTime) runs the physics step asynchronously. It returns immediately, allowing the caller to perform other work while physics is running. After calling simulateAsync, you must retrieve results explicitly:

    • fetchResults() - blocks until the simulation finishes, then writes results.

    • checkResults() - non-blocking check that returns true if the simulation has finished.

Note

Both functions simulate the exact elapsedTime passed with no substepping. It is the caller’s responsibility to provide a reasonable step size (recommended maximum: 1/60 second).

Typical usage patterns:

// Synchronous - simple, blocking
simulation->simulate(1.0f / 60.0f, currentTime);
// Results are already written out, ready to use

// Asynchronous - overlaps work with physics
simulation->simulateAsync(1.0f / 60.0f, currentTime);
// ... do other work while physics runs ...
simulation->fetchResults();  // block and write results

Event Subscription Restrictions

Warning

  • Subscriptions cannot be added or removed within subscribePhysicsOnStepEvents callbacks

  • Contact data from subscribePhysicsContactReportEvents is only valid for one simulation step

  • Step event callbacks with order=0 execute first (highest priority), higher values execute later

C++ Example - Implementing SimulationFns#

#include <omni/physics/simulation/IPhysics.h>
#include <omni/physics/simulation/IPhysicsSimulation.h>

using namespace omni::physics;

class MyPhysicsEngine
{
public:
    MyPhysicsEngine()
    {
        // Initialize simulation functions
        m_simulationFns.initialize = [this](long stageId) -> bool {
            m_attachedStageId = stageId;
            // Initialize physics world from USD stage
            return initializeFromStage(stageId);
        };

        m_simulationFns.close = [this]() {
            m_attachedStageId = 0;
            cleanup();
        };

        m_simulationFns.getAttachedStage = [this]() -> long {
            return m_attachedStageId;
        };

        m_simulationFns.simulate = [this](float elapsedTime, float currentTime) {
            // Run physics simulation
            stepPhysics(elapsedTime);
            m_timestamp++;
            m_stepCount++;

            // Notify pre-step callbacks
            PhysicsStepContext context;
            context.scenePath = m_attachedStageId;
            context.simulationId = m_simId;

            for (auto& [id, callback] : m_stepCallbacks)
            {
                callback(elapsedTime, context);
            }
        };

        m_simulationFns.fetchResults = [this]() {
            // Write results back to USD/Fabric
            writeResultsToStage();

            // Notify contact callbacks
            // IMPORTANT: Contact data is only valid during this callback
            // Copy data if persistence beyond one step is needed
            for (auto& callback : m_contactCallbacks)
            {
                callback(m_contactHeaders, m_contactData, m_frictionAnchors);
            }
        };

        m_simulationFns.checkResults = [this]() -> bool {
            return m_simulationComplete;
        };

        m_simulationFns.flushChanges = [this]() {
            processBufferedChanges();
        };

        m_simulationFns.getSimulationTimestamp = [this]() -> uint64_t {
            return m_timestamp;
        };

        m_simulationFns.getSimulationStepCount = [this]() -> uint64_t {
            return m_stepCount;
        };

        m_simulationFns.subscribePhysicsOnStepEvents =
            [this](bool preStep, int order, OnPhysicsStepEventFn callback) -> SubscriptionId {
                SubscriptionId id = ++m_nextStepCallbackId;
                m_stepCallbacks[id] = callback;
                return id;
            };

        // Note: preStep=true means callback executes BEFORE physics step
        //       preStep=false means callback executes AFTER physics step
        //       order parameter: 0 = highest priority, higher values = lower priority

        m_simulationFns.unsubscribePhysicsOnStepEvents =
            [this](SubscriptionId id) {
                m_stepCallbacks.erase(id);
            };

        m_simulationFns.isCapableOfSimulating =
            [this](const char** schemaNames, size_t schemaNamesCount, bool* isCapable) -> bool {
                // Check if this simulation can handle each schema
                for (size_t i = 0; i < schemaNamesCount; ++i)
                {
                    std::string schemaName(schemaNames[i]);

                    // Check against supported schemas
                    if (schemaName == "PhysicsRigidBodyAPI" ||
                        schemaName == "PhysicsCollisionAPI" ||
                        schemaName == "PhysicsMassAPI" ||
                        schemaName == "PhysicsJoint")
                    {
                        isCapable[i] = true;
                    }
                    else
                    {
                        isCapable[i] = false;
                    }
                }
                return true;  // Return true if capability check was performed
            };
    }

    SimulationFns getSimulationFns() const { return m_simulationFns; }
    void setSimulationId(SimulationId id) { m_simId = id; }

private:
    SimulationFns m_simulationFns;
    SimulationId m_simId;
    long m_attachedStageId = 0;
    uint64_t m_timestamp = 0;
    uint64_t m_stepCount = 0;
    bool m_simulationComplete = false;

    std::unordered_map<SubscriptionId, OnPhysicsStepEventFn> m_stepCallbacks;
    SubscriptionId m_nextStepCallbackId = 0;
    std::vector<OnContactReportEventFn> m_contactCallbacks;
    ContactEventHeaderVector m_contactHeaders;
    ContactDataVector m_contactData;
    FrictionAnchorsDataVector m_frictionAnchors;

    // Implementation methods
    bool initializeFromStage(long stageId) { /* ... */ return true; }
    void cleanup() { /* ... */ }
    void stepPhysics(float dt) { /* ... */ }
    void writeResultsToStage() { /* ... */ }
    void processBufferedChanges() { /* ... */ }
};

// Usage
MyPhysicsEngine engine;

Simulation sim;
sim.simulationFns = engine.getSimulationFns();

SimulationId simId = physics->registerSimulation(sim, "MyEngine");
engine.setSimulationId(simId);

Python Example - Implementing SimulationFns#

from omni.physics.core import (
    Simulation,
    PhysicsStepContext,
    get_physics_interface,
    get_physics_simulation_interface,
    k_invalid_simulation_id
)


class MyPhysicsEngine:
    """Custom physics engine implementation"""

    def __init__(self):
        self.attached_stage_id = 0
        self.timestamp = 0
        self.step_count = 0
        self.step_callbacks = []
        self.simulation_id = k_invalid_simulation_id

    def initialize(self, stage_id):
        """Initialize physics simulation with a USD stage"""
        self.attached_stage_id = stage_id
        # Initialize physics from USD stage
        return True

    def close(self):
        """Close the simulation and cleanup"""
        self.attached_stage_id = 0

    def get_attached_stage(self):
        """Get currently attached stage"""
        return self.attached_stage_id

    def simulate(self, elapsed_time, current_time):
        """Execute one physics step"""
        # Run physics simulation
        self._step_physics(elapsed_time)
        self.timestamp += 1
        self.step_count += 1

        # Notify step callbacks
        context = PhysicsStepContext()
        context.scene_path = self.attached_stage_id
        context.simulation_id = self.simulation_id

        for callback in self.step_callbacks:
            callback(elapsed_time, context)

    def fetch_results(self):
        """Write results back to USD"""
        self._write_results_to_stage()

    def check_results(self):
        """Check if simulation is complete"""
        return True

    def get_simulation_timestamp(self):
        return self.timestamp

    def get_simulation_step_count(self):
        return self.step_count

    def subscribe_on_step_events(self, pre_step, order, callback):
        self.step_callbacks.append(callback)
        return len(self.step_callbacks)

    def unsubscribe_on_step_events(self, subscription_id):
        if 0 < subscription_id <= len(self.step_callbacks):
            self.step_callbacks.pop(subscription_id - 1)

    def is_capable_of_simulating(self, schema_names):
        """Check if this simulation can simulate given USD schemas"""
        supported_schemas = {
            "PhysicsRigidBodyAPI",
            "PhysicsCollisionAPI",
            "PhysicsMassAPI",
            "PhysicsJoint"
        }

        # Return list of booleans indicating capability for each schema
        is_capable = [schema in supported_schemas for schema in schema_names]
        return True, is_capable  # Return (success, capabilities)

    def _step_physics(self, dt):
        """Internal physics stepping"""
        pass

    def _write_results_to_stage(self):
        """Internal results writing"""
        pass


# Usage
physics = get_physics_interface()
physics_simulation = get_physics_simulation_interface()

engine = MyPhysicsEngine()

# Create simulation and wire up functions
simulation = Simulation()
simulation.simulation_fns.initialize = engine.initialize
simulation.simulation_fns.close = engine.close
simulation.simulation_fns.get_attached_stage = engine.get_attached_stage
simulation.simulation_fns.simulate = engine.simulate
simulation.simulation_fns.fetch_results = engine.fetch_results
simulation.simulation_fns.check_results = engine.check_results
simulation.simulation_fns.get_simulation_timestamp = engine.get_simulation_timestamp
simulation.simulation_fns.get_simulation_step_count = engine.get_simulation_step_count
simulation.simulation_fns.subscribe_physics_on_step_events = engine.subscribe_on_step_events
simulation.simulation_fns.unsubscribe_physics_on_step_events = engine.unsubscribe_on_step_events
simulation.simulation_fns.is_capable_of_simulating = engine.is_capable_of_simulating

# Register simulation
simulation_id = physics.register_simulation(simulation, "MyEngine")
engine.simulation_id = simulation_id

Capability Checking - Using isCapableOfSimulating#

The isCapableOfSimulating function allows querying whether a registered simulation can simulate specific USD schema types or schema APIs. This is useful for determining compatibility before attempting to simulate a scene.

C++ Example - Checking Simulation Capabilities#

#include <omni/physics/simulation/IPhysics.h>
#include <omni/physics/simulation/IPhysicsSimulation.h>

using namespace omni::physics;

// Acquire interfaces
IPhysics* physics = framework->acquireInterface<IPhysics>();
IPhysicsSimulation* simulation = framework->acquireInterface<IPhysicsSimulation>();

// Get a registered simulation ID
SimulationId simId = physics->getSimulationIds()[0];

// Check single schema capability
const char* schemaNames[] = { "PhysicsRigidBodyAPI" };
bool isCapable[1] = { false };

bool success = simulation->isCapableOfSimulating(simId, schemaNames, 1, isCapable);

if (success)
{
    if (isCapable[0])
    {
        carb::log("Simulation supports PhysicsRigidBodyAPI");
    }
    else
    {
        carb::log("Simulation does not support PhysicsRigidBodyAPI");
    }
}
else
{
    carb::log("Capability check not available for this simulation");
}

// Check multiple schemas at once
const char* multipleSchemas[] = {
    "PhysicsRigidBodyAPI",
    "PhysicsCollisionAPI",
    "PhysicsMassAPI",
    "PhysicsJoint"
};
bool multipleCapabilities[4] = { false, false, false, false };

success = simulation->isCapableOfSimulating(
    simId, multipleSchemas, 4, multipleCapabilities);

if (success)
{
    for (size_t i = 0; i < 4; ++i)
    {
        carb::log("%s: %s",
            multipleSchemas[i],
            multipleCapabilities[i] ? "supported" : "not supported");
    }
}

Python Example - Checking Simulation Capabilities#

from omni.physics.core import (
    get_physics_interface,
    get_physics_simulation_interface
)

# Get interfaces
physics = get_physics_interface()
simulation = get_physics_simulation_interface()

# Get a registered simulation ID
simulation_ids = physics.get_simulation_ids()
if simulation_ids:
    sim_id = simulation_ids[0]

    # Check single schema capability
    schema_names = ["PhysicsRigidBodyAPI"]
    success, capabilities = simulation.is_capable_of_simulating(sim_id, schema_names)

    if success:
        if capabilities[0]:
            print("Simulation supports PhysicsRigidBodyAPI")
        else:
            print("Simulation does not support PhysicsRigidBodyAPI")
    else:
        print("Capability check not available for this simulation")

    # Check multiple schemas at once
    schema_names = [
        "PhysicsRigidBodyAPI",
        "PhysicsCollisionAPI",
        "PhysicsMassAPI",
        "PhysicsJoint"
    ]
    success, capabilities = simulation.is_capable_of_simulating(sim_id, schema_names)

    if success:
        for schema, is_capable in zip(schema_names, capabilities):
            status = "supported" if is_capable else "not supported"
            print(f"{schema}: {status}")

    # Practical example: Check stage compatibility
    def check_stage_compatibility(stage, simulation_id):
        """Check if a simulation can handle all schemas in a stage"""
        from pxr import Usd

        # Collect all unique schema types from stage
        schema_names = set()
        for prim in stage.Traverse():
            if prim.HasAPI("PhysicsRigidBodyAPI"):
                schema_names.add("PhysicsRigidBodyAPI")
            if prim.HasAPI("PhysicsCollisionAPI"):
                schema_names.add("PhysicsCollisionAPI")
            if prim.HasAPI("PhysicsMassAPI"):
                schema_names.add("PhysicsMassAPI")
            # Add more schema checks as needed

        if not schema_names:
            return True, []  # No physics schemas in stage

        # Check capabilities
        success, capabilities = simulation.is_capable_of_simulating(
            simulation_id, list(schema_names))

        if not success:
            return False, []  # Capability check not available

        # Find unsupported schemas
        unsupported = [
            schema for schema, capable in zip(schema_names, capabilities)
            if not capable
        ]

        return len(unsupported) == 0, unsupported

Stage Update Functions#

The StageUpdateFns structure handles stage attachment and timeline events. This structure is defined in the IPhysicsStageUpdate interface and provides hooks for responding to stage lifecycle changes.

Key Functions#

  • onAttach - Called when a stage is attached

  • onDetach - Called when a stage is detached

  • onUpdate - Called on each frame update

  • onResume - Called when timeline playback resumes

  • onPause - Called when timeline is paused

  • onReset - Called when timeline is stopped/reset

  • handleRaycast - Handle raycast requests for picking

  • forceLoadPhysicsFromUSD - Force load physics objects from USD

  • releasePhysicsObjects - Release all physics objects

  • resetSimulation - Reset simulation state

  • startSimulation - Called when simulation starts, useful for storing the initial transformations

Simulation Events#

The IPhysicsStageUpdate interface provides a getSimulationEventStream() function that returns a Carb event stream which broadcasts simulation lifecycle events. These events allow applications to respond to timeline state changes.

Event Types (SimulationEvent enum):

  • eResumed / RESUMED - Sent when onResume() is called (timeline playback starts)

  • ePaused / PAUSED - Sent when onPause() is called (timeline is paused)

  • eStopped / STOPPED - Sent when onReset() is called (timeline is stopped/reset)

Event Flow:

  1. User presses Play → onResume() called → eResumed event sent

  2. User presses Pause → onPause() called → ePaused event sent

  3. User presses Play again → onResume() called → eResumed event sent

  4. User presses Stop → onReset() called → eStopped event sent

C++ Example - Implementing StageUpdateFns#

#include <omni/physics/simulation/simulator/StageUpdate.h>

using namespace omni::physics;

class MyStageUpdateHandler
{
public:
    MyStageUpdateHandler()
    {
        m_stageUpdateFns.onAttach = [this](long stageId) {
            m_stageId = stageId;
            // Prepare for stage - but don't load physics yet
        };

        m_stageUpdateFns.onDetach = [this]() {
            m_stageId = 0;
            releaseAll();
        };

        m_stageUpdateFns.onUpdate = [this](float currentTime, float elapsedSecs, bool enableUpdate) {
            if (enableUpdate)
            {
                // Perform physics update
                update(elapsedSecs);
            }
            // Update other subsystems even if physics is disabled
            updateVisuals();
        };

        m_stageUpdateFns.onResume = [this](float currentTime) {
            m_isPlaying = true;
            storeInitialState();  // Store state for reset
        };

        m_stageUpdateFns.onPause = [this]() {
            m_isPlaying = false;
        };

        m_stageUpdateFns.onReset = [this]() {
            m_isPlaying = false;
            restoreInitialState();  // Restore to pre-play state
        };

        m_stageUpdateFns.forceLoadPhysicsFromUSD = [this]() {
            // Create physics objects from the attached USD stage
            loadPhysicsFromStage(m_stageId);
        };

        m_stageUpdateFns.releasePhysicsObjects = [this]() {
            // Release all physics objects but keep stage attached
            releasePhysicsObjects();
        };

        m_stageUpdateFns.startSimulation = [this]() {
            // Called when simulation begins
            // Store initial transforms for reset functionality
            storeInitialTransforms();
        };

        m_stageUpdateFns.resetSimulation = [this]() {
            // Reset simulation state while keeping stage attached
            releasePhysicsObjects();
            loadPhysicsFromStage(m_stageId);
        };
    }

    StageUpdateFns getStageUpdateFns() const { return m_stageUpdateFns; }

private:
    StageUpdateFns m_stageUpdateFns;
    long m_stageId = 0;
    bool m_isPlaying = false;

    void releaseAll() { /* ... */ }
    void update(float dt) { /* ... */ }
    void updateVisuals() { /* ... */ }
    void storeInitialState() { /* ... */ }
    void restoreInitialState() { /* ... */ }
    void loadPhysicsFromStage(long stageId) { /* ... */ }
    void releasePhysicsObjects() { /* ... */ }
    void storeInitialTransforms() { /* ... */ }
};

Python Example - Implementing StageUpdateFns#

from omni.physics.core import Simulation


class MyStageUpdateHandler:
    """Handle stage lifecycle and timeline events"""

    def __init__(self):
        self.stage_id = 0
        self.is_playing = False
        self.initial_state = None

    def on_attach(self, stage_id):
        """Called when stage is attached"""
        self.stage_id = stage_id

    def on_detach(self):
        """Called when stage is detached"""
        self.stage_id = 0
        self._release_all()

    def on_update(self, current_time, elapsed_secs, enable_update):
        """Called each frame"""
        if enable_update:
            self._update_physics(elapsed_secs)
        self._update_visuals()

    def on_resume(self, current_time):
        """Called when timeline playback starts"""
        self.is_playing = True
        self._store_initial_state()

    def on_pause(self):
        """Called when timeline is paused"""
        self.is_playing = False

    def on_reset(self):
        """Called when timeline is stopped"""
        self.is_playing = False
        self._restore_initial_state()

    def force_load_physics_from_usd(self):
        """Force create physics objects from USD"""
        self._load_physics_from_stage()

    def release_physics_objects(self):
        """Release physics objects but keep stage"""
        pass

    def start_simulation(self):
        """Called when simulation starts"""
        self._store_initial_transforms()

    def reset_simulation(self):
        """Reset simulation state"""
        self.release_physics_objects()
        self._load_physics_from_stage()

    # Private methods
    def _release_all(self): pass
    def _update_physics(self, dt): pass
    def _update_visuals(self): pass
    def _store_initial_state(self): pass
    def _restore_initial_state(self): pass
    def _load_physics_from_stage(self): pass
    def _store_initial_transforms(self): pass


# Wire up to simulation
handler = MyStageUpdateHandler()

simulation = Simulation()
simulation.stage_update_fns.on_attach = handler.on_attach
simulation.stage_update_fns.on_detach = handler.on_detach
simulation.stage_update_fns.on_update = handler.on_update
simulation.stage_update_fns.on_resume = handler.on_resume
simulation.stage_update_fns.on_pause = handler.on_pause
simulation.stage_update_fns.on_reset = handler.on_reset
simulation.stage_update_fns.force_load_physics_from_usd = handler.force_load_physics_from_usd
simulation.stage_update_fns.release_physics_objects = handler.release_physics_objects
simulation.stage_update_fns.start_simulation = handler.start_simulation
simulation.stage_update_fns.reset_simulation = handler.reset_simulation

Using Simulation Event Stream#

The simulation event stream allows listening for timeline state changes (play, pause, stop) without directly implementing the stage update callbacks. This is useful for extensions that need to respond to simulation state changes.

C++ Example - Subscribing to Simulation Events#

#include <omni/physics/simulation/IPhysicsStageUpdate.h>
#include <carb/events/IEvents.h>

using namespace omni::physics;

// Acquire the physics stage update interface
IPhysicsStageUpdate* stageUpdate = framework->acquireInterface<IPhysicsStageUpdate>();

// Get the simulation event stream
carb::events::IEventStreamPtr eventStream = stageUpdate->getSimulationEventStream();

if (eventStream)
{
    // Create subscription to receive events
    auto subscription = carb::events::createSubscriptionToPop(
        eventStream.get(),
        [](carb::events::IEvent* event)
        {
            SimulationEvent simEvent = static_cast<SimulationEvent>(event->type);

            switch (simEvent)
            {
                case SimulationEvent::eResumed:
                    carb::log("Simulation resumed - timeline is playing");
                    // Start your simulation-dependent operations
                    break;

                case SimulationEvent::ePaused:
                    carb::log("Simulation paused - timeline paused");
                    // Pause your operations
                    break;

                case SimulationEvent::eStopped:
                    carb::log("Simulation stopped - timeline reset");
                    // Clean up and reset state
                    break;
            }
        }
    );

    // Events are delivered when the event stream is pumped
    // This is typically done in the update loop
    eventStream->pump();

    // Keep subscription alive for as long as events should be received
    // When subscription goes out of scope, it automatically unsubscribes
}

Example: Tracking Simulation State

class SimulationStateTracker
{
public:
    SimulationStateTracker(IPhysicsStageUpdate* stageUpdate)
    {
        m_eventStream = stageUpdate->getSimulationEventStream();

        if (m_eventStream)
        {
            m_subscription = carb::events::createSubscriptionToPop(
                m_eventStream.get(),
                [this](carb::events::IEvent* event)
                {
                    handleSimulationEvent(static_cast<SimulationEvent>(event->type));
                }
            );
        }
    }

    void update()
    {
        // Pump events in your update loop
        if (m_eventStream)
        {
            m_eventStream->pump();
        }
    }

    bool isPlaying() const { return m_isPlaying; }
    bool isPaused() const { return m_isPaused; }

private:
    void handleSimulationEvent(SimulationEvent event)
    {
        switch (event)
        {
            case SimulationEvent::eResumed:
                m_isPlaying = true;
                m_isPaused = false;
                carb::log("State: Playing");
                break;

            case SimulationEvent::ePaused:
                m_isPlaying = false;
                m_isPaused = true;
                carb::log("State: Paused");
                break;

            case SimulationEvent::eStopped:
                m_isPlaying = false;
                m_isPaused = false;
                carb::log("State: Stopped");
                break;
        }
    }

    carb::events::IEventStreamPtr m_eventStream;
    carb::events::SubscriptionPtr m_subscription;
    bool m_isPlaying = false;
    bool m_isPaused = false;
};

Python Example - Subscribing to Simulation Events#

from omni.physics.core import (
    get_physics_stage_update_interface,
    SimulationEvent
)

# Get the physics stage update interface
stage_update = get_physics_stage_update_interface()

# Get the simulation event stream
event_stream = stage_update.get_simulation_event_stream()

if event_stream:
    # Define event handler
    def on_simulation_event(event):
        if event.type == SimulationEvent.RESUMED:
            print("Simulation resumed - timeline is playing")
            # Start your simulation-dependent operations
        elif event.type == SimulationEvent.PAUSED:
            print("Simulation paused - timeline paused")
            # Pause your operations
        elif event.type == SimulationEvent.STOPPED:
            print("Simulation stopped - timeline reset")
            # Clean up and reset state

    # Subscribe to events
    subscription = event_stream.create_subscription_to_pop(on_simulation_event)

    # Events are delivered when the event stream is pumped
    # This is typically done automatically by the framework,
    # but manual pumping is also possible if needed
    event_stream.pump()

    # Keep subscription alive for as long as events should be received
    # Set to None to unsubscribe
    # subscription = None

Example: Extension with Simulation State Tracking

import omni.ext
from omni.physics.core import (
    get_physics_stage_update_interface,
    SimulationEvent
)


class MySimulationExtension(omni.ext.IExt):
    """Extension that responds to simulation events"""

    def on_startup(self, ext_id):
        self._is_playing = False
        self._is_paused = False
        self._subscription = None

        # Get event stream
        stage_update = get_physics_stage_update_interface()
        event_stream = stage_update.get_simulation_event_stream()

        if event_stream:
            # Subscribe to simulation events
            self._subscription = event_stream.create_subscription_to_pop(
                self._on_simulation_event
            )

    def on_shutdown(self):
        # Unsubscribe from events
        self._subscription = None

    def _on_simulation_event(self, event):
        """Handle simulation lifecycle events"""
        if event.type == SimulationEvent.RESUMED:
            self._is_playing = True
            self._is_paused = False
            print("Extension: Simulation started")
            self._start_monitoring()

        elif event.type == SimulationEvent.PAUSED:
            self._is_playing = False
            self._is_paused = True
            print("Extension: Simulation paused")
            self._pause_monitoring()

        elif event.type == SimulationEvent.STOPPED:
            self._is_playing = False
            self._is_paused = False
            print("Extension: Simulation stopped")
            self._stop_monitoring()

    def _start_monitoring(self):
        """Start simulation-dependent operations"""
        print("Starting data collection...")

    def _pause_monitoring(self):
        """Pause operations while simulation is paused"""
        print("Pausing data collection...")

    def _stop_monitoring(self):
        """Stop and reset when simulation stops"""
        print("Stopping and resetting data collection...")

Example: Coordinating Multiple Systems

class SimulationCoordinator:
    """Coordinates multiple systems based on simulation state"""

    def __init__(self):
        self.systems = []
        self._subscription = None

        # Subscribe to simulation events
        stage_update = get_physics_stage_update_interface()
        event_stream = stage_update.get_simulation_event_stream()

        if event_stream:
            self._subscription = event_stream.create_subscription_to_pop(
                self._on_simulation_event
            )

    def register_system(self, system):
        """Register a system to be notified of simulation events"""
        self.systems.append(system)

    def _on_simulation_event(self, event):
        """Broadcast simulation events to all registered systems"""
        if event.type == SimulationEvent.RESUMED:
            print("Coordinator: Notifying all systems - Play")
            for system in self.systems:
                system.on_play()

        elif event.type == SimulationEvent.PAUSED:
            print("Coordinator: Notifying all systems - Pause")
            for system in self.systems:
                system.on_pause()

        elif event.type == SimulationEvent.STOPPED:
            print("Coordinator: Notifying all systems - Stop")
            for system in self.systems:
                system.on_stop()

    def shutdown(self):
        self._subscription = None
        self.systems.clear()

Scene Query Functions#

The SceneQueryFns structure provides scene query functionality for raycasts, sweeps, and overlap tests. This structure is defined in the IPhysicsSceneQuery interface.

Note

Scene queries only work after simulation has begun, as collision data is not fully initialized until that point.

Key Function Groups#

  • Raycasts - raycastClosest, raycastAny, raycastAll

  • Sphere Sweeps - sweepSphereClosest, sweepSphereAny, sweepSphereAll

  • Box Sweeps - sweepBoxClosest, sweepBoxAny, sweepBoxAll

  • Shape Sweeps - sweepShapeClosest, sweepShapeAny, sweepShapeAll

  • Overlaps - overlapSphere, overlapSphereAny, overlapBox, overlapBoxAny, overlapShape, overlapShapeAny

For complete function signatures and detailed documentation, refer to the IPhysicsSceneQuery interface header.

C++ Example - Implementing SceneQueryFns#

#include <omni/physics/simulation/simulator/SceneQuery.h>

using namespace omni::physics;

class MySceneQueryProvider
{
public:
    MySceneQueryProvider()
    {
        m_sceneQueryFns.raycastClosest =
            [this](const carb::Float3& origin,
                   const carb::Float3& unitDir,
                   float distance,
                   RaycastHit& hit,
                   bool bothSides) -> bool
        {
            // Perform raycast in your physics engine
            if (performRaycast(origin, unitDir, distance, bothSides, hit))
            {
                return true;
            }
            return false;
        };

        m_sceneQueryFns.raycastAny =
            [this](const carb::Float3& origin,
                   const carb::Float3& unitDir,
                   float distance,
                   bool bothSides) -> bool
        {
            RaycastHit hit;
            return performRaycast(origin, unitDir, distance, bothSides, hit);
        };

        m_sceneQueryFns.raycastAll =
            [this](const carb::Float3& origin,
                   const carb::Float3& unitDir,
                   float distance,
                   RaycastHitReportFn reportFn,
                   bool bothSides)
        {
            std::vector<RaycastHit> hits;
            performRaycastAll(origin, unitDir, distance, bothSides, hits);

            for (const auto& hit : hits)
            {
                // Return false from callback to stop traversal
                if (!reportFn(hit))
                    break;
            }
        };

        m_sceneQueryFns.overlapSphere =
            [this](float radius,
                   const carb::Float3& pos,
                   OverlapHitReportFn reportFn) -> uint32_t
        {
            std::vector<OverlapHit> hits;
            performSphereOverlap(pos, radius, hits);

            uint32_t count = 0;
            for (const auto& hit : hits)
            {
                count++;
                if (!reportFn(hit))
                    break;
            }
            return count;
        };

        m_sceneQueryFns.overlapSphereAny =
            [this](float radius, const carb::Float3& pos) -> bool
        {
            return checkSphereOverlap(pos, radius);
        };

        // Implement other functions similarly...
    }

    SceneQueryFns getSceneQueryFns() const { return m_sceneQueryFns; }

private:
    SceneQueryFns m_sceneQueryFns;

    bool performRaycast(const carb::Float3& origin,
                       const carb::Float3& dir,
                       float distance,
                       bool bothSides,
                       RaycastHit& hit) { /* ... */ return false; }

    void performRaycastAll(const carb::Float3& origin,
                          const carb::Float3& dir,
                          float distance,
                          bool bothSides,
                          std::vector<RaycastHit>& hits) { /* ... */ }

    void performSphereOverlap(const carb::Float3& pos,
                             float radius,
                             std::vector<OverlapHit>& hits) { /* ... */ }

    bool checkSphereOverlap(const carb::Float3& pos, float radius) { /* ... */ return false; }
};

Python Example - Using Scene Queries#

import carb
from omni.physics.core import get_physics_scene_query_interface


scene_query = get_physics_scene_query_interface()

# Raycast closest
origin = carb.Float3(0.0, 10.0, 0.0)
direction = carb.Float3(0.0, -1.0, 0.0)
distance = 100.0

hit_found, hit = scene_query.raycast_closest(origin, direction, distance, both_sides=False)
if hit_found:
    print(f"Hit at position: {hit.position}")
    print(f"Hit distance: {hit.distance}")
    print(f"Hit normal: {hit.normal}")

# Raycast any (just check for hit)
has_hit = scene_query.raycast_any(origin, direction, distance, both_sides=False)

# Raycast all with callback
def on_hit(hit):
    print(f"Found hit at: {hit.position}")
    return True  # Continue traversal (return False to stop)

scene_query.raycast_all(origin, direction, distance, on_hit, both_sides=False)

# Overlap tests
sphere_center = carb.Float3(0.0, 0.0, 0.0)
sphere_radius = 5.0

has_overlap = scene_query.overlap_sphere_any(sphere_radius, sphere_center)

def on_overlap(overlap_hit):
    print(f"Overlapping with: {overlap_hit.collision}")
    return True  # Continue

count = scene_query.overlap_sphere(sphere_radius, sphere_center, on_overlap)
print(f"Total overlaps: {count}")

User Interaction Functions#

The InteractionFns structure handles user interactions such as picking, reset-on-stop behavior and debug data queries. This structure is defined in the IPhysicsInteraction interface.

Key Functions#

  • disableResetOnStop - Control whether simulation resets when stopped

  • isDisabledResetOnStop - Query reset-on-stop state

  • handleRaycast - Handle raycast for object picking

  • getPrimDebugData - Get simulation debug data for a prim

Debug Data Format#

The getPrimDebugData function returns a dictionary where the keys are the debug data item names and the values are themselves dictionaries with the following keys:

  • "type" - One of the DebugDataItemType enum values

  • "value" - The actual data (type depends on the type field, see DebugDataItemType for the expected value format)

  • "doc" - A string describing what this debug item represents

In C++, these are implemented as carb dictionaries, whereas in Python, they are implemented as Python native dictionaries.

DebugDataItemType is an enum that defines the types of data returned from the getPrimDebugData functions.

Debug data items are typed using the DebugDataItemType enum in C++, with the corresponding value format:

  • eFloat (0) - Single floating point value.

  • eVector (1) - 3D vector (for directions, velocities, etc., where magnitude is a quantity) as a carb::Float3/Double3 (x, y, z).

  • ePoint (2) - 3D point (for positions) as a carb::Float3/Double3 (x, y, z).

  • eQuaternion (3) - Quaternion rotation/orientation as a carb::Float4/Double4 (x, y, z, w).

  • eString (4) - String value.

  • eBool (5) - Boolean value.

  • eInt (6) - Integer value.

  • eUndefined (7) - Undefined or unsupported type.

In Python, debug data item types are available from omni.physics.core through the DebugDataItemType class.

from omni.physics.core import DebugDataItemType

# Available type constants (all are plain integers):
DebugDataItemType.FLOAT       # 0 - Single floating-point value
DebugDataItemType.VECTOR      # 1 - 3D vector (for directions, velocities, etc., where magnitude is a quantity) as a tuple of 3 floats (x, y, z)
DebugDataItemType.POINT       # 2 - 3D point (for positions) as a tuple of 3 floats (x, y, z)
DebugDataItemType.QUATERNION  # 3 - Quaternion rotation/orientation as a tuple of 4 floats (x, y, z, w)
DebugDataItemType.STRING      # 4 - String value
DebugDataItemType.BOOL        # 5 - Boolean value
DebugDataItemType.INT         # 6 - Integer value.
DebugDataItemType.UNDEFINED   # 7 - Undefined or unsupported type.

Example structure of a debug data dictionary:

{
    "Position": {
        "type": DebugDataItemType.POINT,
        "value": (0.0, 5.0, 0.0),
        "doc": "The position of the body in world space"
    },
    "Mass": {
        "type": DebugDataItemType.FLOAT,
        "value": 10.0,
        "doc": "The mass of the body in kilograms"
    },
    "Body type": {
        "type": DebugDataItemType.STRING,
        "value": "RigidDynamic",
        "doc": "The type of physics body"
    },
    "Is sleeping": {
        "type": DebugDataItemType.BOOL,
        "value": False,
        "doc": "Whether the body is currently sleeping"
    }
}

C++ Example - Implementing InteractionFns#

#include <omni/physics/simulation/simulator/Interaction.h>
#include <carb/dictionary/DictionaryUtils.h>

using namespace omni::physics;

class MyInteractionHandler
{
public:
    MyInteractionHandler()
    {
        m_interactionFns.disableResetOnStop = [this](bool disable) {
            m_resetOnStopDisabled = disable;
        };

        m_interactionFns.isDisabledResetOnStop = [this]() -> bool {
            return m_resetOnStopDisabled;
        };

        m_interactionFns.handleRaycast =
            [this](const float* origin, const float* direction, bool input)
        {
            if (origin && direction)
            {
                carb::Float3 orig = { origin[0], origin[1], origin[2] };
                carb::Float3 dir = { direction[0], direction[1], direction[2] };

                if (input)
                {
                    // Mouse down - start picking
                    startPicking(orig, dir);
                }
                else
                {
                    // Mouse up - end picking
                    endPicking();
                }
            }
        };

        m_interactionFns.getPrimDebugData = [this](const char* primPath) -> carb::dictionary::Item*
        {
            carb::dictionary::IDictionary* dict = carb::getCachedInterface<carb::dictionary::IDictionary>();
            carb::dictionary::Item* debugData = dict->createItem(nullptr, "",
                carb::dictionary::ItemType::eDictionary);

            // Get physics object for this prim (adjust this to your own implementation)
            auto* body = getPhysicsBody(primPath);
            if (!body)
                return debugData;

            // Add position (3D point) (adjust this to your own implementation)
            auto pos = body->getPosition();
            {
                carb::dictionary::Item* item = dict->makeDictionaryAtPath(debugData, "Position");
                dict->makeAtPath(item, "type", static_cast<int>(DebugDataItemType::ePoint));
                dict->makeAtPath(item, "doc", "The position of the body in world space");
                dict->makeAtPath(item, "value", carb::Double3{pos.x, pos.y, pos.z});
            }

            return debugData;
        };
    }

    InteractionFns getInteractionFns() const { return m_interactionFns; }

private:
    InteractionFns m_interactionFns;
    bool m_resetOnStopDisabled = false;

    void startPicking(const carb::Float3& origin, const carb::Float3& dir) { /* ... */ }
    void endPicking() { /* ... */ }
};

Python Example - Implementing InteractionFns#

import carb
from omni.physics.core import (
    Simulation,
    get_physics_interaction_interface,
    get_physics_interface,
)


class MyInteractionHandler:
    """Handle user interactions"""

    def __init__(self):
        self.reset_on_stop_disabled = False
        self.picked_body = None

    def disable_reset_on_stop(self, disable):
        self.reset_on_stop_disabled = disable

    def is_disabled_reset_on_stop(self):
        return self.reset_on_stop_disabled

    def handle_raycast(self, origin, direction, has_input):
        """Handle picking raycast"""
        if has_input:
            # Mouse down - start picking
            self._start_picking(origin, direction)
        else:
            # Mouse up - release
            self._end_picking()

    def _start_picking(self, origin, direction):
        # Perform raycast and grab nearest dynamic body
        pass

    def _end_picking(self):
        self.picked_body = None

    def get_prim_debug_data(self, prim_path: str) -> dict[str, dict[str, Any]]:
        return {
            "Position": {
                "type": DebugDataItemType.POINT,
                "value": (0.0, 5.0, 0.0),
                "doc": "The position of the body in world space"
                },
            }

# Wire up to simulation
handler = MyInteractionHandler()

simulation = Simulation()
simulation.interaction_fns.disable_reset_on_stop = handler.disable_reset_on_stop
simulation.interaction_fns.is_disabled_reset_on_stop = handler.is_disabled_reset_on_stop
simulation.interaction_fns.handle_raycast = handler.handle_raycast
simulation.interaction_fns.get_prim_debug_data = handler.get_prim_debug_data

# Usage through interface
physics = get_physics_interface()
simulation_id = physics.register_simulation(simulation, "InteractionDemo")

interaction = get_physics_interaction_interface()

# Control reset behavior
interaction.disable_reset_on_stop(True)  # Keep simulation state on stop

is_disabled = interaction.is_disabled_reset_on_stop(simulation_id)

# Manual raycast for picking
origin = carb.Float3(0.0, 10.0, 0.0)
direction = carb.Float3(0.0, -1.0, 0.0)
interaction.handle_raycast(origin, direction, True)  # Start picking
from omni.physics.core import (
    get_physics_interaction_interface,
    DebugDataItemType,
)


# Get the interaction interface
interaction = get_physics_interaction_interface()

# Query debug data for a prim
prim_path = "/World/Cube"
debug_data = interaction.get_prim_debug_data(prim_path)

if debug_data:
    # Iterate through all debug data items
    for key, item in debug_data.items():
        item_type = item['type']
        item_value = item['value']
        item_doc = item['doc']

        print(f"{key}: {item_value}")
        print(f"  Type: {item_type}")
        print(f"  Description: {item_doc}")

Performance Profiling Functions#

The BenchmarkFns structure provides performance profiling and statistics. This structure is defined in the IPhysicsBenchmarks interface.

Key Functions#

  • subscribeProfileStatsEvents - Subscribe to receive profiling statistics

  • unsubscribeProfileStatsEvents - Unsubscribe from profiling events

C++ Example - Implementing BenchmarkFns#

#include <omni/physics/simulation/simulator/Benchmark.h>

using namespace omni::physics;

class MyBenchmarkProvider
{
public:
    MyBenchmarkProvider()
    {
        m_benchmarkFns.subscribeProfileStatsEvents =
            [this](ProfileStatsNotificationFn callback) -> SubscriptionId
        {
            m_statsCallbacks.push_back(callback);
            return m_statsCallbacks.size();
        };

        m_benchmarkFns.unsubscribeProfileStatsEvents =
            [this](SubscriptionId id)
        {
            if (id > 0 && id <= m_statsCallbacks.size())
            {
                m_statsCallbacks.erase(m_statsCallbacks.begin() + id - 1);
            }
        };
    }

    // Call this after each simulation step to report stats
    void reportStats()
    {
        std::vector<PhysicsProfileStats> stats;

        PhysicsProfileStats simStats;
        simStats.zoneName = "Simulation";
        simStats.ms = m_simulationTime;
        stats.push_back(simStats);

        PhysicsProfileStats collStats;
        collStats.zoneName = "Collision Detection";
        collStats.ms = m_collisionTime;
        stats.push_back(collStats);

        // Notify all subscribers
        for (auto& callback : m_statsCallbacks)
        {
            callback(stats);
        }
    }

    BenchmarkFns getBenchmarkFns() const { return m_benchmarkFns; }

private:
    BenchmarkFns m_benchmarkFns;
    std::vector<ProfileStatsNotificationFn> m_statsCallbacks;
    float m_simulationTime = 0.0f;
    float m_collisionTime = 0.0f;
};

Python Example - Using Benchmark Interface#

from omni.physics.core import (
    Simulation,
    PhysicsProfileStats,
    get_physics_interface,
    get_physics_benchmarks_interface,
    k_invalid_subscription_id,
)


class MyBenchmarkProvider:
    """Provide profiling statistics"""

    def __init__(self):
        self.callbacks = []
        self.simulation_time = 0.0
        self.collision_time = 0.0

    def subscribe_profile_stats_events(self, callback):
        self.callbacks.append(callback)
        return len(self.callbacks)

    def unsubscribe_profile_stats_events(self, subscription_id):
        if 0 < subscription_id <= len(self.callbacks):
            self.callbacks.pop(subscription_id - 1)

    def report_stats(self):
        """Call after each simulation step"""
        stats = []

        sim_stat = PhysicsProfileStats()
        sim_stat.zone_name = "Simulation"
        sim_stat.ms = self.simulation_time
        stats.append(sim_stat)

        coll_stat = PhysicsProfileStats()
        coll_stat.zone_name = "Collision Detection"
        coll_stat.ms = self.collision_time
        stats.append(coll_stat)

        for callback in self.callbacks:
            callback(stats)


# Wire up to simulation
benchmark_provider = MyBenchmarkProvider()

simulation = Simulation()
simulation.benchmark_fns.subscribe_profile_stats_events = (
    benchmark_provider.subscribe_profile_stats_events
)
simulation.benchmark_fns.unsubscribe_profile_stats_events = (
    benchmark_provider.unsubscribe_profile_stats_events
)

# Register and use
physics = get_physics_interface()
simulation_id = physics.register_simulation(simulation, "BenchmarkDemo")

benchmarks = get_physics_benchmarks_interface()

def on_profile_stats(stats):
    for stat in stats:
        print(f"{stat.zone_name}: {stat.ms:.2f}ms")

subscription = benchmarks.subscribe_profile_stats_events(on_profile_stats)

# Later, unsubscribe
subscription = None  # or call unsubscribe explicitly

Complete Example - Full Physics Engine Integration#

Here’s a complete example showing how to integrate a custom physics engine:

#include <omni/physics/simulation/IPhysics.h>
#include <omni/physics/simulation/simulator/Simulator.h>

using namespace omni::physics;

class MyCompletePhysicsEngine
{
public:
    MyCompletePhysicsEngine(IPhysics* physics)
        : m_physics(physics)
    {
        setupSimulationFns();
        setupStageUpdateFns();
        setupSceneQueryFns();
        setupInteractionFns();
        setupBenchmarkFns();
    }

    SimulationId registerWithPhysics(const char* name)
    {
        Simulation sim;
        sim.simulationFns = m_simulationFns;
        sim.stageUpdateFns = m_stageUpdateFns;
        sim.sceneQueryFns = m_sceneQueryFns;
        sim.interactionFns = m_interactionFns;
        sim.benchmarkFns = m_benchmarkFns;

        m_simId = m_physics->registerSimulation(sim, name);
        return m_simId;
    }

    void unregister()
    {
        if (m_simId != kInvalidSimulationId)
        {
            m_physics->unregisterSimulation(m_simId);
            m_simId = kInvalidSimulationId;
        }
    }

private:
    void setupSimulationFns()
    {
        m_simulationFns.initialize = [this](long stageId) { return true; };
        m_simulationFns.close = [this]() {};
        m_simulationFns.getAttachedStage = [this]() { return m_stageId; };
        m_simulationFns.simulate = [this](float dt, float t) { step(dt); };
        m_simulationFns.fetchResults = [this]() { writeResults(); };
        m_simulationFns.checkResults = [this]() { return true; };
        // ... configure other functions
    }

    void setupStageUpdateFns()
    {
        m_stageUpdateFns.onAttach = [this](long stageId) { m_stageId = stageId; };
        m_stageUpdateFns.onDetach = [this]() { m_stageId = 0; };
        m_stageUpdateFns.onUpdate = [this](float t, float dt, bool enable) {};
        m_stageUpdateFns.onResume = [this](float t) {};
        m_stageUpdateFns.onPause = [this]() {};
        m_stageUpdateFns.onReset = [this]() {};
        // ... configure other functions
    }

    void setupSceneQueryFns()
    {
        m_sceneQueryFns.raycastClosest = [this](const carb::Float3& o,
            const carb::Float3& d, float dist, RaycastHit& hit, bool both) {
            return performRaycast(o, d, dist, hit);
        };
        // ... configure other functions
    }

    void setupInteractionFns()
    {
        m_interactionFns.disableResetOnStop = [this](bool d) { m_noReset = d; };
        m_interactionFns.isDisabledResetOnStop = [this]() { return m_noReset; };
        m_interactionFns.handleRaycast = [this](const float* o, const float* d, bool i) {};
    }

    void setupBenchmarkFns()
    {
        m_benchmarkFns.subscribeProfileStatsEvents =
            [this](ProfileStatsNotificationFn fn) { return SubscriptionId(1); };
        m_benchmarkFns.unsubscribeProfileStatsEvents =
            [this](SubscriptionId id) {};
    }

    void step(float dt) { /* Physics stepping */ }
    void writeResults() { /* Write back to USD */ }
    bool performRaycast(const carb::Float3& o, const carb::Float3& d,
                       float dist, RaycastHit& hit) { return false; }

    IPhysics* m_physics;
    SimulationId m_simId = kInvalidSimulationId;
    long m_stageId = 0;
    bool m_noReset = false;

    SimulationFns m_simulationFns;
    StageUpdateFns m_stageUpdateFns;
    SceneQueryFns m_sceneQueryFns;
    InteractionFns m_interactionFns;
    BenchmarkFns m_benchmarkFns;
};

Best Practices#

  1. Always unregister simulations when shutting down to avoid dangling references.

  2. Check for null function pointers - not all simulations need to implement all functions.

  3. Use appropriate activation states - deactivate simulations that shouldn’t receive updates.

  4. Thread safety - callbacks may be invoked from different threads; ensure thread-safe access to shared state.

  5. Performance - keep callback implementations lightweight; heavy processing should be done asynchronously.

  6. Scene queries - remember that scene queries only work after simulation has started.

  7. Contact callbacks - contact data is only valid for one simulation step; data must be copied if persistence is needed.