- name
- xgaida-x-nixi-animation
- description
- Скелетная анимация Guildmaster: риг из трансформов (точки вращения, кости, хват предмета), авторинг клипов через риг-API (Bend от покоя, Aim в мировой угол, Arc для направления дуги), профиль-замер рига, валидатор клипов, контактные листы и probe-разметка, миграция имён узлов. Зови на любую задачу про клипы, позы, повороты частей тела, оружие в руке, маски слоёв, аватар и всё под Prefabs/Bones и EditorTools/AnimationLab. НЕ применять к: боевому времени и hitstop (combat-sim), визуальным эффектам удара (gamefeel-vfx), «глаголам движения» на корне юнита (это код LitMotion, gamefeel-vfx), тех-доке об анимации (tech-scribe).
# Скелетная анимация — рабочий контур Guildmaster
Этот скилл — процедура, а не справка. Он существует потому, что работа с поворотами
уходила в костыли: углы клинка считались как «куда он должен смотреть в мире минус
вклад плеча и локтя», в голове и каждый раз заново — и три захода подряд атаки
выходили сломанными без единой строчки в консоли.
**Роль на этом слое:** механика внутри клипа — моя (тайминги, кривые, дуги, оверлаппинг,
чистка, проводка). Художественное решение — Макса: какие позы, какой характер, куда
смотрит поза, где стоит точка хвата. Я не двигаю позы и не меняю вид персонажа без его
слова; я меряю, показываю картинкой и предлагаю.
## Карта инструментов
Всё под `Assets/_Project/Scripts/EditorTools/AnimationLab/` (asmdef
`Guildmaster.AnimationLab.Editor`, editor-only).
| Что | Где |
|---|---|
| Профиль-замер рига: id сустава → путь, поза покоя, ось и длина кости, знак сгиба, лимит, калибровка предмета | `Rig/RigProfile.cs` (+ `RigProfileBuilder`) |
| Авторинг клипов: `Bend`, `Set`, `Aim`, `Sweep`, `Move`, `Write` | `Rig/RigWriter.cs` |
| Фильтр непрерывности для готовых кривых | `Rig/RigWriter.cs` → `RigEulerFilter` |
| Проверка клипов против рига | `Rig/RigValidator.cs` |
| Разметка рига картинкой (суставы, оси, хват, ноль предмета) | `Rig/RigProbe.cs` |
| Зона удара картинкой и числами: путь клинка, фазы, перекрытие щитом; умеет составную позу (`Composition`) и надстройки (`Layers`) | `Rig/RigSweep.cs` |
| Гизмо локомоции: путь подошв, линия земли, длина шага, доля опоры | `Rig/RigStride.cs` |
| Темп локомоции в префаб вида (ед. земли за секунду) | `Rig/LocomotionStrideMeter.cs` |
| Композиция слоёв: СТЕК масок в двух режимах (`Override` / `Additive`), как в Animator | `Rig/RigLayerBlend.cs` |
| Рисование поверх рендера (общее для пробы и зоны) | `Rig/RigCanvas.cs` |
| Переименование узлов с переносом всех путей | `Rig/RigMigrate.cs` |
| Контактный лист и onion skin из клипа | `AnimationLabRenderer.cs` |
| Пост-обработка кривых (оверлаппинг, тангенсы, чистка констант) | `AnimationLabProcessor.cs` |
Меню — `Alebardium/Animation/…`: пересборка профиля (610), валидация клипов (620).
Профиль рига: `Assets/_Project/Prefabs/Bones/BoneUnit_Standart_RigProfile.asset`.
Журнал решений и замеров — `docs/skeletal-animation-progress.md`, канон дизайна —
`docs/wiki/gdd/10-vision/character-animation.md`.
## Модель рига
- **Кость — это трансформ.** Никаких пакетов: связка `Transform` + `SpriteRenderer` +
`Animator` уже является скелеткой. Ничего не покупаем (см. `skeletal-animation-vector`).
- **Точки вращения названы явно:** `Rotation Point (Shoulder / Elbow / Grip / Hip / Knee /
Ankle)`. То, что в скобках, — логический id для API (плюс сторона: `elbow.R`, `knee.L`).
- **Ноль имеет смысл только там, где его калибровали.** У меча: при нуле хвата клинок
стоит под прямым углом к предплечью (замер 90.61°). У щита: при нуле верх щита
вертикален (мировые 90°). Калибровочный оффсет живёт **в спрайтовом дочернем узле**
(`Sword` = −89.39°, `Shield` = −24.76°), поэтому анимируемый узел остаётся чистым нулём.
- **Стойка живёт в анимируемых узлах** (плечо 10°, предплечье 26.61°, бедро 22.12°) —
решение Макса. Поэтому API работает **в дельтах от покоя**, а не в абсолютных углах.
- **Два слоя движения:** риг намеренно скучный, узнаваемость даёт код на корне (LitMotion).
Не вкладывать уникальность в клипы.
## Процедура: клип от постановки до сдачи
1. **Прочитать профиль**, а не префаб глазами. Если структура рига менялась — пересобрать
профиль (`Alebardium/Animation/Rebuild Rig Profile`). Замеряемые поля обновятся,
авторские (знак сгиба, лимиты) переживут пересборку.
2. **Писать клип через `RigWriter`**, а не через сырые кривые:
`Bend("elbow.R", 40)` — от покоя, «плюс всегда сгибается»;
`Aim("weapon", -140)` — мировой угол, цепочка вычитается сама;
`Sweep("weapon", -25, Arc.Ccw)` — длинная дуга, направление явно.
3. **Прочитать отчёт `Write`.** Он предупреждает о том, что не легло: цель, которую клип
не играет; сгиб за лимитом; засеянный ключ в нуле.
4. **Прогнать `RigValidator`** по папке клипов. Ошибка — чинить до показа; предупреждение
`wrapped-arc` — подтвердить, что длинная дуга задумана.
5. **Посмотреть глазами:** `AnimationLabRenderer.RenderContactSheet` с `InBetweens = 1..2`
(ровные интервалы врут — удар живёт в трёх кадрах) и `RigProbe` там, где вопрос про
геометрию, а не про позу.
6. **Для атаки — прогнать `RigSweep`** ПЕРЕД доводкой клипа, а не после. Он отвечает на то,
чего поза не показывает: где проходит клинок, сколько длится удар, где стоит контакт
относительно него и какую долю тела реально закрывает щит. Три правила приёмки атаки:
дуга широкая (размах — это линия, по которой потом рисуется слеш-трейл и свечение),
дуга не идёт сквозь собственные ноги и землю, контакт стоит **внутри** быстрой фазы и
на корпусной высоте цели (у этого рига полоса тела −0.07…0.19 по мировому Y).
Удар меньше ~0.12 с на дугу в 200°+ размажет трейл в кольцо — растягивать, а не сужать.
6b. **Для локомоции — прогнать `RigStride`**, у неё свои три вопроса, и поза не отвечает ни на
один. Длина шага — число, на которое показ делит скорость юнита (`_runUnitsPerSecond` /
`_sprintUnitsPerSecond` в `UnitView`, пишет `LocomotionStrideMeter`): короткий шаг заставляет
клип крутиться быстро, и юнит семенит. Подошва не должна уходить под линию земли. Опорная нога
должна СТОЯТЬ, пока тело проходит над ней (доля опоры ~40-55% на ногу; меньше — юнит парит).
Гизмо мерит **подошву спрайта, а не узел голеностопа**, и уровень земли берёт из позы покоя
каждой ноги отдельно: ноги этого рига разной длины (правая голень 0.105 против 0.117), и по
общей линии короткая читалась бы как вечно висящая в воздухе.
7. **Показать Максу ДВЕ картинки: кадры И ГИЗМО.** HARD, требование Макса от 29.07 — сдача
правки клипа без гизмо не принимается. Контактный лист показывает позы, но не показывает
ГЛАВНОГО: где на самом деле идёт клинок. Одна поза может выглядеть отлично и при этом
вести дугу сквозь ноги, ронять контакт мимо корпусной полосы или рисовать эпициклоиду.
Для локомоции гизмо — `RigStride` (путь подошв и земля), для удара — `RigSweep` (путь клинка)
вместо круга — на листе этого не видно, на гизмо видно мгновенно.
- **атака** — `RigSweep` обязателен (путь клинка, фазы, контакт, перекрытие щитом), и с 30.07 —
по СОСТАВНОЙ позе: атака едет слоем, база под ней живёт своей жизнью. Судить надо обе боевые
комбинации: удар с разбега поверх бега и обычный удар поверх стойки, обе — с гвардией;
- **локомоция и позы** — `RigSweep` по клипу тоже: он показывает, что несомое оружие не
пашет землю и не проходит сквозь тело;
- если клип трогали больше одного раза — гизмо после КАЖДОГО прогона, а не в конце.
Боевую приёмку в play-mode делает Макс.
## Форма удара — правила Макса (29.07), приняты после разбора гизмо
Это про геймфил, а не про технику. Нарушение видно на гизмо мгновенно.
- **Слеш — это два ключа.** Где дуга началась и где кончилась. Ключ ВНУТРИ дуги разбивает одно
ускорение на два, ключ ПОСЛЕ удара перезапускает уже остановившийся клинок — оба читаются как
лаг, а не как вес. Вес после удара живёт в паузе и в возврате в стойку.
- **Доп. кадры не добавлять и в рекавери.** Прямой запрет Макса: разная скорость на этапах
выглядит плохо. Возврат тоже два ключа; форма задаётся дугой и тангенсами, не промежуточной позой.
- **Дуга должна быть круглой.** Круг получается тогда и только тогда, когда **угол между клинком и
предплечьем постоянен**: рука с мечом работает как жёсткий рычаг, и кончик описывает окружность
вокруг плеча. Как только кисть докручивает поверх руки, два вращения с разной скоростью дают
эпициклоиду — на гизмо это «загогулина» с вмятиной у основания. Замер: разница клинок−рука плыла
с +11° до −49°, радиус кончика рос на 30% за удар; после фиксации разницы (11°→6°) разброс 3%.
- **Низкий финиш покупается корпусом, а не кистью.** Наклон опускает плечо, а вместе с ним весь круг.
Доворот кисти на те же градусы ломает форму. Цена честности: жёсткий рычаг отдаёт ~40° дуги.
- **Широкая дуга — цель, а не риск.** Размах — это линия, по которой потом рисуется слеш-трейл и
свечение (SAO-переход). Узкий удар нечем украшать.
- **Клинок, направленный в пол, — норма.** Для большого замаха снизу его специально опускают;
«пашет землю» само по себе не дефект, дефект — когда сквозь ноги проходит вся дуга.
- **Единый ритм у обычных атак, свой у разбега.** Сетка Макса: замах 0.25 → пауза до 0.333 →
удар до 0.583 → пауза до 0.667 → возврат. `AttackCharge` живёт отдельно: удар 0.30 с, паузы по
8 кадров, дуга 293°, таз проваливается сквозь удар.
## Локомоция — правила и числа (30.07)
- **Четыре позы на шаг, а не две:** contact → down → passing → up. Двухпозный цикл (contact + passing)
и есть причина семенящего шага: расти шагу негде, кроме угла бедра, а таз обязан проседать НА контакте,
где настоящий ещё падает. Таз внизу в `down`, вверху в `up`.
- **Голеностопы анимируются.** Без переката (носок вверх на контакте, вниз на отталкивании) стопа —
доска на шарнире колена, и это видно при любой длине шага.
- **Длина шага — это ЧИСЛО, на которое показ делит скорость** (`_runUnitsPerSecond` /
`_sprintUnitsPerSecond`). Короткий шаг вынуждает клип крутиться быстро, чтобы ноги не скользили, —
отсюда мельтешение. Замеры 30.07: ходьба шаг 0.80 мировых при цикле 0.7 с (темп 2.27 ед/с), разбег
0.93 при 0.5 с (3.70). Это ~3.2 и ~3.6 шага в секунду на скорости Защитника.
- **Своё число у каждого клипа локомоции.** Общее на бег и разбег гнало разбег в 1.8 раза быстрее земли.
- **Числа пишет `LocomotionStrideMeter`, мерит `RigStride`** — один владелец. Правишь размах в рецепте —
перегони замер, а не подкручивай поле руками.
## Слои: телеграф-поза и композиция (30.07)
- **Надстройка скрабится СВОИМ окном, а не проигрывается.** Глобальный `animator.speed` принадлежит
свингу и в замахе равен нулю: щит, поднимающийся «сам», в этот момент застыл бы.
- **Телеграф — это «встать и подождать», а не «успеть к кадру».** Поза обязана быть в финале за
0.1–0.2 с ДО события (Макс, 30.07): подъём кончается раньше, остаток подводки — стоп-кадр. Линейный
скраб на всё окно приводит позу к финалу ровно к событию, и жест читается как реакция, а не как
предупреждение.
- **Клип надстройки не возвращается в стойку.** Опускать руку — дело ВЕСА слоя; клип держит позу столько,
сколько его держат. Возврат внутри клипа уводил щит раньше, чем кончался барьер.
- **Композицию судить композицией.** Игра не играет один клип: щит встаёт поверх бега, свинга, разбега.
И контактный лист, и `RigSweep` берут стек через `RigLayerBlend` (`Composition` — «этот клип едет слоем
на движущемся теле», `Layers` — «а это лежит поверх»), кадрирование считается по составной позе.
Сэмплировать два клипа подряд НЕЛЬЗЯ — второй перезаписывает риг вместе с маской.
- **Маска щита — только рука со щитом**, поэтому база (ноги, торс) продолжает своё.
## Атака живёт СЛОЕМ, а не стейтом базы (30.07)
Переезд случился потому, что удар с разбега физически едет вперёд весь замах (сим снял рут, см.
`CanCloseIntoReach`): свинг, владеющий всем телом, вёз бы юнита на неподвижных ногах.
| Слой | Режим | Маска | Что держит |
|---|---|---|---|
| Base | — | всё тело | локомоция, стан, смерть — всё, что владеет НОГАМИ |
| `Action` | Override | `Mask_Action` (руки + торс + голова, 19 из 34 узлов) | свинг: атаки и будущие касты |
| `ActionHips` | Additive | `Mask_Hips` (только `Hips`) | просадку таза ДЕЛЬТОЙ поверх bob'а базы |
| `Block` | Override | `Mask_ShieldArm` | гвардию, и лежит ВЫШЕ удара |
- **Каст — не отдельный слой.** Атака и каст никогда не идут одновременно: `IsCastBusy` не пускает новый
замах, а занесённый замах каст доигрывает. Верх тела занимает ровно одно ДЕЙСТВИЕ, поэтому слой один.
Второй был бы копией, которая всю жизнь простаивает (решение Макса, 30.07).
- **Порядок решает спор масок.** `Mask_Action` и `Mask_ShieldArm` обе держат левую руку. `Block` выше —
и это измеримо: без гвардии перекрытие тела щитом на контакте 9%, с гвардией 51%.
- **Таз — Additive, и это снимает выбор.** Override заставил бы выбирать: маска без таза роняет вес удара
(он держится на просадке), маска с тазом роняет bob бега. Аддитив складывает. Замер на живом риге:
таз бега 0.038, с аддитивным слоем 0.001, дельта −0.037 — ровно дельта клипа; Override дал бы
абсолютные −0.022 и стёр бы bob.
- **Дельта считается от ПЕРВОГО КАДРА клипа**, а не от позы покоя рига — так её берёт Animator. У
`AttackCharge` нулевой кадр это поза бега, и отсчёт обязан идти от неё.
- **База выбирается по тому, чем юнит занят НОГАМИ:** локомоция, если сим его везёт, и тот же клип
действия, если он стоит. Решается на входе в свинг и до конца свинга не меняется. Так стоячий удар
сохраняет свой выпад, а едущий получает бегущие ноги.
- **Покадровым это не грозит:** слоёв у них нет, они деградируют на прежнее поведение — как уже
деградирует гвардия.
### Что теперь можно судить соло, а что нельзя (замер 30.07)
Композиция меняет не всё, и знать это полезно, чтобы не гонять лишние рендеры:
- **Дугу клинка можно смотреть и по одному клипу.** Рука целиком на слое, бег ей не мешает: путь кончика
4.369 соло против 4.416 в композиции, разворот те же 536°.
- **Позу, ноги и щит — только композицией.** Путь кончика ЩИТА за тот же удар: 0.253 соло против 0.435 в
композиции — почти вдвое. Он висит на левой руке, но качается вместе с тазом и корпусом.
## Гизмо предметов — что мерить (30.07)
- **Зона строится по ПЛОЩАДИ спрайта, а не по отрезку «рукоять-кончик».** Для клинка разница мала, для
щита принципиальна: он плоскость, и вопрос к нему — сколько он закрывает. Отрезок рисовал веер, который
не отвечал ни на что (поймал Макс).
- **Углы берутся через трансформ узла**, а не из мирового AABB рендерера: AABB даёт прямоугольник по осям
экрана и врёт ровно в наклоне, ради которого гизмо и рисуется.
- **В кадре обязан быть второй предмет** (контуром): блок судят, видя, где меч, удар — видя, где щит.
- **Перекрытие тела — число, а не впечатление.** Поза «щит поднят» с перекрытием 6-10% не блок;
после правки локтя (он РАЗГИБАЛСЯ, и прямая рука уводила щит от тела) стало 51%.
## Направление вращения — пять ловушек, все замерены
Это самый дорогой класс ошибок на этом слое: Макс поймал пять подряд. Перед любой работой с углами
прочитать таблицу целиком.
| Ловушка | Правило |
|---|---|
| Дуга в локальных углах | `Arc` задаётся для **мировой** дуги предмета, не для локального угла хвата: автор видит клинок, а не кисть. |
| Абсолютный расчёт | Целевой локальный угол считать **в дельтах** (сколько мира нужно минус сколько даёт цепочка). Эквивалент ±360k «совпадает» по позе, но кисть проворачивается целиком. |
| `DeltaAngle` для пути | Вклад цепочки — это **пройденный путь**, интеграл по кадрам. `DeltaAngle` складывает всё больше 180° в диапазон: −194° читается как +166°, и предмет докручивает фантомные 360°. |
| Короткий путь ≠ верный | У сустава сторона задаётся явно, когда путь близок к 180°: от 168° до −26° короткий путь +166° уводит руку назад над головой, нужный −194°. |
| Значение без анатомии | Шарнир (локоть, колено) гнётся в одну сторону. Числа из чужих клипов проверять на переразгиб, отсчёт **от позы покоя рига**, а не от нуля. |
Проверки, которые это ловят автоматически: `long-way-round`, `hinge-inverted`, `swing-stutter`,
самопроверка аимов в `RigWriter.Write` (порог 8° — меньше даёт шум от оверлаппинга).
## HARD-правила
- **Углы пишутся непрерывным рядом и не нормализуются.** Значения вне ±180 (357, −420) —
законны: именно они несут дугу. Нормализовать = схлопнуть замах.
- **Кривая пишется целиком.** Отсутствующую ось Unity дописывает нулём, а не оставляет как
было: `localEulerAnglesRaw` — все три оси, `m_LocalPosition` — все три.
- **Один тип rotation-биндингов на клип.** У нас везде `localEulerAnglesRaw` (режим Unity
«Euler Angles», держит полный диапазон). Кватернионные кривые срезают дугу по короткому
пути и больше 180° не умеют — не смешивать.
- **ОРУЖИЕ НЕ КРУТИТСЯ САМО — ни хватом, ни своим трансформом (HARD, 07.08.2026).** Узлы
`Weapon_R` / `Weapon_L` не анимируются вовсе: поворот клинка живёт на КИСТИ, и меч следует
за ней, потому что держат его жёстко. Держит тест `WeaponFollowsTheHandTests` — клип с
кривой на узле предмета роняет прогон.
<br>Прежняя формулировка звучала «предмет крутится своим хватом», и девять клипов честно
крутили хват — то есть заводили ВТОРОГО владельца ориентации оружия. Расплата пришла
оттуда, где не ждали: дуга за клинком строится от плеча к острию, «где остриё» стало
зависеть от двух независимых поворотов, и след лёг мимо меча. Требование Макса дословно:
«надо поставить клинок в одно положение с рукой вдоль одной прямой и ЗАПРЕТИТЬ крутить
оружие без руки... И чтобы на анимации любой это тоже НЕ сломалось. Всегда соблюдалось.»
<br>Следствие для постановки: поворачивая кисть, ты поворачиваешь и меч — это и есть
задуманное поведение, а не побочный эффект.
- **Куда смотрит оружие — говорит РИСУНОК, а не число.** Длина вылета объявляется на
`UnitHeldItem` (величина игровая, перерисовка арта не должна её двигать), направление
всегда берётся с меша рабочей части. Числом направление объявлялось до 07.08.2026 и
разошлось с картинкой на 33°.
- **Узлы рига переименовываются только через `RigMigrate`.** Иначе молча рвутся пути в
клипах, записи масок и аватар — ни одной ошибки в консоли.
- **`Cw` / `Ccw` заданы для нефлипнутого facing** (юнит смотрит вправо). Разворот идёт через
`scale.x = −1` на facing-root, а зеркальный трансформ обращает видимое направление
вращения. Авторить для неотражённой стороны.
- **Сохранять точечно** (`AssetDatabase.SaveAssetIfDirty`), не `SaveAssets` — он уносит
чужие несохранённые правки. Перед правкой префаба проверять открытую prefab-stage.
- **Правки Макса в окне Animation живут в памяти редактора.** На диске их нет, `git status` чист,
а рецепт при следующем прогоне их сотрёт. Порядок: проверить `EditorUtility.IsDirty` по клипам →
сохранить → закоммитить его правки ОТДЕЛЬНЫМ коммитом → перенести их в рецепты → только потом
править. Рецепт и клип с разными значениями — это два владельца одного факта, то есть дефект.
- **Не гнать рецепт до перекомпиляции.** После правки `.cs` сначала `refresh_unity`, потом вызов:
иначе выполнится старая версия метода и запишет старые числа (ловилось на маркере события).
- **Не менять позы и вид персонажа без слова Макса.** Мера, картинка, предложение — да;
тихая правка стойки — нет.
## Готчи, каждая куплена заходом
- **Кривая держит первый ключ назад во времени.** Одинокий ключ на 0.8 с сгибает сустав с
нулевого кадра. `RigWriter` засевает покой в t=0 сам; при ручной работе — помнить.
- **Цель нельзя решать в момент вызова.** Более поздний `Bend` выше по цепочке уводит
клинок (замер: 40° на локте сдвинули цель на 25°). Аимы решаются в конце, когда поза
готова. Засев покоя тоже идёт **до** решения аимов — иначе своя же ошибка на 40°.
- **Euler filter не восстанавливает намерение.** Как только 297.8 нормализовали в −62.2,
длинная дуга потеряна навсегда — её надо переавторить через `Arc`. Фильтр лечит только
разрывы (170 → −170 = 340° назад вместо 20° вперёд).
- **Хват — не конец кости.** Он точка крепления внутри кисти и на этом риге стоит в пивоте
предплечья: считать его следующим суставом дало «длину локтевой кости» 0.0024.
- **Маски и аватар молчат.** `AvatarMask` без валидного аватара на generic-риге не работает
без единого предупреждения; аватар описывает иерархию, поэтому переименование его
инвалидирует. `CopySerialized` переносит и имя — возвращать руками. С четырьмя слоями вместо
одного цена случайного переименования выросла: только через `RigMigrate`, без исключений.
- **Синхронизированный слой НЕ наследует клипы источника.** Он молча играет пустоту: первый замер
аддитивного таза дал дельту ровно 0.000 при внешне правильной раскладке. Каждому стейту источника
нужен явный `AnimatorControllerLayer.SetOverrideMotion`.
- **`RigLayerBlend.Sample` начинается с сэмпла базы**, поэтому доложить слои поверх уже собранной
композиции им нельзя — сотрёт её целиком и молча съест слой. Для этого есть `Fold`.
- **Переименование маски guid не рвёт.** `AssetDatabase.RenameAsset` сохраняет guid, ссылка из
контроллера остаётся живой (`Mask_Arms` → `Mask_Action` пережила это без правки контроллера).
- **`execute_code` может отработать, а ответ потерять.** Несколько правок контроллера отдали
«Timeout receiving Unity response», хотя применились. Перед повтором — ПРОВЕРИТЬ состояние, иначе
сделаешь работу дважды.
- **Одинаковый угол ≠ одинаковое число.** Одна поза лежит и как −16.34, и как 343.66:
сравнивать только через `Mathf.DeltaAngle`.
- **Ровный контактный лист врёт.** Удар занимает 3 кадра, ровная сетка шагает через него, а
длинный возврат получает большинство ячеек — сэмплить ключи плюс промежуточные.
- **`execute_code` исполняет тело метода:** `using` нельзя, писать полные имена;
`Object` неоднозначен — только `UnityEngine.Object`. `AssetDatabase.DeleteAsset` требует
`safety_checks: false`.
在 GitHub 查看