USD materials¶
A material is the authored answer to "what does this surface look like?" The
renderer does not choose a shader by prim name or folder. A UsdShade network
selects a WESL source, the source declares its parameters, and the runtime
projects that into a ShaderLook.
The network¶
def Material "DustyMaterial"
{
token outputs:surface.connect = </World/Looks/DustyMaterial/Shader.outputs:surface>
def Shader "Shader"
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:wesl:sourceAsset = @twin://my-rover-twin/shaders/dusty_surface.wesl@
float inputs:dust_density = 0.35
float inputs:clump_scale = 8.0
color4f inputs:tint = (0.62, 0.62, 0.64, 1.0)
}
}
Bind it to a prim:
def Mesh "Hull" (
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
rel material:binding = </World/Looks/DustyMaterial>
}
| Element | Meaning |
|---|---|
Material |
The bindable material prim |
outputs:surface |
The material's surface terminal |
Shader |
A source-backed shader node |
info:implementationSource |
Selects a source asset rather than inline text |
info:wesl:sourceAsset |
The WESL fragment stage |
info:wesl:vertexAsset |
Optional WESL vertex stage |
info:wesl:interface |
Optional declared shader ABI |
inputs:<name> |
Authored parameter or texture input |
material:binding |
The all-purpose visual binding |
Source selection¶
info:implementationSource = "sourceAsset" is the standard UsdShade selector.
info:wesl:sourceAsset follows the standard source-type convention;
vertexAsset is a LunCo renderer extension because UsdShade has no second
stage selector.
A source used as sourceAsset must declare a callable @fragment entry point.
An optional vertexAsset must declare a callable @vertex entry point. One
module may declare both and is valid for either role.
A portable material can keep both entry points in one source asset and use that same asset for both projected stages. The terrain pair uses separate files:
uniform token info:implementationSource = "sourceAsset"
uniform asset info:wesl:sourceAsset = @lunco://shaders/terrain_layered.wesl@
uniform asset info:wesl:vertexAsset = @lunco://shaders/terrain_geomorph.wesl@
The render binder validates the requested stage after the asset loads. An
invalid contract rejects the owning material and publishes a structured
RuntimeDiagnostics error. Rust does not substitute StandardMaterial, a
guessed terrain shader, or another guessed source.
Interface declaration¶
A shader may declare an ABI identifier:
The USD Shader prim authors the same identifier:
This is validation metadata. It names a shader ABI, never a shader asset; the
asset path remains wholly selected by the composed UsdShade binding. The
identifier does not change GPU material identity.
Parameter inputs¶
A USD-backed parameter edit belongs to the bound Shader prim's
inputs:<name> attribute. The editor and SetObjectProperty share the
bound-Shader resolver: it follows the composed material:binding network, uses
the composed typeName without collapsing USD roles or array shape, validates
the USDA literal, and authors a typed UsdOp.
The input name must match the WESL field after the shared snake-case normalization. A name that is not in the reflected schema is dropped at pack time with a warning; it is never silently mis-packed into a neighbouring field.
Geometry primvars: are reserved for standard UsdGeom primvars such as
primvars:displayColor. They are not a generic shader-parameter namespace.
Texture inputs¶
Texture inputs are asset-typed inputs: properties:
asset inputs:albedo_map = @twin://terrain/apollo15/albedo.png@
asset inputs:normal_map = @twin://terrain/apollo15/normals.png@
asset inputs:surface_map = @twin://terrain/apollo15/surface.png@
The projection maps the authored role to the named TextureLayer the shader
declares. The authorable set is albedo_map, mineral_map, surface_map,
normal_map, detail_albedo_map, detail_normal_map, detail_surface_map,
and displacement_map.
height_map and shadow_cache are engine-filled and deliberately not
addressable from USD. Continuation and annotation roles are runtime-owned.
A connected input is skipped by the texture reader because it is fed by a producer node, not by an authored file.
Binding strength and purpose¶
material:binding is the all-purpose visual binding.
material:binding:physics is the physics-purpose binding. A purpose-specific
binding wins over the all-purpose binding for that purpose; the fallback lets a
single material serve both.
Bindings compose through references like any other relationship. An instance can override the material without editing the referenced asset:
def Xform "RoverB" (
prepend references = @lunco://vessels/rovers/skid_rover.usda@
)
{
over "Chassis" {
rel material:binding = </Looks/DustyPaint>
}
}
A resolved binding that targets a missing or non-material prim rejects admission visibly.
Display color¶
primvars:displayColor is a standard UsdGeom primvar, not a shader parameter
namespace. It is appropriate for simple unlit markers, debug geometry, and
assets that must render without a material network.
Route markers are a good example: their unvisited color is authored in
primvars:displayColor and primvars:displayOpacity, and the generic
route_follow policy changes the visited state through the transient view
layer.
Sharing and caching¶
Identical looks share one material and one bind group. The binder caches by the
look key, which quantises floats to 1e-4 so a rounding error cannot mint a
second material. Texture handles are part of the key.
Vary a parameter per instance and you get a material per instance. If instances must differ, bucket the value first. A per-tile terrain look is a deliberate exception because the tiles genuinely carry different rasters.
Failure behavior¶
| Failure | Result |
|---|---|
| Shader source missing | Material rejected, RuntimeDiagnostics error |
| No fragment entry point | Material rejected, stage contract error |
| Parameter name not in schema | Dropped with a warning at pack time |
| Binding targets a non-material prim | Admission rejected |
| Malformed authored literal | Typed edit rejected before authoring |
| Unsupported texture format | Asset load fails |
| Declared interface does not match | Continuation or lint error; the material is not silently accepted |
The renderer never repairs a malformed material by guessing. An authored Rhai policy can observe the diagnostic and choose a replacement; that choice is visible policy, not a renderer fallback.
Related sections¶
- Materials — the scene-authoring view
- Parameters — the reflected schema
- Texture layers — the named roles
- Terrain — the canonical layered material