reckfellENGINE

2D skeletons: bones, weights and IK

Deform one polygon, define a rest pose and run a two-bone target solver.

On this page

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.

Rest polygon — the striped lower arm

The same vertices and weights after rotating the second bone

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
C#
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

DataRelative to
Bone2D.RestThe bone parent's local system
Polygon2D.Skeleton = "../Skeleton"The Polygon2D itself
AddBone("Upper/Lower", ...) pathsThe assigned Skeleton2D
UVTexture 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
C#
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

SymptomCheck
Polygon stays rigidSkeleton assignment, Bone2D paths and nonzero weights; hierarchy alone is insufficient
Some vertices do not followWeight-array length, vertex order and bone paths
Distortion before movementLocal Rest transforms, UV and coordinate conventions
Poor elbow deformationTopology, internal vertices and weight distribution
Bone is unregisteredA direct Bone2D chain to Skeleton2D without an intermediate Entity2D
IK has no effectStack and modifier Enabled, correct paths and a target Entity2D in the tree
Rest is immediately overwrittenDisable IK or another pose writer before resetting
Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.