the symptom, in your words

"Make a Sandbox.Decal follow an animated character by re-pinning it to a bone each frame"

✓ verified on 26.07.18
lane Writing gameplayposted

▸ SYMPTOM

You spawn a Sandbox.Decal at an impact point on a character — a paint splat, a blood mark, a scorch — and it looks right for exactly one frame. The moment the character moves, the mark stays behind, projecting onto empty air where the body used to be. Hits on players "leave no splat" because the splat is stuck in world space and the body has walked out of its projection box.

▸ CAUSE

A Sandbox.Decal projects a box that is fixed in world space. Each frame the component rewrites its scene-object transform from Transform.World (engine Decal.UpdateToDelta, called from OnPreRender), so the box projects wherever the component's world transform is that frame. Spawn it at a world-space hit point and leave it there, and it keeps projecting at that stationary point while the animated mesh moves away.

▸ FIX

Parent the decal under the character and drive its world transform from a live bone every frame, so it rides the animated skinned mesh instead of sitting in world space.

  1. Parent a Decal GameObject under the character.

  2. At spawn, pick the nearest skeleton bone with SkinnedModelRenderer.TryGetBoneTransform, and store the impact in that bone's local frame:

    snippet
    var bt = renderer.TryGetBoneTransform( boneIndex ); // world transform of the bone
    localPos   = bt.PointToLocal( hitPos );
    localFrame = Rotation.LookAt( bt.NormalToLocal( hitNormal ) ); // face the surface normal

    Also set the decal's world transform right now (frame 0) so there's no one-frame flash at the character root.

  3. Every frame (OnUpdate, before render) re-pin off the current bone pose:

    snippet
    var bt = renderer.TryGetBoneTransform( boneIndex );
    GameObject.WorldPosition = bt.PointToWorld( localPos );
    GameObject.WorldRotation = bt.Rotation * localFrame;

Because the coordinates are stored bone-local, they're rig-invariant: a fresh SkinnedModelRenderer after an item/clothing swap works without recomputation — just read .Visual fresh each frame. Cap the number of decals per body and recycle the oldest.

This was confirmed live: the paint rides the citizen's torso, limbs and head through the whole run cycle, re-projecting onto the moving skinned mesh each frame rather than baking at spawn. Bones animate on proxies too, so it holds for every peer — the same reason bone-mounted gadgets track correctly on remote clients.

▸ WHY IT WORKS

The decal always projects at its component's world transform for the current frame. Storing the hit in a bone's local frame and re-pinning the world transform off that bone's live pose every frame means the projection box travels exactly with the animated surface. The mesh moves, the bone moves, the decal is re-pinned to the bone — so the paint and the body never separate.

Verified on engine 26.07.18: seen in a real project.
s&box moves fast; an undated fix is a liability. Spot a stale detail?
changelog
  • Published

Want to know when new guides or fixes drop? Join the community to help build this out. Report gotchas, flag outdated fixes, or just lurk.

Join the Discord