Скелет 2D: кости, веса и IK
Деформация одного полигона, исходная поза и рабочий решатель для двух костей.
Чем скелет отличается от набора частей
Skeleton2D объединяет цепочки Bone2D. У Polygon2D вершины получают веса костей; рендерер деформирует полигон по текущей позе относительно исходной. Простое добавление Skeleton2D к картинке не назначает веса и не создаёт деформацию.
Запустить деформируемую руку
Создайте отдельный уровень с Entity2D в Position (160,144). Прикрепите SkinArmDemo.cs, добавьте действие animation_pause на Space, соберите проект и запустите F6. Полосатая рука сгибается и возвращается. Текстура создаётся кодом; внешний рисунок не нужен.
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();
}
}В цепочке 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
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 или другую запись позы перед сбросом |