Materials¶
A material is the authored answer to "what does this surface look like?" In LunCoSim it is also the answer to "how does this surface behave physically?" Both answers use standard USD material binding, with different purposes.
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. If the network is invalid, the
material is rejected visibly; Rust does not substitute StandardMaterial or a
guessed shader.
The material network¶
A minimal authored material:
def Material "HullPaint"
{
token outputs:surface.connect = </Looks/HullPaint/Surface.outputs:surface>
def Shader "Surface"
{
uniform token info:implementationSource = "sourceAsset"
uniform asset info:wesl:sourceAsset = @lunco://shaders/mli_foil.wesl@
color3f inputs:base_color = (0.72, 0.74, 0.78)
float inputs:roughness = 0.35
float inputs:metallic = 0.1
token outputs:surface
}
}
Bind it to a prim:
def Mesh "Hull" (
prepend apiSchemas = ["MaterialBindingAPI"]
)
{
rel material:binding = </Looks/HullPaint>
}
The pieces:
| 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 an inline implementation |
info:wesl:sourceAsset |
The WESL fragment stage |
info:wesl:vertexAsset |
Optional WESL vertex stage |
inputs:<name> |
Authored parameter or texture input |
material:binding |
The all-purpose visual binding |
material:binding:physics |
The physics-purpose binding |
Source-backed shaders¶
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 portable material can keep both entry points in one source asset and use that same asset for both 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@
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.
The render binder validates the requested stage after the asset loads. An
invalid contract rejects the owning material and publishes a structured
RuntimeDiagnostics error. An authored Rhai policy may observe that diagnostic
and explicitly author a replacement or stop the scenario; recovery is visible
and overrideable, not hidden in the renderer.
Parameters¶
A shader declares its own parameter block. Rust reflects it into a schema at
load time, so adding a parameter means editing a .wesl file, never editing
Rust:
//!@ui dust_density 0 1 "Dust density"
//!@default dust_density 0.35
//!@ui clump_scale 0 64 "Clump scale (/m)"
//!@default clump_scale 8
//!@ui tint color "Tint"
//!@default tint 0.6,0.6,0.62,1.0
struct Material {
dust_density: f32,
clump_scale: f32,
tint: vec4<f32>,
};
@group(2) @binding(0) var<uniform> material: Material;
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.
A name that is not in the schema is dropped at pack time with a warning. It is never silently mis-packed into a neighbouring field.
See Shader parameters for the annotation language and the uniform block contract.
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. A shader that does not declare a binding is unaffected by that layer
being set. The same slot set serves every shader.
Rasters used as material data must be produced by the correct asset pipeline.
A grayscale orthophoto is not intrinsic albedo; the albedo processor removes
the low-frequency illumination field before encoding the material colour. A
generic map output is analysis/display data and must not be bound directly as
inputs:albedo_map.
See Texture layers for the role list and the channel-packing rules.
Physics materials¶
Friction and restitution are not bare properties on a body. They are
UsdPhysicsMaterialAPI opinions on a Material prim, bound through the
physics-purpose relationship:
def Scope "PhysicsMaterials"
{
def Material "Regolith" (
prepend apiSchemas = ["PhysicsMaterialAPI"]
)
{
float physics:dynamicFriction = 1.0
float physics:staticFriction = 1.0
}
}
def Cube "Ground" (
prepend apiSchemas = ["PhysicsCollisionAPI"]
)
{
rel material:binding:physics = </World/PhysicsMaterials/Regolith>
}
Friction comes off the bound material, never off a bare physics:friction on
the body prim. That name is not defined by UsdPhysics, so no other
physics-aware consumer reads it.
Binding resolution — namespace inheritance and the purpose-to-all-purpose fallback — is shared with the renderer. One material can drive both look and friction when it carries both sets of opinions.
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>
}
}
The binding cache is shared across prim and purpose resolution in an immutable snapshot. 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. It is not a
generic place to put shader inputs.
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.
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 |
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.
Rules of thumb¶
- Use one
Materialper appearance, not one per prim. - Prefer
sourceAssetover inline shader text. - Declare every parameter in the WESL
Materialblock; do not rely on undeclared uniforms. - Bind physics friction through
material:binding:physics. - Keep display color for markers, not for material parameters.
- Treat a rejected material as an authoring error, not as a reason to add a fallback.
Continue with Addresses and assets, which explains where the referenced files live.