2D skeletons: bones, weights and IK
Deform one polygon, define a rest pose and run a two-bone target solver.
How a skeleton differs from separate parts
Skeleton2D groups Bone2D chains. Polygon2D vertices carry bone weights; the renderer deforms the polygon from its rest pose to the current pose. Adding a Skeleton2D beside an image does not assign weights or create deformation.
Run the deforming arm
Create a separate Entity2D level at Position (160,144). Attach SkinArmDemo.cs, add animation_pause on Space, build and run with F6. The striped arm bends and returns. Code creates the texture, so no external drawing is required.
SkinArmDemo.cs
using Reckfell;
public partial class SkinArmDemo : Entity2D
{
public Skeleton2D Skeleton { get; private set; } = null!;
public Bone2D Upper { get; private set; } = null!;
public Bone2D Lower { get; private set; } = null!;
public Polygon2D Skin { get; private set; } = null!;
public AnimationPlayer Player { get; private set; } = null!;
public override void _Ready()
{
Skeleton = new Skeleton2D { Name="Skeleton" };
Upper = new Bone2D { Name="Upper" };
Lower = new Bone2D { Name="Lower", Position=new Vector2(80,0) };
Upper.SetAutocalculateLengthAndAngle(false); Upper.SetLength(80);
Lower.SetAutocalculateLengthAndAngle(false); Lower.SetLength(80);
Upper.Rest=Upper.Transform; Lower.Rest=Lower.Transform;
Upper.AddChild(Lower); Skeleton.AddChild(Upper); AddChild(Skeleton);
Skin = new Polygon2D
{
Name="Skin", Skeleton=new EntityPath("../Skeleton"),
Polygon=[new(0,-20),new(80,-20),new(160,-20),new(160,20),new(80,20),new(0,20),new(80,0)],
InternalVertexCount=1,
UV=[new(0,0),new(32,0),new(64,0),new(64,16),new(32,16),new(0,16),new(32,8)],
Polygons=new Reckfell.Collections.Array
{
new int[]{0,1,6},new int[]{1,2,6},new int[]{2,3,6},
new int[]{3,4,6},new int[]{4,5,6},new int[]{5,0,6}
}
};
using var image=Image.CreateEmpty(64,16,false,Image.Format.Rgba8);
for(int x=0;x<64;x++)
for(int y=0;y<16;y++)
image.SetPixel(x,y,(x/8)%2==0 ? new Color(.15f,.35f,1) : Colors.White);
Skin.Texture=ImageTexture.CreateFromImage(image);
Skin.AddBone(new EntityPath("Upper"),new float[]{1,.5f,0,0,.5f,1,.5f});
Skin.AddBone(new EntityPath("Upper/Lower"),new float[]{0,.5f,1,1,.5f,0,.5f});
AddChild(Skin);
Player = new AnimationPlayer { Name="AnimationPlayer",RootEntity=new EntityPath("..") };
AddChild(Player);
var bend=new Animation { Length=2,LoopMode=Animation.LoopModeEnum.Linear };
int track=bend.AddTrack(Animation.TrackType.Value);
bend.TrackSetPath(track,new EntityPath("Skeleton/Upper/Lower:rotation"));
bend.TrackInsertKey(track,0,0f); bend.TrackInsertKey(track,1,.8f); bend.TrackInsertKey(track,2,0f);
var library=new AnimationLibrary(); library.AddAnimation("bend",bend);
Player.AddAnimationLibrary("",library); Player.Play("bend");
}
public void ApplyRestPose()
{
Player.Stop(keepState:true);
Upper.ApplyRest(); Lower.ApplyRest();
}
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("animation_pause")) return;
if (Player.IsPlaying()) Player.Pause(); else Player.Play("bend");
GetViewport().SetInputAsHandled();
}
}Both bones in Skeleton → Upper → Lower have length 80. Automatic length calculation is disabled, so the last bone needs no additional child marker. Each Rest is its local transform before animation. An intermediate entity between Skeleton and a Bone2D chain breaks bone registration; keep the chain direct.
Three coordinate conventions
| Data | Relative to |
|---|---|
Bone2D.Rest | The bone parent's local system |
Polygon2D.Skeleton = "../Skeleton" | The Polygon2D itself |
AddBone("Upper/Lower", ...) paths | The assigned Skeleton2D |
UV | Texture pixels, not normalized 0…1 coordinates |
The texture is 64 × 16 and the polygon 160 × 40. UV maps the drawing onto it. Do not replace Rest with the current animated pose; that changes the binding reference and future deformation.
Weights and internal vertices
Each weight array has the same number of elements as Polygon; weight indices match vertex indices. Here there are 7 vertices, including the final internal one, and 7 weights each for Upper and Lower. Joint vertices share influence equally. Rendering uses the four largest influences per vertex and normalizes them; a zero-weight vertex will not automatically follow the intended bone.
InternalVertexCount = 1 marks the final vertex as internal. Polygons defines six surrounding triangles. Internal vertices help shape a bend but do not replace topology; update UV, triangle indices and every weight array when adding or reordering vertices. Polygon remains the source geometry; visible skinning does not write the deformed result back into it.
The Polygon2D editor offers Polygons, UV and Bones modes. Check outline and subdivision, then UV and weights. Skeleton2D's Overwrite Rest Pose stores the current pose as rest; Reset to Rest Pose restores the saved rest pose. Do not use overwriting rest as a preview button.
Gameplay-time IK example
Also add TwoBoneIkDemo.cs. In a separate level, attach it to Entity2D instead of SkinArmDemo; keep both files in the C# project. Run with F6 and move Target through the Remote Inspector. The arm tip follows the white target within reach.
TwoBoneIkDemo.cs
using Reckfell;
public partial class TwoBoneIkDemo : SkinArmDemo
{
public Entity2D Target { get; private set; } = null!;
public SkeletonModificationStack2D Stack { get; private set; } = null!;
public override void _Ready()
{
base._Ready();
ApplyRestPose();
Target = new Entity2D { Name="Target",Position=new Vector2(100,60) };
Target.AddChild(new Polygon2D
{
Polygon=[new(-5,0),new(0,-5),new(5,0),new(0,5)],
Color=Colors.White
});
AddChild(Target);
_ = Skeleton.GetBoneCount();
var ik=new SkeletonModification2DTwoBoneIK { Enabled=true };
ik.SetJointOneBone2DEntity(new EntityPath("Upper"));
ik.SetJointTwoBone2DEntity(new EntityPath("Upper/Lower"));
ik.SetTargetEntity(Skeleton.GetPathTo(Target));
Stack=new SkeletonModificationStack2D { Enabled=true,Strength=1 };
Stack.AddModification(ik);
Skeleton.SetModificationStack(Stack);
}
public void SetIkEnabled(bool enabled)
{
Stack.Enabled=enabled;
if (!enabled) ApplyRestPose();
}
public override void _UnhandledInput(InputEvent @event)
{
// The IK stack owns this pose; do not restart the base clip here.
}
}The class inherits geometry creation, stops the clip and assigns a Skeleton2D modification stack. Both Bone2D targets and Target use paths relative to the skeleton. The stack runs the solver during gameplay; an editor IK chain alone does not. Animation and IK should not both act as the final writer of the same angles.
SetIkEnabled(false) disables the stack and restores Rest. Use SetFlipBendDirection() to change bend direction. Two length-80 bones cannot reach beyond 160 without a separate stretching mechanism. The 2D modification stack is marked experimental in the API; recheck your rigs when upgrading the engine.
GetIndexInSkeleton() is a current skeleton index, not a persistent save-object identifier. Changing the chain can change it. Bone access requires the skeleton in the tree; GetBoneCount() also updates deferred setup.
Diagnostics
| Symptom | Check |
|---|---|
| Polygon stays rigid | Skeleton assignment, Bone2D paths and nonzero weights; hierarchy alone is insufficient |
| Some vertices do not follow | Weight-array length, vertex order and bone paths |
| Distortion before movement | Local Rest transforms, UV and coordinate conventions |
| Poor elbow deformation | Topology, internal vertices and weight distribution |
| Bone is unregistered | A direct Bone2D chain to Skeleton2D without an intermediate Entity2D |
| IK has no effect | Stack and modifier Enabled, correct paths and a target Entity2D in the tree |
| Rest is immediately overwritten | Disable IK or another pose writer before resetting |