# `$.real2d` — Real2D v4: вычисляемый layered warp персонажа

Модуль подсистемы `$`: рендер персонажа из **отдельных семантических
RGBA-компонентов** (лицо, глаза, нос, рот, волосы), согласованных 2D-сеток и
коэффициентов функций ракурса. Каждый угол yaw **вычисляется**, а не выбирается
из готовых кадров: в контейнере нет ни одного полнофигурного ракурса.

Спецификация и границы — [REAL2D_V4_SPEC.md](https://github.com/Nikide/russiano2d/blob/main/demos/real2d/REAL2D_V4_SPEC.md),
план стадии — [STAGE_A_PLAN.md](https://github.com/Nikide/russiano2d/blob/main/demos/real2d/STAGE_A_PLAN.md).

**Чем отличается от `$.re2dSprite` (v2/v3).** Тот синтезирует кадр из одной
большой развёртки, где попиксельно закодированы part-ID, скининг и перспектива.
Здесь другой вход (отдельные компоненты + cage + манифолд + гейты видимости) и
другой путь кадра (инверсно-барицентрическая растеризация реальных texels с
композицией по псевдоглубине). Это не вторая реализация той же подсистемы:
формат данных, математика и доказательства разные.

## Стадия A (что реально работает)

| Ограничение | Значение |
|---|---|
| Часть тела | только голова (`head-only`) |
| Yaw | полный круг `S¹`, радианы |
| Pitch | **не вход**: стадия B отдельно |
| Anchors | 12 геометрических, Fourier `K=3`, `L=0` |
| Контейнер | `.r2d4` — ZIP store-only: `manifest.json`, `atlas/atlas_00.png`, `geometry/*` |
| Композиция | linear RGB, premultiplied alpha, вывод в sRGB8 |

## API модуля

```js
const id = $.real2d.load('demos/real2d/assets/head_real2d_v4.r2d4'); // кэш по пути
const sprite = $.real2d.frame(id, { yaw: 1.05, blink: 0, mouth: 0, scale: 512 });
const info = $.real2d.info(id);        // структурные факты ассета и кадра
const rgba = $.real2d.pixels(id);      // Uint8Array RGBA8 последнего кадра
const chain = $.real2d.provenance(id, x, y);  // patch → triangle → UV (если рендер с provenance)
$.real2d.debugSetAtlas(id, bytes);     // мутация источника для доказательных тестов (null — вернуть)
$.real2d.dispose(id);
```

| Вызов | Возвращает | Назначение |
|---|---|---|
| `load(path)` | `number` | id контейнера; повторный вызов отдаёт тот же id |
| `frame(id, opts)` | `number` | id спрайта в общем батче движка |
| `pixels(id)` | `Uint8Array` | **копия** RGBA8 последнего кадра на момент вызова (для проверок) |
| `info(id)` | `object` | canvas, atlas, rank, dim, счётчики кадра, фазы `ms`, `last_error` |
| `provenance(id, x, y)` | `object\|null` | цепочка patch → треугольник → UV для пикселя |
| `provenanceMap(id)` | `Uint16Array` | индекс патча+1 на пиксель (нужен рендер с `provenance`) |
| `atlasPixels(id)` | `Uint8Array` | копия атласа (или подменённого) — для доказательных мутаций |
| `patchWeights(id, yaw)` | `array` | что решил движок: `gate`, перенос, масштаб по патчам |
| `debugSetAtlas(id, bytes)` | `bool` | подмена атласа целиком (доказательные тесты) |
| `handles()` | `Map` | путь → `{ id, users }`: что уже загружено (отладка и тесты) |
| `yaw(rad)` | `number` | нормализовать угол в `0..2π` (та же функция, что у `.real2dYaw()`); нечисло — `RangeError` |
| `dispose(id)` | `bool` | освободить ассет и его текстуру |

`frame` принимает `{ yaw, blink, mouth, scale, validate, provenance }`:
`yaw` — радианы, `blink`/`mouth` — 0..1, `scale` — размер растра 128..1024,
`validate` включает счётчик записей на пиксель (проверка fill rule),
`provenance` — запись цепочки для `provenance()`.

## Узел `<real2d>`

```js
$('<real2d>', { id: 'head' })
    .at(400, 300).size(512, 512)
    .real2dSrc('demos/real2d/assets/head_real2d_v4.r2d4')
    .real2dPose(0.6, { blink: 0 })
    .appendTo($.world);

$.update(dt => $('#head').real2dYaw($.time.now()));   // угол меняется кодом
$('#head').real2dSpin(0.8);                            // или автоповорот, рад/с
```

| Метод | Что делает |
|---|---|
| `.real2dSrc(path)` | загрузить контейнер и отрисовать кадр |
| `.real2dPose(yaw, { blink, mouth })` | задать угол и состояние, пересчитать кадр |
| `.real2dYaw(yaw)` | только угол |
| `.real2dSpin(rad_per_sec)` | автоповорот в тике |
| `.real2dSize(px)` | размер растра (128..1024) |
| `.real2dReload()` | перечитать контейнер с диска (после bake) |
| `.real2dInfo()` | `$.real2d.info` плюс состояние узла (метод-геттер первого узла) |

## Композиция

Слои группы «лицо» (кожа головы, варианты вида) складываются **внутри группы**
(`Σ w·α`, `Σ w·α·C`) и сбрасываются на кадр одним проходом с нормированным
покрытием: два appearance-варианта по 0.5 дают alpha 1, а не 0.75 — иначе лицо
на переходе становится полупрозрачным «призраком» (спека §7 правило 5).
Остальные слои (чёлка, пряди, глаза, нос, рот) накладываются обычным `over` по
псевдоглубине; детали лица дополнительно умножаются на маску силуэта лица.

Переходы между вариантами — короткие (1.5–10° в зависимости от кольца), то есть
быстрый свап, а не длинный dissolve: при 60 кадрах и обычной скорости поворота
это доли кадра, зато нет «двойного лица» на пол-оборота.

## Ограничения и честные границы

* pitch не поддерживается: стадия A объявляет домен `pitch = 0`, вне домена
  ассет не притворяется, что умеет;
* полнотелого персонажа нет — это head-only стадия, и выдавать её за v4 целиком
  запрещено;
* растеризация идёт через общий пул потоков движка (`R2D_ROT_THREADS=1` —
  последовательный режим для замеров и отладки). Замер на объявленной машине
  (Apple Silicon, Release, полный круг): **13.5 мс/кадр** при 512×512 и
  **4.3 мс** при 256×256; до распараллеливания было 24.9 и 5.6 мс (фаза
  растеризации ускорилась в 2.5 раза). Разбивка по фазам — `$.real2d.info().ms`
  (`clear`, `evaluate`, `raster`, `composite`, `encode`, `upload`).
  Цель спеки (≤1 мс) не достигнута: остаток — стоимость на пиксель
  (≈19 нс на проверку) и три отдельных прохода (растр → композит → кодирование);
  план — слияние композита с растеризацией и SIMD-выборка;
* `debugSetAtlas` — диагностический вход для доказательных тестов, а не игровой
  путь: он меняет источник texels, чтобы проверить provenance и мутации.
