Physics Schemas#

Physics behavior in a USD scene is described by schemas: typed sets of attributes and relationships applied to prims. ovstage populates scenes authored with these schemas into an ovstage.Stage; ovphysx attaches that stage and simulates it. This page explains the schema layers ovphysx understands and how to make the PhysX-specific ones available, both to ovstage when it populates a scene and to a stock usd-core when you author or validate offline.

Schema Layers#

  • USD Physics Schema (UsdPhysics) — the standard OpenUSD schema for annotating assets with physics: PhysicsScene, PhysicsRigidBodyAPI, PhysicsCollisionAPI, PhysicsMassAPI, PhysicsMaterialAPI, the joint types, and articulation APIs. It ships with stock usd-core and has typed Python bindings, for example UsdPhysics.RigidBodyAPI.Apply(prim). Refer to the OpenUSD UsdPhysics reference.

  • PhysX Schema (Physx*, codeless) — extends UsdPhysics with PhysX-specific functionality and tuning: PhysxSceneAPI (solver, GPU settings), PhysxRigidBodyAPI (CCD, sleep, stabilization), PhysxCollisionAPI (contact and rest offsets), PhysxForceAPI, joint extensions such as PhysxJointAxisAPI (refer to Joints), deformable extensions (refer to Deformables), and more. ovphysx ships this schema as codeless artifacts (plugInfo.json plus generatedSchema.usda, no compiled library).

  • Omni Physics Deformable Schema (codeless) — extends UsdPhysics with deformable bodies (OmniPhysicsDeformableBodyAPI and friends). Refer to Deformables.

  • Newton Schema (newton:*, not shipped) — the Newton USD schema, a subset of whose attributes ovphysx reads as fallbacks for the PhysX spellings. Install and register it yourself; refer to Registering the Newton USD schema.

The complete attribute set for each schema — types, defaults, and allowed values — is authoritative in the schema definitions themselves and is rendered in the Omni Physics documentation and the OpenUSD UsdPhysics reference. This guide describes intent, not full attribute definitions, so the two do not drift.

How ovphysx Ships the PhysX Schemas#

ovphysx exposes the PhysX USD schemas (PhysxSchema and OmniUsdPhysicsDeformableSchema) as codeless artifacts: a root plugInfo.json (with Includes: ["*/resources/"]) plus, per module, a <Module>/resources/ directory holding a plugInfo.json (Type: resource) and a generatedSchema.usda, with no compiled library. They live under schemas/physx/ in the SDK and under ovphysx/schemas/physx/ in the wheel. Codeless schemas carry no typed helper class — there is no PhysxSchema.PhysxRigidBodyAPI binding. You apply them by identifier and author their attributes generically.

ovphysx never modifies the environment (PXR_PLUGINPATH_NAME or any other plugin-path variable) and leaves registration to the application; it verifies at attach_ovstage that the registration happened before the first population and refuses the attach otherwise. It tells the application where the schemas are:

  • C: ovphysx_get_codeless_schema_root(ovphysx_string_t* out_root) returns the schema root directory. The string is NUL-terminated, owned by ovphysx, and valid until the calling thread calls the function again. It returns OVPHYSX_API_INVALID_ARGUMENT for a NULL argument and OVPHYSX_API_ERROR (details in ovphysx_get_last_error()) when the schema tree is missing. It does not initialize ovphysx, load USD, acquire Carbonite, or modify the environment, and is safe to call before ovphysx_create_instance().

  • Python: ovphysx.codeless_schema_root() returns the same root as a pathlib.Path, and ovphysx.codeless_schema_paths() returns the per-module resources directories. Both are pure Python and never trigger native loading.

Because they are codeless, the core UsdPhysics typed APIs (rigid body, collider, mass, scene) work out of the box with stock usd-core, while Physx* attributes require registering the codeless schemas first.

Making Schemas Available#

The application owns the USD runtime(s) in its process and registers the codeless schemas with each of them. There are two registration paths, depending on which USD runtime is in play.

Registering with ovstage#

ovstage ingests USD scenes through its own internal namespaced OpenUSD runtime, and that runtime knows nothing about the PhysX schemas until the application registers them. Register them once per process, before the first population call (open_usd, apply_usd_changes, or an export), by passing the schema root to ovstage.population.register_usd_schemas():

import ovphysx
import ovstage

ovstage.population.register_usd_schemas([str(ovphysx.codeless_schema_root())])
stage = ovstage.Stage("scene")

In C, obtain the root with ovphysx_get_codeless_schema_root() and pass it to ovstage_population_register_usd_schemas():

#include <ovphysx/ovphysx.h>
#include <ovstage/ovstage.h>
#include <ovstage/ovstage_population.h>

static int register_physx_schemas(void)
{
    ovphysx_string_t root;
    if (ovphysx_get_codeless_schema_root(&root).status != OVPHYSX_API_SUCCESS)
    {
        return 0;
    }
    ovx_string_t path;
    path.ptr = root.ptr;
    path.length = root.length;
    return ovstage_population_register_usd_schemas(&path, 1) == OVSTAGE_OK;
}

The bundled samples do exactly this: the Python samples call register_usd_schemas() in their attach_scene helper right before creating the ovstage.Stage, and the C samples call ovphysx_sample_register_physx_schemas() from tests/c_samples/common/ovstage_sample.h.

The ordering is not advisory. USD assembles its schema registry once, on first read, and ignores plugins registered afterwards; a registration that arrives after ovstage’s first schema read registers cleanly but contributes nothing, and ovstage reports it as an error (OVSTAGE_ERROR_OP_FAILED, an ovstage.OvstageError in Python). A registration that arrives after some other USD consumer in the process read the schemas cannot be detected and fails silently. No re-registration can repair the registry for the rest of the process. Without the registration, population resolves only the properties authored in the file rather than each prim’s full schema-declared property set. Registration is process-scoped and irreversible; registering the same root twice is a no-op.

Registering the Newton USD schema#

ovphysx also reads a subset of the Newton USD schema (NewtonJointAPI, NewtonArticulationRootAPI, NewtonSceneAPI, …) as fallbacks for the PhysX spellings. newton:velocityLimit, for example, maps onto physxJoint:maxJointVelocity: an authored PhysX limit below FLT_MAX wins, then an authored Newton value, then the PhysX default. An explicitly unlimited PhysX value (inf or FLT_MAX) counts as no PhysX opinion and yields to an authored Newton limit; the per-attribute contract is in PhysxJointAPI. ovphysx does not ship this schema. It is a separate package that the application installs and registers, in the same call and under the same ordering rule as the PhysX schemas:

pip install newton-usd-schemas
import ovphysx
import ovstage

ovstage.population.register_usd_schemas(
    [str(ovphysx.codeless_schema_root()), str(ovphysx.newton_schema_root())]
)

The bundled Python samples register it this way; their project file declares newton-usd-schemas, so uv run resolves it, and a plain pip install ovphysx environment needs pip install newton-usd-schemas before running them. ovphysx.newton_schema_root() returns the directory of the installed newton-usd-schemas package without importing it (importing it requires pxr and registers the schema with that stock USD runtime instead), and raises FileNotFoundError with the install hint when the package is missing. Without the pip package, download the schema from its GitHub repository and pass the directory holding its plugInfo.json to register_usd_schemas() (in C, ovstage_population_register_usd_schemas()) yourself.

ovstage populates an attribute only when a registered schema defines it, so a scene that authors newton:* attributes without this registration loses them silently: a joint authoring only newton:velocityLimit simulates unlimited. PhysX.attach_ovstage() therefore checks, once per process and before the native attach, that the installed newton-usd-schemas package was registered before the first population and emits a RuntimeWarning otherwise. When the package is not installed at all it logs a warning on the ovphysx Python logger instead: nothing is wrong for a scene without newton:* attributes, and a suite that promotes warnings to errors keeps running. ovstage keys registration on the plugin family, so a complete copy of the Newton schema registered from any directory (a GitHub checkout) is recognized. The check probes only once USD has built its schema definitions; a stage authored procedurally through ovphysx.population and attached before any USD population is left alone, so the check never registers a schema on the application’s behalf. A registration ovstage cannot observe (another USD consumer in the process read the schema definitions first) is not detected; silence the check in that case with PhysXConfig(carbonite_overrides={"/ovphysx/schemas/warnMissingNewtonSchema": False}) (the string "false" is accepted too). The C API cannot locate the package and performs no such check.

Authoring with a stock usd-core#

For a standalone Python authoring or validation tool, install the optional stock USD package in that tool’s environment before running an example that imports pxr:

python -m pip install usd-core

usd-core is owned by the authoring tool. It is not an ovphysx package dependency or the simulator’s USD runtime, and ovphysx does not accept the authoring tool’s USD runtime directly. The public ingestion path is authored USD -> ovstage population into an ovstage.Stage -> ovphysx attachment and simulation.

PyPI currently provides neither a Linux aarch64 usd-core wheel nor a source distribution. On Linux aarch64, hand-author .usda, author on a supported host, or supply a compatible OpenUSD Python build.

The codeless PhysX USD schemas shipped in ovphysx can be registered with any USD runtime — including stock usd-core from PyPI — without starting the simulator. This is useful for offline authoring, validation, and non-interactive tooling:

import sys

import ovphysx

try:
    from pxr import Plug, Tf, Usd
except ImportError:
    # Stock usd-core has no wheel for some platforms (e.g. linux-aarch64). The
    # codeless schemas still ship in the wheel. Without a stock USD runtime to
    # register them into, skip rather than fail.
    print("usd-core is not available on this platform; skipping codeless schema demo.")
    sys.exit(0)


def main() -> int:
    print("ovphysx version:", ovphysx.__version__)

    # Discover the codeless PhysX USD schema packages bundled with ovphysx.
    # Each path is a resources/ directory holding a codeless plugInfo.json
    # (Type=resource) and generatedSchema.usda, with no compiled library.
    schema_paths = ovphysx.codeless_schema_paths()
    print("Codeless schema packages:")
    for path in schema_paths:
        print("  ", path)

    # Register them with the stock usd-core runtime. This must happen before
    # anything in the process opens a stage or queries USD's schema registry.
    # That registry is built once, on first access, and a late call fails
    # silently. If USD may already be initialised (any DCC host), preset
    # PXR_PLUGINPATH_NAME before the process launches instead. Refer to
    # docs/physics_schemas.md.
    registry = Plug.Registry()
    registered = []
    for path in schema_paths:
        registered.extend(registry.RegisterPlugins(str(path)))
    registered_names = sorted(plugin.name for plugin in registered)
    print("Registered USD plugins:", registered_names)
    assert "physxSchema" in registered_names, registered_names
    assert "omniUsdPhysicsDeformableSchema" in registered_names, registered_names

    # Codeless schemas carry no compiled C++/Python bindings, so apply API
    # schemas by their schema identifier and query with the generic USD API.
    stage = Usd.Stage.CreateInMemory()
    prim = stage.DefinePrim("/World/Box", "Cube")
    assert prim.ApplyAPI("PhysxRigidBodyAPI"), "failed to apply PhysxRigidBodyAPI"
    assert prim.ApplyAPI("PhysxCollisionAPI"), "failed to apply PhysxCollisionAPI"

    rigid_body_type = Tf.Type.FindByName("PhysxSchemaPhysxRigidBodyAPI")
    assert rigid_body_type != Tf.Type.Unknown, "PhysxRigidBodyAPI not in schema registry"
    assert prim.HasAPI(rigid_body_type), "prim is missing PhysxRigidBodyAPI after apply"

    print("Applied API schemas:", list(prim.GetAppliedSchemas()))
    print("Codeless PhysX schema registration succeeded.")
    return 0


if __name__ == "__main__":
    sys.exit(main())

RegisterPlugins() must run before the process touches USD, and fails silently if it does not. USD builds its schema registry lazily on first access and never rebuilds it. Plug.Registry().RegisterPlugins() populates the plugin registry — a different structure — and nothing propagates from there into an already-built schema registry. If anything in the process opened a stage or queried the schema registry first, registration cannot repair it, and no re-registration or explicit plugin.Load() recovers.

The failure gives no warning: the registered-plugin count is still correct, Tf.Type.FindByName("PhysxSchemaPhysxRigidBodyAPI") still resolves, and the plugin still reports isLoaded=True. Only ApplyAPI fails, with Tf.ErrorException: ApplyAPI: Cannot find a valid schema for the provided schema identifier 'PhysxRigidBodyAPI'. There is no supported way to detect the state beforehand.

The recipe above is therefore for processes you control from the first line — offline authoring, validation, and non-interactive tooling. If USD may already be initialised, use PXR_PLUGINPATH_NAME instead (below).

After registration, apply the PhysX APIs by identifier and set attributes with USD’s generic attribute API:

import ovphysx
from pxr import Plug, Sdf, Usd, UsdPhysics

Plug.Registry().RegisterPlugins(
    [str(path) for path in ovphysx.codeless_schema_paths()]
)
stage = Usd.Stage.CreateInMemory()
prim = stage.DefinePrim("/World/Box", "Cube")
UsdPhysics.RigidBodyAPI.Apply(prim)
UsdPhysics.CollisionAPI.Apply(prim)
if not prim.ApplyAPI("PhysxRigidBodyAPI"):
    raise RuntimeError("Failed to apply PhysxRigidBodyAPI")
attribute = prim.CreateAttribute(
    "physxRigidBody:disableGravity", Sdf.ValueTypeNames.Bool
)
if not attribute.Set(True):
    raise RuntimeError("Failed to set physxRigidBody:disableGravity")
if not stage.GetRootLayer().Export("physx_rigid_body.usda"):
    raise RuntimeError("Failed to export physx_rigid_body.usda")

This writes physx_rigid_body.usda, ready for ovstage to populate for ovphysx simulation.

codeless_schema_paths() returns the per-module resources directories, and codeless_schema_root() returns the directory that holds them. Both are pure-Python and never trigger native loading, so they are safe to use in an authoring-only process.

Host processes that already initialised USD#

Inside a DCC host — Blender, Maya, Houdini, or any application embedding usd-core — you cannot assume USD is untouched. A host is a USD application: one File > Import > USD or File > Export > USD, or any other addon that imports pxr, builds the schema registry before your code runs. The RegisterPlugins() recipe above is then already too late, for the rest of the session.

For these processes, point PXR_PLUGINPATH_NAME at the codeless resources directories before the host process launches. USD reads that variable while constructing the registry, which is the only moment early enough, and no RegisterPlugins() call is needed at all:

# Compute the paths in a launcher process, then start the host with them set.
import os
import subprocess

import ovphysx

env = dict(os.environ)
# Append rather than replace: the variable may already carry plugin roots that
# the host or another package needs.
entries = [str(path) for path in ovphysx.codeless_schema_paths()]
existing = env.get("PXR_PLUGINPATH_NAME")
if existing:
    entries.append(existing)
env["PXR_PLUGINPATH_NAME"] = os.pathsep.join(entries)
subprocess.run(["blender"], env=env)

or from a shell, likewise preserving any existing value:

SCHEMA_PATHS="$(python -c 'import os, ovphysx; print(os.pathsep.join(str(p) for p in ovphysx.codeless_schema_paths()))')"
export PXR_PLUGINPATH_NAME="$SCHEMA_PATHS${PXR_PLUGINPATH_NAME:+:$PXR_PLUGINPATH_NAME}"
blender

Setting PXR_PLUGINPATH_NAME from inside an already-running host has no effect — by then the registry exists.

PXR_PLUGINPATH_NAME is stock USD’s own plugin-path variable, read by the host’s USD runtime; ovphysx never sets it. It is not how the schemas reach ovstage: register them with ovstage through register_usd_schemas() as described in Registering with ovstage.

Authoring Routes#

The same physics content can be authored two ways; both produce a .usda/.usd file that ovstage can populate for ovphysx simulation.

  • Hand-authored .usda text. Apply schemas through the apiSchemas metadata list and set attributes directly. No Python or schema registration needed to write the file:

    #usda 1.0
    
    def Cube "box" (
        prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysxRigidBodyAPI", "PhysicsCollisionAPI"]
    )
    {
        bool physxRigidBody:disableGravity = 0
    }
    
  • Python with a USD runtime. Use typed UsdPhysics bindings for core schemas and the codeless ApplyAPI pattern above for Physx* schemas.

Common pitfall. PhysxSchema.PhysxRigidBodyAPI is unavailable with stock usd-core; there is no compiled PhysxSchema module. Register the codeless schemas, then apply prim.ApplyAPI("PhysxRigidBodyAPI") by identifier.

For a hands-on authoring walkthrough (scene, ground, rigid body, colliders), refer to the bundled ovphysx-usd-authoring skill and the per-topic pages under Simulation Setup.