reckfellENGINE

Скелет 2D: кости, веса и IK

Деформация одного полигона, исходная поза и рабочий решатель для двух костей.

На этой странице

Чем скелет отличается от набора частей

Skeleton2D объединяет цепочки Bone2D. У Polygon2D вершины получают веса костей; рендерер деформирует полигон по текущей позе относительно исходной. Простое добавление Skeleton2D к картинке не назначает веса и не создаёт деформацию.

Полигон в исходной позе — полосатая рука внизу

Те же вершины и веса после поворота второй кости

Запустить деформируемую руку

Создайте отдельный уровень с Entity2D в Position (160,144). Прикрепите SkinArmDemo.cs, добавьте действие animation_pause на Space, соберите проект и запустите F6. Полосатая рука сгибается и возвращается. Текстура создаётся кодом; внешний рисунок не нужен.

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();
    }
}

В цепочке Skeleton → Upper → Lower обе кости имеют длину 80. Автоматический расчёт длины выключен, последняя кость не требует ещё одного дочернего маркера. Rest каждой кости задан её локальной трансформацией до анимации. Отдельная Entity между Skeleton и цепочкой Bone2D разрывает регистрацию костей; держите цепочку прямой.

Три разных набора координат

ДанныеОтносительно чего
Bone2D.RestЛокальной системы родителя кости
Polygon2D.Skeleton = "../Skeleton"Самого Polygon2D
Пути AddBone("Upper/Lower", ...)Назначенного Skeleton2D
UVПикселей текстуры, а не нормализованного диапазона 0…1

В примере рисунок имеет размер 64 × 16, а полигон — 160 × 40. UV растягивает рисунок на полигон. Не подменяйте Rest текущей анимированной позой: изменится основа привязки, и рисунок начнёт деформироваться иначе.

Веса и внутренние вершины

У каждого массива весов столько же элементов, сколько у Polygon. Индекс веса относится к индексу вершины. Здесь 7 вершин, включая внутреннюю последнюю, и по 7 весов для Upper и Lower. В области сустава влияние делится пополам. На этапе рисования учитываются четыре наибольших влияния на вершину с нормализацией; вершина с нулевыми весами не начнёт следовать нужной кости сама.

InternalVertexCount = 1 означает, что последняя вершина внутренняя. Polygons задаёт шесть треугольников вокруг неё. Внутренние вершины помогают управлять изгибом, но не заменяют топологию: после добавления или перестановки обновите UV, индексы треугольников и все массивы весов. Polygon остаётся исходной геометрией; видимый результат skinning не записывается обратно в этот массив.

В редакторе Polygon2D доступны режимы Polygons, UV и Bones. Сначала проверьте контур и разбиение, затем UV и веса. У Skeleton2D команда Overwrite Rest Pose записывает текущие позы как исходные; Reset to Rest Pose возвращает к уже сохранённым. Не используйте перезапись Rest как кнопку предпросмотра.

Пример IK во время игры

Добавьте в проект также TwoBoneIkDemo.cs. В отдельном уровне прикрепите его к Entity2D вместо SkinArmDemo; оба файла должны оставаться в C#-проекте. Запустите F6 и двигайте Target в Remote Inspector. Конец руки следует за белой целью в пределах досягаемости.

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.
    }
}

Класс наследует создание геометрии, останавливает клип и назначает стек модификаций Skeleton2D. Две Bone2D и Target задаются путями относительно скелета. Решатель обновляется стеком во время игры; редакторская IK-цепочка сама этого не делает. Автоматическая анимация и IK не должны одновременно считать себя последним владельцем одних углов.

SetIkEnabled(false) выключает стек и возвращает Rest. Для смены направления сгиба используйте SetFlipBendDirection(). Две кости длиной 80 не могут достать цель дальше 160 без отдельного механизма растяжения. Стек 2D-модификаций помечен экспериментальным в API: при обновлении движка отдельно проверяйте свои риги.

GetIndexInSkeleton() — индекс текущего скелета, не постоянный идентификатор объекта сохранения. При изменении цепочки он может измениться. Доступ к костям требует скелет в дереве; GetBoneCount() также актуализирует отложенную настройку.

Диагностика

СимптомПроверка
Полигон не гнётсяSkeleton, пути Bone2D и ненулевые веса; одной иерархии недостаточно
Часть вершин не следует за костьюДлина массива весов, порядок вершин и правильные пути
Рисунок перекручен ещё до движенияЛокальные Rest-трансформации, UV и совпадение систем координат
Плохой сгиб у локтяТопология, внутренние вершины и распределение весов
Кость не зарегистрированаПрямая цепочка Bone2D до Skeleton2D, без промежуточной Entity2D
IK не влияетEnabled у стека и модификации, правильные пути, целевая Entity2D в дереве
Возврат Rest сразу отменяетсяВыключите IK или другую запись позы перед сбросом
Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.