> Совместимость: эта страница относится к `$.re2d.room`, `.kind(Re2D)` и старому `$.re2d.world`. Ограничения room/billboards ниже не относятся к новому `$.re2dWorld`. Для нового проекта начните с [World guide](RE2D_WORLD_GUIDE). Замеры ниже исторические, без текущей benchmark-провенанс; не используйте их для обещаний FPS.

# Legacy Re2D: room/kind и прежний World

Re2D — **дополнение** к 2D, а не замена: мир остаётся плоским (позиция, размер,
тело, слои, события узла — обычные 2D-поля), а Re2D добавляет **вид** на него:
камеру от первого лица и отрисовку узлов в перспективе. Замысел, правила и план
фаз — [RE2D.md](RE2D); вид узла — [kinds.md](highlevel/kinds); камера —
[camera.md](highlevel/camera) §5; нативная проекция — [internal/NATIVE.md](internal/NATIVE) (`engine.re2d.*`).

```js
$.ready(() => {
    $.camera.kind(Re2D).eye(48).fov(70).mouseLook(true);   // от первого лица, мышь крутит взгляд
    $('<player>', { id: 'hero' }).at(640, 1100).controls('wasd').kind(Re2D).appendTo($.world);
    $.camera.follow('#hero');                              // глаза на теле, без сглаживания
});
```

Узел и камера **без** `kind` — обычный 2D, и для них ничего не изменилось.

---

## 1. Что здесь есть

| Вызов | Результат |
|---|---|
| `$.re2d.room(opts)` | комната-коробка: пол, потолок и четыре стены-плиты вокруг внутреннего прямоугольника; → обёртка узлов (класс `re2d-room`) |
| `$.re2d.info()` | факты: `{ camera, native, surfaces, buckets }` — камера ([camera.md](highlevel/camera) §5), вид и счётчики ядра, сколько поверхностей и какие «вёдра» отправлены в кадре |
| теги `<wall>`, `<floor>`, `<ceiling>` + `.kind(Re2D)` | поверхности мира (§2) |
| теги `<player>`, `<npc>`, `<enemy>`, `<pickup>`, `<bullet>`, `<sprite>`, `<rect>`, `<rotsprite>` + `.kind(Re2D)` | билборды: картинка на полу, всегда лицом к камере (§3) |
| `$.re2d.poseStep(deg?)` | шаг квантования позы Re2DSprite, градусы (по умолчанию 3; 0 — без квантования) |
| `$.re2d.poseBudget(n?)` | предел синтезов позы за кадр (по умолчанию 4; 0 — без предела) |

Остальной API Re2D — это **те же** методы `$`, у которых при `kind(Re2D)`
меняется смысл (таблица «метод × вид» в [миграции](https://github.com/Nikide/russiano2d/blob/main/docs/re2d/RE2D_MIGRATION.md)), и новые слова
камеры там, где в 2D смысла нет (`pitch`, `eye`, `fov`, `mouseLook`, `look`).

## 2. Поверхности мира

Мир по-прежнему плоский: у каждой поверхности есть обычный 2D-прямоугольник
`x, y, w, h` на полу (по нему же работает физика), а Re2D добавляет высоту.

| Тег | Что рисуется | Высота |
|---|---|---|
| `<wall>` | призма над прямоугольником; с телом `static`, как обычная 2D-стена | от `.depth(z)` (основание, по умолчанию 0) до `attr('top')` (по умолчанию основание + 256) |
| `<floor>` | горизонтальная плоскость | на высоте `.depth(z)` (0 по умолчанию) |
| `<ceiling>` | горизонтальная плоскость | на высоте `attr('top')` (по умолчанию 256) |

```js
$('<wall>', { top: 300, tile: 128 }).at(600, 400).size(200, 32).sprite('art/brick.png')
    .color('#ffffff').kind(Re2D).appendTo($.world);       // стена-перегородка высотой 300
```

* **Текстура — обычный `.sprite(path)`**: цвет узла (`.color()`) умножается на
  текстуру, поэтому у текстурной стены ставьте `#ffffff` (у `<wall>` по
  умолчанию серый `#555555`, как в 2D). Без спрайта поверхность заливается цветом.
* **`tile`** (атрибут) — сторона тайла в единицах мира: через столько текстура
  повторяется; по умолчанию 64 (без текстуры — 256, повторять нечего).
* **`top`**, а не `height`: в конструкторе узла `height` — это размер спрайта.
* **Порядок `.sprite()` и `.size()`.** `.sprite(path)` подгоняет узел под размер
  картинки, если ширина ещё 32 (так и в 2D): сначала `.sprite()`, потом `.size()`
  — `$.re2d.room` делает именно так.
* **Лицевая сторона.** Грани призмы рисуются, только если обращены к камере (по
  обходу), поэтому комната из четырёх плит видна изнутри, а снаружи плиты
  «прозрачны». Пол и потолок двусторонние.
* **Под 2D-камерой** `<wall>` и `<floor>` деградируют в обычные 2D-прямоугольники —
  вид сверху на тот же мир (план этажа, миникарта); `<ceiling>` скрыт.

## 3. Билборды и персонажи Re2DSprite

Узел вида Re2D с «картиночным» тегом — плоская картинка, стоящая на полу и
повёрнутая к камере (как в Doom). Позиция `(x, y)` — точка на полу, основание —
`.depth(z)` (высота над полом, по умолчанию 0), `.size(w, h)` — размер картинки в
единицах мира, а не пикселях экрана: на экране она масштабируется глубиной.

```js
$.re2dSprite.from('demos/rotsprite/russi.character.json', { id: 'russi' })
    .re2dStyle('pixel').re2dVariant('costume', 'police')
    .at(640, 380).size(150, 150).kind(Re2D);     // маскот в комнате; angle — куда он смотрит
$('#russi').get(0).angle = Math.PI / 2;           // лицом на юг (+y); 0 — на +x
```

* **Один нативный вызов на кадр.** Отрисовщик только записывает билборд; в конце
  прохода основания всех записей проецируются `engine.re2d.project` разом, записи
  сортируются от дальних к ближним (при равной глубине — по `uid`, порядок
  детерминирован) и уходят в общий батч спрайтов. Позади камеры и за краем кадра
  билборд не рисуется; прозрачность узла (с учётом родителей) запоминается в момент
  записи.
* **Туман.** `$.camera.fog(far, min)` затемняет и билборды: rgb × `clamp(1 − d/far, min, 1)`.
* **Анимация и кадры.** Спрайт берётся у узла (`.frames()`, `.animate()` работают),
  у `<rotsprite>` — картинка модели.

### Re2DSprite поворачивается за камерой

Для узла `<rotsprite>` Re2D каждый кадр считает позу модели, какой её видит
камера, и ставит `.re2dPose(yaw, pitch)`:

* **yaw** — на сколько персонаж повёрнут относительно взгляда камеры: 0 — лицом к
  зрителю, +90° — лицом вправо от зрителя, ±180° — спиной. Берётся из направления
  `node.angle` на полу и положения камеры, поэтому при обходе вокруг персонажа он
  плавно поворачивается — в Doom для этого рисовали 8 ракурсов;
* **pitch** — под каким углом зритель видит центр персонажа (положителен, если глаза
  ниже центра, отрицателен — если выше).

Поза **квантуется** (`$.re2d.poseStep`, 3° по умолчанию) и меняется только при смене
квантованного значения: синтез картинки в C стоит заметно. Цифры замера (headless,
один персонаж): `re2dPose` в **pixel**-стиле — около 1.3 мс, в **anime** — около
11 мс. Поэтому:

* маскоты для Re2D делайте в `pixel`-стиле (`.re2dStyle('pixel')`) — он же даёт
  ретро-вид «как в Doom»; anime-стиль годится для одного-двух персонажей;
* за кадр синтезируется не больше `$.re2d.poseBudget` поз (4 по умолчанию): при
  обходе камерой поправки всех персонажей не приходят в один кадр, остальные
  догоняют на следующих. `$.re2d.info().poses` показывает `{ updated, deferred }`;
* анимация JSON-модели (`re2dMotion('idle')`) — это синтез каждый кадр на
  персонажа независимо от камеры (pixel: около 3 мс), поэтому в сцене держите
  немного анимированных маскотов.

Пример замера (3 маскота в pixel-стиле, idle-анимация, камера стоит): `JS: логика`
9.6 мс; при обходе камерой добавляется около 3.4 мс на синтез поз.

## 4. Игрок от первого лица

`.controls('wasd')` у узла вида Re2D под Re2D-камерой работает по **взгляду** камеры,
а не по осям экрана: `W` ведёт «вперёд» туда, куда смотрят глаза, `S` — назад, `A`/`D`
— боком. Это тот же метод `$`, у которого при `kind(Re2D)` другое значение (таблица
«метод × вид», [миграции](https://github.com/Nikide/russiano2d/blob/main/docs/re2d/RE2D_MIGRATION.md)):

```js
$('<player>', { id: 'hero' }).at(640, 1130).size(36, 36).collision(30, 30)
    .speed(240).controls('wasd').kind(Re2D).appendTo($.world);
$.camera.follow('#hero').yaw(-90);                       // глаза на теле, взгляд на север
$.camera.mouseLook({ on: true, sensitivity: 0.0026 });   // мышь крутит камеру
```

* Скорость задаётся по обеим осям пола; тело — обычное 2D (Box2D), поэтому стены, слои
  и события контакта работают как в 2D. Гравитации нет (`$.world.gravity(0, 0)`).
* Ввод — `$.input.vec(...)`, повёрнутый на yaw камеры (`re2dMove`, чистая функция).
* Узел без `kind` и любой узел под 2D-камерой управляются как раньше.
* Mouse-look детерминирован: сдвиг мыши за кадр попадает в `--record` (поля `dx`/`dy`),
  и прогулка воспроизводится в ту же точку ([RECORD_REPLAY.md](RECORD_REPLAY)).

Маскотов и их реакции движок **не** выдумывает: расстояние → поворот → эмоция — это
обычная игровая логика в демо ([demos/re2d_world](demos/re2d_world)),
а состояние лежит в свободном атрибуте (`attr('state')`), поэтому проверяется без
пикселей: `$.expect('#maid').state('smile')`. Поворачивать узел с телом нужно методом
`.angle(rad)` (он двигает и тело): прямая запись в `node.angle` перезаписывается физикой.

## 5. Как это устроено

1. **Вид камеры.** `$.camera.kind(Re2D)` переключает проход мира. Для обычной
   камеры `render.js` проверяет `cam.kind !== '2d'` один раз на проход.
2. **Проход вида.** Если у камеры вид с зарегистрированным проходом
   (`$.kinds.pass`), 2D-мир не рисуется: вызывается `begin(cam)`, затем рисуются
   узлы **этого же вида** (узлы других видов, в том числе 2D, под такой камерой
   не рисуются — у них нет места в её пространстве), затем `end(cam)`.
3. **Вёдра.** Отрисовщик поверхности не шлёт меш по одному узлу: треугольники
   копятся в вёдрах по паре «текстура, режим граней» (стены с отбраковкой, плоскости
   без), а `end` отправляет каждое ведро одним `engine.re2d.mesh`. Порядок не важен —
   разбирает z-буфер.
4. **Нарезка и кэш.** Грань режется на ячейки по тайлу: текстуры меша аффинные, и
   большая ячейка кривит картинку. Геометрия узла строится один раз и
   пересобирается, только если изменились его поля (положение, размер, `depth`,
   `top`, `tile`, цвет, спрайт); число ячеек на сторону ограничено 96.
5. **Деградация.** Узел вида `re2d` под обычной 2D-камерой рисуется как 2D-узел:
   включать вид можно по частям.

Стоимость кадра комнаты 1024×1024 с тремя текстурами (около 900 треугольников
в вёдрах): JS-часть — `логика` 0.76 мс, `сборка батча` 1.14 мс (замер
`$.debug.profile()`, 240 кадров, headless).

## 6. Ограничения

* Камера Re2D — **главная**. Дополнительные камеры сплитскрина и PIP
  ([viewports.md](highlevel/viewports)) остаются 2D.
* Свет, туман и тени 2D (`$.gfx.light`, `$.gfx.fog`) в проходе Re2D не
  участвуют; затемнение с расстоянием — `$.camera.fog(far, min)`.
* Физика остаётся плоской (вид сверху); высота `z` узла нужна только рисованию.
* Спрайты всегда рисуются поверх меша (ограничение z-буфера, [depth.md](highlevel/depth) §4):
  объекты внутри комнаты, которые должны закрывать персонажей, пока не
  поддержаны ([миграции](https://github.com/Nikide/russiano2d/blob/main/docs/re2d/RE2D_MIGRATION.md)).
* Билборд всегда плоский и стоит прямо: наклон камеры не искажает картинку, как
  и в Doom. Спрайты поверх меша, поэтому билборд никогда не заслонён стеной или
  столбом (внутри комнаты-коробки это верно).
* Re2D-узлы других тегов (`<text>`, `<particles>`, `<tilemap>` …) под Re2D-камерой
  пока не рисуются.

## 7. Проверка

```bash
build/_deps/quickjs-build/qjs tests/js/camera_re2d_test.mjs     # JS-слой камеры
build/_deps/quickjs-build/qjs tests/js/re2d_test.mjs            # геометрия поверхностей, вёдра
./build/tests/r2d_re2d_test                                     # математика (C)
python3 tests/agent/re2d_native_test.py                         # ядро в движке, пиксели
python3 tests/agent/highlevel_camera_re2d_test.py               # камера в движке
python3 tests/agent/highlevel_re2d_room_test.py                 # комната: пиксели, физика, текстуры
python3 tests/agent/highlevel_re2d_billboards_test.py           # билборды и маскоты Re2DSprite
python3 tests/agent/highlevel_re2d_world_test.py                # демо: ходьба, стены, маскоты, запись/воспроизведение
```


## 8. Сохранённый `$.re2d.world(description)`

Принимает объект с `walls` и `cells`, где cell задаёт x/y/w/h и свободные spans. Это прежний CPU compositor, не alias нового `$.re2dWorld`. Сигнатура:

```js
const oldWorld = $.re2d.world({
  walls: [{from:[100,-70],to:[100,70],bottom:0,top:60,color:'#c83c28'}],
  cells: [{x:-200,y:-200,w:600,h:400,spans:[{bottom:0,top:128}]}]
});
// npc — заранее созданный Re2DSprite; eye — высота камеры.
$.render(() => oldWorld.render({x:0,y:0,eye:48,yaw:0,pitch:0,fov:70}, npc,320,180));
```

`render(view,entities=[],width=320,height=180)` получает sprite nodes при каждом вызове. Он сохраняет прежнюю JS-подготовку и CPU stamp с приближённой глубиной изображения. `support`, `blocked`, `ray`, `info`, `dispose` сохранены. Здесь нельзя подменить второй аргумент числом разрешения по примеру нового API: это место для entities.

Прежний `worldPrimitives` преобразует объект в плоские стены/spans и не передаёт portal topology/continuous slopes. Для новых материалов/света/sample depth/native registrations используйте новый loader. Перенос шаг за шагом — [RE2D_MIGRATION.md](https://github.com/Nikide/russiano2d/blob/main/docs/re2d/RE2D_MIGRATION.md). Legacy demos: `demos/re2d_world`, `demos/re2d_bsp_world`; новые fixtures: `demos/re2d_world_renderer_lab`, `demos/re2d_dust2`.
