USD¶
OpenUSD is LunCoSim's scene and composition format. It describes what the world is made of: hierarchy, transforms, geometry, materials, physics schemas, and the typed connections between parts. Integration, continuous equations, and mission policy live in the runtime, Modelica, and Rhai.
The distinction matters because USD is often asked to do too much. A USD stage can carry a reference to a Modelica model, but it does not contain the model's equations. It can carry a rigid body and a joint, but it does not integrate them. It can carry a connection between two ports, but it does not decide what value should flow. Those jobs belong to the runtime, to Modelica, and to Rhai respectively.
What USD owns¶
| Fact | Authored as | Consumed by |
|---|---|---|
| Prim hierarchy and identity | def prims under a defaultPrim |
Projection, selection, authoring tools |
| Placement | xformOp:translate, xformOp:orient, xformOp:scale |
Rendering, physics, mounts |
| Geometry | Mesh, Cube, Sphere, Cylinder, Capsule, Plane, NurbsPatch |
Rendering and collision cooking |
| Appearance | UsdShade material networks and WESL sources |
Render binder |
| Physics | UsdPhysics bodies, colliders, masses, joints, drives |
Avian projection |
| Typed ports | inputs:<name>, outputs:<name>, connectors:<name> |
Co-simulation and telemetry |
| Connections | .connect on a consuming property |
Co-simulation wiring |
| Programs | LunCoProgramAPI prims |
Modelica, Rhai, Python runtimes |
| Composition | Sublayers, references, payloads, variants, inherits | Stage resolver |
| Scene policy | LunCoPolicy, LunCoEnvironment, LunCoUiSchemaAPI |
Authored runtime policy |
Units and orientation¶
A LunCoSim scene uses metres and a Y-up, right-handed world. A normal root stage declares that explicitly:
#usda 1.0
(
defaultPrim = "MyTwin"
upAxis = "Y"
metersPerUnit = 1
)
def Xform "MyTwin" (kind = "assembly")
{
}
The defaultPrim matters: it gives the stage a stable root for references and
for the runtime projection. A stage without one is still readable, but a
reference to it cannot omit the target prim path.
The engine converts a supported non-canonical stage once, at the importer. A
Z-up or centimetre stage is valid interchange content; it is not a valid
runtime frame. metersPerUnit, upAxis, and kilogramsPerUnit are read at the
stage boundary, and unsupported values reject projection instead of being
silently approximated. Authoring a non-SI document is allowed; admitting it as
a live world is not.
The three document layers¶
A running scene is not one editable file. The document system keeps three authored layers over one composed stage:
| Layer | Lifetime | Saved to | Purpose |
|---|---|---|---|
| base | Durable | The source .usda file |
The authored scene |
| runtime | Durable | .lunco/runtime/<scene> when enabled |
Edits that must survive but stay out of the source file |
| view | Disposable | Never | Derived presentation such as route ribbons |
The composed CanonicalStage resolves references, sublayers, variants, and
the three layers in strength order. It is the projection engine: an authored
operation replayed onto it fires the change sink, and the runtime reconciles
the ECS world from that notice. The stage is thread-affine and lives on the
main thread; the serializable layers are what save, journal, and network sync
touch.
This is why editing a large scene does not re-flatten it on every change. The typed operation is applied to the document, journaled with its inverse, and replayed onto the live stage. A full reload is a separate contract and does not invent an authored edit.
Compose, do not duplicate¶
USD composition is how large scenes stay maintainable. A scene can reference a reusable rover, place two instances with different transforms, and select a variant without copying its geometry:
#usda 1.0
(
defaultPrim = "PracticeYard"
upAxis = "Y"
metersPerUnit = 1
)
def Xform "PracticeYard"
{
def Xform "RoverA" (
prepend references = @lunco://vessels/rovers/skid_rover.usda@
)
{
double3 xformOp:translate = (0, 0, 0)
uniform token[] xformOpOrder = ["xformOp:translate"]
}
def Xform "RoverB" (
prepend references = @lunco://vessels/rovers/skid_rover.usda@
)
{
double3 xformOp:translate = (8, 0, 4)
uniform token[] xformOpOrder = ["xformOp:translate"]
}
}
lunco:// addresses the installed asset library. twin:// addresses a file in
the open Twin. Use the narrowest authority that owns the file; do not escape an
asset root with ...
Physics is authored as SPD schemas¶
LunCoSim uses standard USD physics schemas rather than custom body objects. A movable part generally has:
- a rigid body (
UsdPhysics.RigidBodyAPI) - one or more colliders (
UsdPhysics.CollisionAPIand a shape) - mass properties (
UsdPhysics.MassAPI) - a joint when it is attached to another body
A movable mounted part needs both a rigid body and a joint. Being a child in the hierarchy is not an attachment. The scene linter reports a nested body without a joint.
Programs are prims¶
A Modelica model or Rhai scenario is attached through the LunCoProgramAPI
schema. The schema applies to the physical prim itself when the behavior is
intrinsic, or to a child Scope when the behavior is separable. The example
uses a child Scope, so deleting it removes the behavior without deleting the
rover:
def Xform "Rover" (kind = "assembly")
{
def Scope "Controller" (
prepend apiSchemas = ["LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @twin://my-rover/models/Drive.mo@
uniform bool lunco:program:realtimeSafe = true
}
}
A shipped model uses @lunco://models/...@; a Twin-owned model uses
@twin://...@. The source path is an asset address, not a relative path from
the current working directory.
Connections are USD facts¶
A connection is authored on the prim that consumes the value:
def Xform "DescentLander" (
prepend apiSchemas = ["PhysicsRigidBodyAPI", "LunCoProgramAPI"]
)
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:sourceAsset = @lunco://models/Lander.mo@
uniform bool lunco:program:realtimeSafe = true
float inputs:external_throttle = 0.0
float outputs:throttle
def Scope "MainPropulsion"
{
float inputs:valve_opening.connect = </DescentLander.outputs:throttle>
}
}
The composed stage resolves references, payloads, variants, and the connection
graph. The runtime then derives a SimConnection from the authored wire. A
model output becomes externally connectable only when the component prim
declares the matching outputs:<name> property; a connection target alone does
not create a public port.
Reading this chapter¶
| Section | Question it answers |
|---|---|
| Composition | How do layers, references, payloads, and variants form one stage? |
| Physics | How do bodies, colliders, joints, and gravity become a live simulation? |
| Ports and connections | How does a value travel from one component to another? |
| Programs | How is a Modelica model or Rhai scenario attached to a prim? |
| Materials | How does a prim get a shader, and how are parameters authored? |
| Addresses and assets | What do lunco:// and twin:// mean, and where do bytes come from? |
| Authoring | How do the Editor and Build perspectives change a stage safely? |
The composed stage is inert. It becomes a live world only when the runtime projects it and the admitted programs start.