Builtin Library
README.md
SkinnedModel
Component that drives skinned model playback and animation graph integration.
Like Model, it carries the material field (AssetRef<material>) — material lives on the model that renders it, not as a standalone component. Set it at add time or assign entity(id).component.get("SkinnedModel").material = "..." later; :setMaterialProperty / :getMaterialProperty / :getMaterialPropertyNames operate on the assigned material (registry-wide).
Tint is synced. Every tint* field replicates, so :setTint is the colour every participant sees. A colour that changes every frame on many characters (a damage flash, a team colour, a heat map) belongs in an instance-data lane instead: wear @builtin::materials.instance_color (or instance_color_unlit) and write renderer.instanceData.set(id, 2, r, g, b, coverage), which replicates nothing. guides { path = "topics/rendering-instance-data" } has the lane layout.
Blend model
skinning chooses how the pass combines a vertex's joint influences.
"linear"(default) averages the joint matrices themselves. The average of two rotations is not a rotation, so a strongly twisted joint loses volume toward its axis — at a 180° twist a limb collapses onto the axis entirely."dualQuaternion"averages the joints' rigid motions as unit dual quaternions and renormalises, so the blend is a rotation at every weight and the cross-section is carried through the twist. Non-uniform joint scale rides along as a separately blended stretch. It costs a quaternion extraction per influence, so it is worth setting on characters whose wrists, shoulders or spine twist far, and leaving off elsewhere.
entity.find("hero").component.get("SkinnedModel").skinning = "dualQuaternion"
A rig at rest renders identically under either model.
Render self-check
A few frames after its mesh attaches, a SkinnedModel checks that the mesh
names something the engine can draw (on the GPU, or held off the device until
this display puts it back) and that the pose it is drawn in has not collapsed it: the
posed vertices the skinning pass wrote are measured against the mesh's own
bind-pose extent, so a 1 cm part of a prop is judged by its own size. A mesh
that fails is swapped for the builtin ERROR marker, drawn un-skinned with the
error material, and the reason is logged. Setting model again takes the
marker down and checks the new mesh.
renderCheck() answers the verdict: { state = "pending" | "passed" | "failed" | "skipped", reason }.
local v = entity.find("prop_part").component.get("SkinnedModel"):renderCheck()
if v.state == "failed" then print(v.reason) end