"Why SetBoneTransform / SetIk silently do nothing"
▸ SYMPTOM
- You call
SetBoneTransform/TryGetBoneTransformand nothing useful happens, or worse, tails wind through the floor / torso spazzes. SetIk/ClearIkcompile and run but hands/feet never reach the target.- No exception; silent failure or intermittent corruption.
▸ CAUSE
Two different APIs, two different traps:
SetIk is AnimGraph-gated
SkinnedModelRenderer.SetIk does nothing on a direct-sequence rig. Engine XML docstring (Sandbox.Engine.xml):
Sets an IK parameter. This sets 3 variables that should be set in the ANIMGRAPH:
ik.{name}.enabled/position/rotation.
Working hand/foot IK needs a .vanmgrph with those params wired. If you play sequences directly (Sequence.Name = ..., no AnimGraph), SetIk / ClearIk are inert.
SetBoneTransform is unsound on clip-animated bones
The API exists (TryGetBoneTransform[Local], SetBoneTransform(in Bone, localTx), verified against Sandbox.Engine.dll) but behaves inconsistently:
- Overrides persist on bones the active sequence does not key → reads return your own prior write → unbounded accumulation (e.g. tail through the floor).
- Overrides get stomped on bones the sequence does key → a delta-backout then corrupts the clean pose → torso spazz through shared skin weights.
Read-modify-write against live bone state cannot be made safe from component code on a sequence-driven rig.
Why, and the probe that settles it: SetBoneTransform is the ragdoll write path. Read through the engine source, SkinnedModelRenderer.SetBoneTransform routes SceneModel.SetBoneOverride → CSceneAnimatableObject::SetPhysicsBone, the same call a ragdoll drives every physics bone with. Override three or four bones of a skeleton whose remaining bones are still animation-driven and one skin is fed two incompatible pose sources; on a humanoid arm chain the mesh shatters into flat polygon shards, and no value tunes it out.
The decisive test is a passthrough probe: read each bone's post-animation transform and write it straight back unchanged. That is a no-op by construction, yet it was recorded shattering identically, which pointed at the write itself, not the value being written, as the fault. Run it before spending a session tuning a procedural pose.
Dated correction (engine 26.08.05). A later spike re-ran that exact case (a partial-bone write over a body still driven by a full AnimGraph) and the mesh stayed intact. So "because it shatters" is build-dependent and no longer a claim to lean on unverified: retest the passthrough probe on the build in front of you before citing shatter as the reason. The architectural guidance below is unchanged regardless: prefer
ModelPhysicsbecause it drives the whole physics skeleton and you never hand-write bones one at a time, not merely "because the alternative shatters."
Three related calls, distinguished:
| Call | Routes to | What it is |
|---|---|---|
SetBoneTransform | SceneModel.SetBoneOverride (persists) | object-relative override applied AFTER animation (takes a transform relative to the renderer's own object) |
SetBoneWorldTransform | SceneModel.SetBoneWorldTransform | world-space override; a silent no-op on a graph-driven rig when you write only a subset of bones |
TryGetBoneTransform | SceneModel.GetBoneWorldTransform | the FINAL transform, INCLUDING the override |
TryGetBoneTransformAnimation | SceneModel.GetWorldSpaceAnimationTransform | after animation, BEFORE overrides |
Pairing the first two produces the unbounded accumulation above: a write feeds its own read. Reading only through the third removes that feedback and makes every write absolute rather than a delta, and the mesh still shatters, so escaping the feedback loop does not make the API usable on a partially clip-driven skeleton. Clearing goes through SceneModel.ClearBoneOverrides, which is coarse: it drops every override on the model, not just yours.
Dated correction (engine 26.08.05). Two later spikes corrected the coordinate space and the graph-on behavior of these calls. First:
SetBoneTransformdoes not take world space. It takes a transform relative to the renderer's own object, and the engine composes the object's transform back on afterwards.SceneModel.SetBoneWorldTransformis the call that takes world. On a subject at the world origin with no rotation the two look identical, so this can survive a whole spike undetected; give the subject a rotation and anything drawn at a computed bone position sits a body's width from the joint it belongs to. Second, and worse: with the animation graph RUNNING, the two are not interchangeable. Writing a subset of bones (a hand's finger chain, not the whole body) throughSetBoneWorldTransformbecomes a silent no-op. The value stores,TryGetBoneTransformreads it back exact (gap0.0000, same frame), and it never reaches the skinned mesh, so a twenty-unit displacement photographs identically to the write never having happened. FeedSetBoneTransformthe object-relative transform instead (renderer.WorldTransform.ToLocal( world )) and the pose reaches the render under the same graph-on, partial-write conditions. Every numeric instrument agrees the write worked in both cases, so a test battery built on transform readbacks alone passes a pose feature that draws nothing. Only a rendered frame separates a call that draws from one that does not. Retest the exact graph on/off and full-or-partial-write condition in front of you before pickingSetBoneWorldTransformon a graph-driven rig.
There is a newer Procedural Bones path (GetBoneObject(i) → move the bone GameObject → ReadBonesFromGameObjects()), distinct from SetBoneTransform, but the bone must be flagged Procedural (model/AnimGraph authoring), and RMW against clip-keyed bones is still unsound.
▸ FIX
Verdict for a sequence-only rig: no runtime hand IK without authoring an AnimGraph.
When hands must grab a moving world target on such a rig:
- Proxy props welded to the target in world space: small meshes parented to the character root (not a visual child that carries squash/flip). Cartoon-acceptable, zero rig work.
- Whole-visual motion: rotate/offset the visual GameObject; bake follow-through into clips.
- Commit to AnimGraph: additive layers, bone masks, real IK params (
renderer.Set("param", value)/Parameters.Set/SetLookDirection/UseAnimGraph). Citizen'sCitizenAnimationHelperis the reference caller pattern.
For ragdoll crumple, use ragdoll-scripted-rig-npc (ModelPhysics), not per-bone C# posing.
For smooth clip switches only, use crossfade-without-animgraph. Do not reach for SetBoneTransform.
▸ WHY IT WORKS
Sequence playback owns the pose every frame for keyed bones; writing over that from gameplay code fights the animator. SetIk was never a free IK solver; it only feeds AnimGraph parameters. Proxy props sidestep the skeleton entirely: the grab is a separate scene object, so there is no bone write to corrupt or ignore.
- Dated correction (26.08.05): SetBoneTransform takes an object-relative transform, not world; SetBoneWorldTransform takes world. On a graph-driven rig, writing a subset of bones through SetBoneWorldTransform is a silent no-op that every readback reports as success. Added the SetBoneWorldTransform routing row and the correction.
- Dated correction (26.08.05): a later spike ran the exact partial-bone-write-over-full-animgraph case and the mesh stayed intact, so 'it shatters' is no longer a claim to lean on unverified. The reason to prefer ModelPhysics is unchanged; retest the shatter behaviour on your own build before citing it.
- Added the engine-source mechanism: SetBoneTransform routes to SetPhysicsBone (the ragdoll write path), so driving a subset of a partly clip-animated skeleton shatters the skin. Added the passthrough-probe test and the SetBoneTransform / TryGetBoneTransform / TryGetBoneTransformAnimation routing table.