# R2D Re2DSprite — руководство разработчика и художника

Редакция 2026-10-08. PNG v2 + описание модели JSON v1 + анимации JSON v1.
Это документация реализованного прототипа, включая его ограничения.
Точный компактный контракт: [RE2DSPRITE_JSON.md](RE2DSPRITE_JSON).
API: [highlevel/re2dsprite.md](highlevel/re2dsprite).

Название технологии — **Re2DSprite**. Основной API `$.re2dSprite`, методы
`.re2dPose`, `.re2dMotion`, `.re2dAttach` и остальные `.re2d*`. Старые
`$.rotSprite`/`.rot*` остаются алиасами. Внутренние пути rotsprite, native
engine.rotSprite*, tag `<rotsprite>` и заголовок PNG R2D/ROT сохранены
для совместимости данных. Сцена `re2dsprite`, старое имя `rotsprite` также работает.

## 1. Что делает технология

Re2DSprite синтезирует обычный спрайт R2D из развёртки. Цвета находятся в
верхней части PNG; нижняя часть содержит координаты и принадлежность
каждого участка. При повороте C преобразует эти точки/непрерывные участки,
разрешает глубину внутри модели и записывает изображение в текстуру.
Текстура рисуется существующим 2D-батчем, с обычной камерой и слоями R2D.

PNG не является sprite sheet: в нём нет заранее нарисованных направлений
0/45/90° или кадров ходьбы. JSON не содержит треугольный OBJ-меш.
Карта XYZ задаёт псевдообъём, а иерархия костей двигает его части. Сетки
поверхностей действительно имеют пространственные координаты; это данные
генератора 2D-спрайта, а не новый универсальный 3D renderer.

Технология не угадывает форму по рисунку. Если у носа, предмета или одежды
не задана боковая/задняя поверхность, поворот не создаст её автоматически.
Сначала автор задаёт форму, затем рисует материал, соответствующий её UV.
Готовые виды персонажа полезны как художественный референс, но не являются
входными кадрами Re2DSprite.

## 2. Какие файлы нужны

| Файл | Назначение | Читается игрой |
|---|---|---|
| `object.png` | единый материал + карты поверхности | да |
| `object.character.json` | части, скелет, сокеты, пути и настройки | да |
| `object.animations.json` | клипы движения и мимики | да, если указан |
| `object.surface.json` | авторская сетка/контрольные точки | нет |
| `object.material.png` | рисунок до компиляции карт | нет |
| `build.json` | задания компилятору | нет |

Анимации можно поместить прямо в описание модели; внешний файл удобнее для
редактирования. `.surface.json` не обязателен в опубликованной игре, если
готовый PNG уже скомпилирован. Он нужен для дальнейшего изменения формы.
Не теряйте его вместе с оригинальным рисунком.

Пути в модели разрешаются относительно её JSON. Например, если модель
лежит в `art/cat/cat.character.json`, поле `atlas:"cat.png"` указывает на
`art/cat/cat.png`. При `.from(объект)` относительные пути идут от корня игры.
Используйте `/`, не пути конкретного компьютера. Для этого демо префиксы
`demos/...` разрешает существующий файловый слой R2D.

## 3. Быстрый запуск готовых настроек

Из корня R2D:

```sh
./build/russiano2d --game demos --scene re2dsprite
```

G / «Предмет»: без предмета → АК → пистолет → дробовик. Стрелки меняют yaw
и pitch, пробел включает автоповорот. L переключает стойку, ходьбу, бег;
C — костюм; H — волосы; V — эмоцию; E/M — глаза/рот; B/T — моргание/речь;
Q/W — независимый поворот головы; R перечитывает данные. Кисти можно тянуть
мышью. Движение пока проигрывается на месте, позицию на карте задаёт игра.

Готовый персонаж:

```js
const russi = $.re2dSprite.from('demos/rotsprite/russi.character.json', {id:'russi'})
    .at(600,360).size(512,512).re2dMotion('idle').re2dHotReload();
russi.re2dPose(35,8).re2dEmotion('happy');
russi.re2dVariant('costume','police');
russi.re2dVariant('hair','short');
```

Готовые модели технического предмета и животного:

```js
const prop = $.re2dSprite.from('demos/rotsprite/templates/prop.character.json')
    .at(300,300).size(256,256).re2dMotion('spin');
const animal = $.re2dSprite.from('demos/rotsprite/templates/animal.character.json')
    .at(500,300).size(256,256).re2dMotion('walk');
```

Это цветные разработческие заготовки, не готовые художественные ассеты.
Животное имеет собственные body/head/tail/paw кости и ID 90..96.
Человеческий скелет ему не навязывается.

## 4. Базовый атлас персонажа

Для новой модели на стандартных пропорциях используйте:

- `assets/rotsprite/rotsprite_v2_model_template.png` — базовый PNG с текущими
  ID коленей и предплечий;
- `demos/rotsprite/templates/russi.character.json` — совместимое описание;
- `demos/rotsprite/source/russi_maid.surface.json` — авторские координаты;
- `demos/rotsprite/russi.animations.json` — готовые циклы и позы.

```js
const base = $.re2dSprite.from('demos/rotsprite/templates/russi.character.json')
    .at(400,300).size(512,512).re2dPose(45,0);
```

Старый `rotsprite_v2_template.png` сохраняется для legacy `.create(PNG)`;
он не содержит новых ID предплечий. Не смешивайте его без перекомпиляции
с JSON-позой, рассчитанной на два звена руки. `.create(PNG)` продолжает
использовать встроенные legacy правила маскота. Для пользовательских сеток
и нового проекта выбирайте `.from(JSON)`.

Скопируйте шаблон и JSON в папку своего персонажа, исправьте пути и рисуйте
верхнюю материальную область. Чтобы использовать стандартную геометрию
с новым рисунком, запустите общий компилятор:

```sh
python3 tools/compile_rotsprite.py demos/rotsprite/source/russi_maid.surface.json --material art/my_character.material.png --output art/my_character.png --size 4096 --segments
```

В копии character.json поменяйте atlas на `my_character.png`, путь animations
на вашу копию. variants/equipment либо исправьте на свои пути, либо удалите.
Ссылка surface служит автору и не влияет на runtime.

Для регенерации базового шаблона:

```sh
python3 tools/build_rotsprite_assets.py demos/rotsprite/templates/build.json
```

## 5. Устройство единственного PNG

PNG MUST быть квадратным RGBA, размером 1024, 2048, 3072 или 4096.
Alpha MUST быть только 0 или 255, включая неиспользуемые области.
Все дальнейшие координаты приведены для канонического поля 1024×1024.
Для PNG 4096 умножайте их на 4, для 2048 на 2, для 3072 на 3.

Верхние 1024×768 — свободное поле материалов. Его сетка состоит из ячеек
4×4; одна ячейка задаёт один отсчёт поверхности. Поэтому карты имеют
256×192 отсчёта. PNG 4096 не создаёт в четыре раза больше геометрии:
текущая точность координат и рабочая сетка остаются теми же.

Нижние карты соответствуют верхнему полю независимо от раскладки деталей:

| Область x/y/w/h | Значение |
|---|---|
| 0 / 768 / 256 / 192 | ID части, канал R |
| 256 / 768 / 256 / 192 | Z, канал R |
| 512 / 768 / 256 / 192 | coverage: R=255 непрозрачный, R=128 прозрачная опора SUB, R=0 отсутствует |
| 768 / 768 / 256 / 192 | X в R, Y в G |
| 0 / 960 / 1024 / 64 | заголовок и резерв |

Ячейке материала `(4mx,4my)` соответствуют пиксели `(mx,768+my)` в ID,
`(256+mx,768+my)` в Z, `(512+mx,768+my)` в coverage и
`(768+mx,768+my)` в XY. На большом PNG масштабируются обе стороны связи.

Кодирование:

```text
X = (Rxy - 128) / 4       диапазон -32 .. 31.75
Y = (Gxy - 128) / 2       диапазон -64 .. 63.5
Z = (Rz  - 128) / 4       диапазон -32 .. 31.75
```

X вправо, Y вниз, Z к зрителю при yaw=0. Это базовое кодирование без SUB.
С маркером SUB `(83,85,66,255)` в `(3,960)` добавляются:
`X += (XY.B >> 4)/64`, `Y += (XY.B & 15)/32`, `Z += floor(depth.G/17)/64`.
Шаг SUB: X/Z=1/64, Y=1/32. ID.G хранит исходную группу материала.
С маркером BLD `(66,76,68,255)` в `(4,960)` ID.B хранит вторую часть,
depth.B — её вес 0..255. BLD читается только вместе с SUB.
Coverage=128 сохраняет прозрачную опорную точку. Эти каналы MUST NOT
редактироваться как декоративные цвета.
Эти пределы относятся к хранимой геометрии; костные преобразования могут
вывести точки дальше. Не рассчитывайте на их видимость за границами
выходного растера: поле результата фиксировано.

Заголовок в `(0,960)`, `(1,960)`, `(2,960)`:
`[82,50,68,255]`, `[82,79,84,255]`, `[2,4,4,255]`.
ID активной части MUST лежать в 1..254. 0 и 255 запрещены. Alpha активных служебных карт MUST быть 255. Без SUB coverage и alpha материала
MUST быть 255; с SUB coverage=128 и alpha материала=0 сохраняют прозрачную
опорную точку, coverage=255 обозначает непрозрачную. Полностью
пустая модель запрещена. Неописанный в character.json ID не рисуется.

Карты MUST NOT проходить сглаженное масштабирование, цветокоррекцию,
сжатие JPEG, перевод в художественную палитру или смешивание слоёв.
Самый безопасный путь — рисовать material.png, а карты пересобирать
компилятором. Не считайте чёрный цвет прозрачным: прозрачность задаёт alpha.

## 6. Раскладка материалов стандартного персонажа

Эта таблица относится только к готовому шаблону Руси-тян. У своей сетки
можно выбрать совершенно другую раскладку в верхних 1024×768.

| Канонический прямоугольник x/y/w/h | Материал |
|---|---|
| 0 / 0 / 640 / 192 | непрерывная развёртка головы |
| 640 / 0 / 192 / 192 | левое и правое ухо/банты |
| 832 / 0 / 96 / 192 | глаза, 4 строки по 48 |
| 928 / 0 / 48 / 192 | рот, 4 строки по 48 |
| 976 / 0 / 48 / 192 | брови, 4 строки по 48 |
| 0 / 208 / 832 / 188 | восемь лент волос |
| 832 / 208 / 192 / 188 | хвост |
| 0 / 408 / 320 / 168 | торс |
| 320 / 408 / 192 / 168 | обе руки |
| 512 / 408 / 192 / 168 | обе ноги |
| 704 / 408 / 320 / 168 | юбка/низ |
| 0 / 576 / 576 / 192 | дополнительные материалы/резерв |
| 576 / 640 / 448 / 128 | обувь |

Фронт головы в центре полосы, затылочный шов по краям. Рисуйте одну
непрерывную поверхность, а не последовательность фронта/профиля/затылка.
Для торса фронт фартука находится в центре UV-полосы; бок и спина продолжают
её. Ленты волос самостоятельны: прозрачные промежутки не должны становиться
новой поверхностью. У рисунка обязаны быть материалы скрываемых поворотом
сторон, иначе появятся дырки.

Для гладкого anime используйте крупные спокойные цветовые области, тонкие
локальные линии и минимум тёмной обводки. Толстая линия, попавшая в маленькую
ячейку, может стать широким пятном после проекции. Сглаживание режима anime
не удаляет контуры из вашего материала и не исправляет неправильную форму.

Чтобы поменять нос/подбородок, правьте XYZ в surface.json. Рисунок штриха
носа сам по себе не создаёт выступ в профиль. Текстуру лица и его карту
нужно править согласованно; отдельно проверить yaw=±90 и pitch=±45.

## 7. Как создать свою сетку без 3D-модели

Сетка здесь — набор параметризованных поверхностей, не список треугольников.
В surface.json есть `version:1`, массив patches и необязательные segments.
Каждая patch занимает уникальные ячейки верхнего материального поля.
В местах стыков XYZ соседних поверхностей SHOULD совпадать в пределах
точности кодирования. Общей системы сглаженных весов пока нет.

### Плоскость grid

Например, двусторонняя вывеска: создайте две grid-patch для передней и
задней поверхности и дополнительные полосы торцов при необходимости.
Каждой выделяется свой UV-прямоугольник, даже если она использует тот же цвет.

```json
{
  "version":1,
  "patches":[{
    "type":"grid", "id":80, "rect":[0,0,256,128], "color":[120,170,210],
    "points":[[[-12,-6,1],[12,-6,1]],[[-12,6,1],[12,6,1]]]
  }]
}
```

points — прямоугольная сетка не меньше 2×2, каждая точка `[x,y,z]`.
Для формы сложнее прямоугольника добавляйте строки/столбцы контрольных
точек. Между ними используется билинейная интерполяция. Параметры u/v
измеряются от начала rect; точки отсчётов берутся через 4 канонических
текселя, поэтому последний отсчёт не находится строго на u/v=1.

Одна плоскость будет выглядеть тонкой в профиль. Это правильное следствие
её формы. Чтобы получить толщину, задайте заднюю сторону и торцы,
а не растягивайте изображение плоскости в зависимости от yaw.

### Объём loft

Loft подходит для головы, конечности, ствола дерева, туловища животного,
бутылки и других продольных форм. Профиль по высоте задаётся sections:

```json
{
  "version":1,
  "patches":[{
    "type":"loft", "id":80, "rect":[0,0,256,192], "color":[106,148,178],
    "sections":[
      [0,7,7,7,0,-16,0,0],
      [0.03,14,14,14,0,-15,0,0],
      [0.95,14,14,14,0,14,0,0],
      [1,7,7,7,0,15,0,0]
    ]
  }]
}
```

Строка: `[v,rx,frontDepth,backDepth,cx,cy,cz,angleOffset]`.
v MUST строго возрастать; rx — радиус по X; front/back — глубина по Z;
cx/cy/cz — центр сечения. angleOffset — в радианах, исключение из обычных
градусов костей. Для каждого u вычисляется угол `(u-0.5)*2π+angleOffset`.
X=cx+rx*sin(angle); Z=cz+depth*cos(angle), где depth выбирается по стороне;
Y=cy. Параметры между сечениями интерполируются линейно.

Верх/низ loft автоматически отдельными крышками не закрываются. Для
заметных торцов добавьте grid-patch либо сведите радиус к вершине.
Начните с templates/prop.surface.json и меняйте радиусы, центры и материал.
Профиль головы требует разных радиусов/глубин на уровне лба, носа, губ
и подбородка, а не постоянного цилиндра.

### Явные samples

Для произвольной авторской формы можно записать каждую ячейку напрямую:

```json
{"version":1,"patches":[{"type":"samples","samples":[
  [0,0,80,-2,-3,1],[4,0,80,0,-3,1],[8,0,80,2,-3,1]
]}]}
```

Строка `[u,v,id,x,y,z]`; u/v — канонические координаты материала.
Так записаны нынешние поверхности Руси-тян. Это даёт полный контроль,
но для ручной работы с новым предметом grid/loft обычно удобнее.
rect и UV MUST быть целочисленными, кратными 4, без перекрытия ячеек;
поверхность MUST помещаться в поле материала, координаты — в диапазоны PNG.
Для grid/loft доступна matrix из 12 чисел, affine 3×4; она применяется после
расчёта точек. samples уже содержат окончательные координаты.

### Разделение на суставные части

```json
"segments":[
  {"id":7,"axis":"y","greaterThan":4,"assign":20,"blendWidth":6}
]
```

При `--segments` участки руки 7 ниже локтя Y=4 становятся предплечьем 20.
`blendWidth` — полная ширина плавного перехода вокруг порога; 0 — жёсткий
стык. Для maid локти используют 6 единиц, колени — 7. Вес задаёт smoothstep,
а жёсткие матрицы смешиваются двойными кватернионами. Сокеты не смешиваются.
Готовый персонаж использует 7/11 для плеч, 20/21 для предплечий;
8/12 для бёдер, 10/15 для голеней. Это данные автора, не обязательные
номера для других моделей. Добавляйте части и привязывайте их к костям
в character.json, иначе новые ID будут скрыты.

## 8. Рисунок и сборка PNG

Создайте material.png размером 1024/2048/3072/4096 и рисуйте в верхних
трёх четвертях. Для начала можно обойтись color каждой patch без материала.
При наличии material компилятор берёт его цвета, а color не заполняет рисунок.

Входной material MUST иметь бинарную alpha. Текущий общий компилятор
не делает художественный alpha-threshold автоматически для отдельного
входного PNG. Верхний левый пиксель используемой ячейки 4×4 MUST быть
непрозрачным: именно по нему компилятор активирует карту. Рисовать
надёжнее заполненными ячейками; тонкая линия внутри прозрачной ячейки
может не создать поверхность. Маскотно-специфический сборщик нормализует
полученные исходники отдельно.

```sh
python3 tools/compile_rotsprite.py art/sign.surface.json --output art/sign.png --size 1024
python3 tools/compile_rotsprite.py art/sign.surface.json --material art/sign.material.png --output art/sign.png --size 4096
```

Установка инструментария: Python 3 и Pillow (`python3 -m pip install Pillow`
в выбранном окружении). Нативный движок для компиляции PNG не требуется.
Общий компилятор отклоняет пересечения UV и координаты за диапазоном.

Для нескольких вариантов используйте build.json:

```json
{"version":1,"jobs":[{
  "surface":"sign.surface.json", "material":"sign.material.png",
  "output":"sign.png", "size":4096, "segments":false
}]}
```

```sh
python3 tools/build_rotsprite_assets.py art/build.json
```

Пути задания относительны build.json. Есть также materials для технической
упаковки исходных cutout: source, crop `[left,top,right,bottom]` в долях
исходника и rect `[x,y,w,h]` в каноническом поле. Каждая вырезка нормализуется
по непрозрачной границе, укладывается nearest и получает бинарную alpha.
Альтернатива — color для полосы торца. Полученный рисунок сохраняется в
materialOutput. Реальный пример — demos/rotsprite/weapons/build.json.

Сборка поставляемых файлов:

```sh
python3 tools/make_rotsprite_v2.py
python3 tools/build_rotsprite_assets.py demos/rotsprite/build.json
python3 tools/build_rotsprite_assets.py demos/rotsprite/templates/build.json
python3 tools/build_rotsprite_assets.py demos/rotsprite/weapons/build.json
```

Первая команда собирает материалы и legacy PNG маскота; вторая записывает
JSON-модельные PNG с разделёнными коленями/локтями. Для изменения только
костей, сокетов и анимаций перекомпиляция PNG не нужна.

## 9. Собственное описание модели

Минимальное описание для grid/loft с ID 80:

```json
{
  "version":1,"atlas":"sign.png","style":"anime",
  "rig":{"bones":[{"name":"root","pivot":[0,0,0]}],
         "parts":[{"id":80,"bone":"root"}]},
  "groups":{"paint":[80]},"defaults":{"body":true}
}
```

JSON MUST содержать version=1, atlas, 1..64 bones и 1..254 уникальных parts.
У всех частей MUST существовать bone. Имена MUST быть уникальными и не
являться __proto__/constructor/prototype. Неизвестные поля не превращаются
автоматически в новые функции движка.

part.bind содержит scale, rotation, translation (по три числа).
Это постоянная подгонка локального материала к системе персонажа.
portraitBind — аналог для головы; portrait:true разрешает часть в режиме
body=false. bodyScale по умолчанию 1, portraitScale 2, диапазон (0,8].
Для плоского предмета обычно нужен defaults.body=true; иначе его части
без portrait:true будут скрыты.

Форма носа, одежды и конечностей живёт в surface/PNG. bind удобно менять
для масштаба/смещения целой части, но он не заменяет контрольные точки
при изменении силуэта самой поверхности.

## 10. Псевдоскелет и суставы

```json
"bones":[
  {"name":"root","pivot":[0,0,0]},
  {"name":"arm","parent":"root","pivot":[9.5,-10,0]},
  {"name":"forearm","parent":"arm","pivot":[9.5,4,0]}
]
```

Родитель MUST идти раньше ребёнка. Все pivot заданы в общей системе покоя:
forearm.pivot здесь не `[0,14,0]`. Собственное вращение вокруг pivot затем
композируется с вращением родителя. Порядок осей: X, затем Y, затем Z,
матрица Rz*Ry*Rx. translation задаёт смещение; ручные/клиповые углы в градусах.
Каждая часть привязана к одной кости, смешивания skin weights нет.

Для управления из игры:

```js
model.re2dBone('forearm',{rotation:[30,0,0]});
model.re2dBone('forearm',{translation:[0,0,0],rotation:[0,0,0]});
```

Это абсолютные ручные каналы поверх анимации, а не прибавка к текущему углу.
Заданное значение 0 также перекрывает клип. Для прибавляемого управления
опишите controls: `armLift:{bone:'arm',axis:'z'}`, затем
`.re2dRig({armLift:15})`. Значение control складывается с клипом и ручным углом.
При reload ручные значения сохраняются. Для чистого нового состояния можно
создать модель заново.

joints — именованные точки для UI/логики. В info их x/y находятся в поле
128×128, независимо от растера anime=512. Экранная позиция без внешнего
поворота/flip узла: `nodeCenter + (joint-64)*nodeSize/128`; учитывайте камеру.
Сокеты возвращают пространственные матрицы, joints — уже проекцию.

## 11. Анимации, моргание и рот

```json
{"version":1,"clips":{
  "wave":{"duration":1,"loop":true,"tracks":[
    {"target":"forearm","channel":"rotation.z","keys":[[0,-20],[0.5,20],[1,-20]]}
  ]},
  "blink":{"duration":4,"loop":true,"tracks":[
    {"target":"face","channel":"eyes","keys":[[0,"open"],[3.8,"closed"],[3.94,"open"],[4,"open"]]}
  ]}
}}
```

Каналы костей rotation.x/y/z и translation.x/y/z. Ключи MUST возрастать
в 0..duration, duration>0. Интерполяция linear по умолчанию, step по выбору.
Углы не выбирают автоматически короткий путь через 360: линейный переход
0→360 означает полный оборот. loop:true повторяет клип, loop:false удерживает
конечный ключ. Переходы между разными клипами сейчас без плавного crossfade.

```js
model.re2dMotion('wave',1);     // сбрасывает время основного клипа, включает тело
model.re2dMotion('wave',0);     // тот же клип остановлен с начала
model.re2dSeek(0.5);           // время основного клипа, секунды
model.re2dLayer('blink');      // собственный таймер слоя
model.re2dLayer('blink',false);
```

Слои перекрывают только перечисленные каналы; последний добавленный слой
выигрывает конфликт. Повторное rotLayer(name,true) сохраняет его время.
Слой не удаляется автоматически по окончании одноразового клипа.
Лимиты: 64 клипа, 256 треков/клип, 1024 ключа/трек. Дубликат канала запрещён.
Скорость неотрицательная, время использует игровой dt, а не часы компьютера.
Legacy phase/stride не создают JSON-локомоцию: рисуйте ключи своих костей.

Чтобы подключить мимику, автор MUST нарисовать варианты в независимых UV
областях, назначить их part.selector и part.variant. Иначе переключатель
глаз не создаст нужный рисунок.

| selector | variant 0 / 1 / 2 / 3 |
|---|---|
| eyes | open / half / closed / happy |
| mouth | closed / open / smile / talk |
| brows | neutral / angry / sad / surprised |

Все варианты находятся на соответствующей поверхности лица; скрывается
невыбранный вариант. oneSided:true SHOULD применяться к деталям лица,
чтобы дальний глаз не проступал сквозь затылок. Нормаль зависит от порядка
UV-точек: если исчезает ближняя сторона, проверьте их ориентацию.

`.re2dExpression({eyes,mouth,brows})` задаёт базовую мимику; пропущенные поля
сбрасываются в open/closed/neutral. Активный слой лица перекрывает базу.
Для ручного открытия глаз выключите blink. Речь — только изменение рта
по таймеру, анализа звука/фонем здесь нет. emotions в JSON сопоставляет
произвольное имя набору этих трёх состояний.

## 12. Костюмы, волосы и подмены

```json
"groups":{"costume":[6,7,11,20,21,8,12,10,15,9,13,14]},
"variants":{"costume":{"normal":"normal.png","police":"police.png"}}
```

```js
model.re2dVariant('costume','police');
model.re2dPart('costume','another_donor.png');
```

rotPart заменяет геометрию И материал указанных ID из донорского PNG.
Это не простая перекраска. Донор MUST быть v2 и содержать совместимые
координаты/ID; скелет, имена костей и анимации берутся у принимающей модели.
Новая причёска может иметь другую форму, если остаётся привязанной к
правильной группе и голове. Если меняется сам rig, используйте другое
описание модели, а не только донор PNG.

При последовательных перекрывающихся группах важен порядок применения;
последняя подмена выигрывает. При reload порядок повторяется. Явного
«снять одну подмену» пока нет: подставьте базовый совместимый PNG или
пересоздайте модель. Донор может иметь только нужную группу, но не может
быть полностью пустым. Путь rotPart — обычный путь игры; rotVariant
разрешает путь относительно character.json.

## 13. Сокеты, предметы и изготовка

Сокет — точка и ориентация в общей bind-системе, привязанная к кости:

```json
"sockets":[
  {"name":"handRight","bone":"forearmRight","point":[9.5,18,0]},
  {"name":"back","bone":"root","point":[0,-2,-8],"rotation":[0,0,35]}
]
```

У предмета есть собственные sockets, например trigger/foregrip/muzzle.
Это игровые якоря; стрелять Re2DSprite сам по себе не умеет.

```js
const item = $.re2dSprite.from('art/item.character.json').re2dHotReload();
item.re2dAttach(model,'handRight',{grip:'trigger',rotation:[0,180,0]});
item.re2dDetach();
item.remove();
```

Формула крепления: parentSocket * offsetRotationScale * inverse(itemGrip).
После этого преобразуются все части предмета. Без grip совмещается его
начало. offset по умолчанию [0,0,0], rotation [0,0,0], scale [1,1,1]; scale
MUST быть положительным. Поворот закреплённого предмета идёт от родителя;
его rotPose запрещён, локальная ориентация задаётся rotAttach.rotation.

Для готовых наборов equipment:

```js
const rifle = $.re2dSprite.equip(model,'ak47',{id:'held-rifle'}).re2dHotReload();
model.re2dLayer('holdRifle');
```

equip читает model/socket/grip/offset/rotation/scale из equipment JSON,
создаёт самостоятельный узел и закрепляет его. Он не удаляет предыдущий
предмет и не включает hold-клип автоматически: это логика игры/инвентаря.
Демонстрационная игра использует поле pose для выбора слоя.

Новая holdRifle — изготовка вперёд, с согнутыми локтями. В authored bind
системе правая кисть [7,-4,10], левая [7,-5,22]. Сокеты trigger и foregrip
совмещены с ними; продольная ось АК направлена +Z персонажа. Поворот yaw
меняет направление вместе с телом. Числа rotation оборудования компенсируют
ориентацию кисти, поэтому их не следует заменять одним поворотом 90° без
проверки позы. Дробовик использует те же точки хвата; для нового размера
предмета SHOULD авторить собственную позу.

Это заранее рассчитанная JSON-поза двухзвенной руки, не runtime IK.
Если меняются длина рук, положение рукояти, цевья или scale предмета,
вторую руку нужно подогнать в клипе. Возврат в исходную позу:
`model.re2dLayer('holdRifle',false)`. При walk/run слой удержания перекрывает
движение рук основного клипа, ноги продолжают свой цикл.

Оба узла MUST быть корневыми узлами сцены, не обычными parent_node-детьми.
Граф Re2DSprite-креплений отдельный; циклы запрещены, цепочки допускаются.
Предмет наследует проекционное поле, yaw/pitch и слой родителя. Сам имеет
собственную модель/текстуру и может проигрывать свои клипы. В режиме головы
предмет скрыт, если сокет не portrait:true. При удалении родителя предмет
отсоединяется и остаётся самостоятельным; игра решает, удалить ли его.

## 14. Обновление без перезапуска

`.re2dHotReload(true)` наблюдает собственный PNG, описание JSON, внешний файл
анимаций и текущие PNG доноров. Опрос раз в 0.5 секунды. VFS-упакованные
ассеты считаются неизменяемыми. `.re2dReload()` принудительно повторяет
загрузку и подмены. Невалидное обновление сохраняет прежний ресурс,
info.reloadError объясняет ошибку.

Сохраняются pose, expression, rig, motion time/speed, ручные кости, слои,
варианты и граф креплений. Нельзя удалить активный клип, активный слой или
занятый сокет и ожидать успешного reload. Изменения equipment не меняют
параметры уже созданного крепления: переоснастите предмет через equip либо
rotAttach. Именно поэтому после изменения изготовки демо перезапускается.

surface.json и material.png runtime не отслеживает. Их правки вступают
в силу после компиляции нового object.png. Поставляемые компиляторы сохраняют
готовый PNG атомарно (сначала временный файл, затем замена), чтобы движок
не пытался открыть недописанный файл. Сторонние авторские инструменты SHOULD также использовать атомарную замену.
Предварительная проверка материала остаётся задачей авторского инструмента.

## 15. API и наблюдение за состоянием

| Метод | Назначение |
|---|---|
| `$.re2dSprite.from(source,opts?)` | JSON-модель; opts — обычные свойства узла |
| `$.re2dSprite.create(PNG,opts?)` | legacy v1/v2 без JSON |
| `$.re2dSprite.definition(target)` | независимая копия описания |
| `$.re2dSprite.equip(parent,key,opts?)` | создать предмет из equipment |
| `.at(x,y).size(w,h)` | позиция и поле спрайта |
| `.re2dPose(yaw,pitch=0)` | yaw wrap [-180,180), pitch clamp ±75 |
| `.re2dStyle('anime'|'pixel')` | сменить проекцию с сохранением состояния |
| `.re2dRig(options)` | body и controls; legacy поля совместимости |
| `.re2dMotion(name,speed=1)` | основной клип, сброс времени |
| `.re2dSeek(seconds)` | время основного клипа |
| `.re2dLayer(name,enabled=true,speed=1)` | наложенный клип |
| `.re2dBone(name,options)` | ручные каналы кости |
| `.re2dExpression(options)` / `.re2dEmotion(name)` | база мимики |
| `.re2dVariant(group,key)` / `.re2dPart(group,PNG)` | доноры |
| `.re2dAttach(parent,socket,options)` / `.re2dDetach()` | крепление |
| `.re2dHotReload(enabled=true)` / `.re2dReload()` | обновление |
| `.re2dSpriteAtlas(PNG)` | заменить ресурс и сбросить JSON/состояние |
| `.remove()` | удалить узел, освободить ресурс |
| `$.re2dSprite.dispose(target)` | освободить ресурс, сохранить узел |
| `$.re2dSprite.info(target)` | сведения или null |

Методы узла возвращают цепочку. Не используйте rotSpriteAtlas как замену
rotReload для JSON-модели: он создаёт legacy-состояние.

info содержит version, style, atlasWidth, width/height, surfaceSamples,
yaw/pitch, eyes/mouth/brows (индексы 0..3), body, rig, motion, parts,
revision, hotReload, reloads/reloadError, definition, animationTime, layers,
joints, sockets и attachment. Сокет возвращает matrix из 12 чисел:

```text
[ r00 r01 r02 tx
  r10 r11 r12 ty
  r20 r21 r22 tz ]
```

Матрицы в модели до общего yaw/pitch камеры. Для закреплённого объекта
они уже преобразованы в систему родителя. Сравните translations [3,7,11]
сокетов handRight/trigger для проверки основного хвата и handLeft/foregrip
для второго. Не сравнивайте их непосредственно с экранными joints.x/y.

Игра SHOULD использовать `$`. Native bulk API rotSpriteModelPose получает
scale и rows `[id,selector,variant,oneSided,visible,...matrix12]`, атомарно
проверяет значения и пересчитывает кэш. Оно нужно обёртке/расширениям,
а не обычному игровому инвентарю. Реализация: src/highlevel/rotsprite.js,
src/rotsprite.c, src/rotsprite_math.c; компиляторы в tools/.

## 16. Качество, ограничения и отладка

Pixel: растер v2 128×128, nearest и целый масштаб/привязка к пикселям.
Anime: внутреннее поле 1024×1024, сглаженный выход 512×512, плавная позиция,
linear. V1: голова 64×64. Новый JSON по умолчанию anime; legacy create — pixel.
Поворот меняет силуэт и детали, но не добавляет динамическое освещение:
все тени сейчас нарисованы в материале.

| Симптом | Что проверить |
|---|---|
| отказ загрузки PNG | размеры, header, alpha 0/255, хотя бы один активный ID |
| цвет есть в атласе, часть исчезла | coverage, непрозрачный anchor ячейки, parts.id, body/portrait |
| срез/дырки в профиль | недостаточная поверхность, разрыв XYZ/UV, отсутствие торца |
| глаз виден сзади | selector, oneSided, ориентация grid/UV и глубина лица |
| нос/подбородок плоские | surface XYZ, не только рисунок материала |
| шов в локте/колене | совпадение bind координат, ID сегмента, положение pivot |
| предмет боком или вверх | локальная ось предмета и вращение socket/grip; компенсация кисти |
| левая рука не на цевье | отдельная authored hold-поза; автоматического IK нет |
| мимика не меняется вручную | активный слой blink/talk перекрывает базу |
| правка JSON не появилась | hotReload, relative path, reloadError, активный удалённый клип |
| PNG обновился, просмотр атласа прежний | обычный sprite-кэш обзорного UI, переоткрыть сцену |
| предмет режет кисть/тело | межмодельное перекрытие пока приближённое |

Между разными Re2DSprite нет общего Z-buffer: предмет рисуется целиком
перед/за родителем по приблизительной глубине его центра. Это может дать
неверное перекрытие руки/приклада даже при точных сокетах. Нельзя выдавать
его за исправление анатомии. Внутри одной модели глубина по отсчётам есть.

Нет blended skin weights, общего IK, тканевой физики, collision-меша,
синхронизации губ со звуком, автоматического OBJ-импорта или редактора
сеток с GUI. Пользовательская сетка сейчас редактируется в JSON/своём
авторском инструменте. Референс OBJ Руси-тян использовался только офлайн.

Общий цвет/alpha, камера, слои и clip работают через обычный узел. Внешние
angle/pivot, неравномерный flip/scale и пользовательские shader/outline/shadow
узла для Re2DSprite пока не поддерживаются как для обычного sprite.
Для наклона используйте rotPose или кости; для размера — size/projection.

Проекция CPU экспериментальная. Debug с несколькими непрерывно меняющимися
моделями может работать медленно; 60 FPS не подтверждены. Не обещайте
массовые толпы и не назначайте 4096 как способ увеличить точность сетки.
Одинаковая поза не должна вызывать новый native upload. Анимация, меняющая
координаты, требует нового изображения. Каждая модель имеет свой ресурс;
remove/dispose и смена сцены освобождают его.

Материалы АК сделаны по референсу пользователя. Пистолет и дробовик пока
используют переразмещённые материалы металла/дерева АК и собственную
геометрию; отдельные art-запросы были отклонены генератором. Это прототипные
игровые ассеты, не окончательная художественная работа. Происхождение и
запросы: demos/rotsprite/weapons/README.md и source/*.md.

## 17. Проверка перед добавлением в игру

Сначала проверьте одну модель без анимации, затем кости, потом мимику и
крепления. Посмотрите фронт, оба профиля, 3/4, затылок, pitch ±45; после
этого walk/run и крайние сгибания суставов. PNG и успешная сборка сами по
себе не подтверждают хорошее сочленение или правильное перекрытие.

Проверки из корня репозитория:

```sh
cmake --build build -j 6
./build/tests/r2d_rotsprite_test
./build/_deps/quickjs-build/qjs tests/js/rotsprite_json_test.mjs
python3 tests/rotsprite/surface_test.py
python3 tests/rotsprite/profile_test.py
python3 tests/rotsprite/material_test.py
python3 tools/run_tests.py highlevel_rotsprite_test rotsprite_json_test
```

Нативный тест проверяет проекцию, произвольные ID/матрицы и совместимость;
JS — формат, интерполяцию, слои, сокеты, циклы и reload; authoring — grid,
loft, UV/ID/XYZ; агентские — реальные ресурсы, три предмета, изготовку,
оба хвата, повороты, live JSON, animal/prop и управление демо. Отдельно
профиль/материал проверяют исходные PNG головы и одежды маскота.

Если проверки прошли, сохраните вместе material, surface, character,
animations и build JSON. Для релиза достаточно runtime PNG/JSON и всех
используемых donor/equipment assets. Спецификация фиксирует текущую
версию; расширения с новым кодированием MUST получить новый номер формата,
а не переопределять существующие байты незаметно для загрузчика.

Полные формулы, функции и таблица частей: [Математика Re2DSprite](RE2DSPRITE_MATH).
