# Несколько камер — `$.camera.add/split/views`

Сплитскрин и «второй вид»: одна сцена рисуется с нескольких камер в разные части
окна. Для игрока это одна подсистема с камерой, поэтому методы живут на
`$.camera`; отдельный модуль — потому что состояние вторичных камер своё.

```js
$.camera.split(2);                        // две камеры, половины окна
$.camera.viewAt('p2', hero2.x, hero2.y);  // куда смотрит вторая

$.camera.split(4);                        // квадраты 2×2
$.camera.viewAt('p3', 1200, 300);
```

---

## 1. Как это работает без сциссора и без целей

Движок рисует в **один проход и одну цель**: сциссора и смены `viewport` в
проходе нет, а «камера в текстуру» на каждую камеру стоила бы отдельной цели из
бюджета 256 текстур. Но регион экрана выражается **проекцией**:

> камера с зумом `k` и центром `c` занимает прямоугольник шириной `W/k` вокруг
> точки, куда смотрит.

Значит, вторая камера — это не второй проход с текстурой, а **пересчитанные
`x`, `y` и `zoom`** так, чтобы её кадр лёг ровно в нужный прямоугольник. Вся
арифметика — в `regionCamera()`; отдельная цель не нужна вовсе.

Камеры рисуются в **один батч кадра**, по порядку. Регионы не пересекаются,
поэтому поздняя камера просто ложится поверх.

**Важно:** во время прохода мира `view` (то, что двигает узлы) остаётся
**выключенным**. Узлы переводятся в экранные координаты сами, и включённый
`view` применил бы камеру **второй раз** — при сплитскрине спрайт второй камеры
уезжал на `x = −6000` вместо `600`. Это был настоящий дефект, найденный замером
фактических координат спрайтов.

---

## 2. Методы

| Вызов | Смысл |
|---|---|
| `$.camera.add(name, opts?)` | завести вторичную камеру (`opts`: `x`, `y`, `zoom`, `rect`) |
| `$.camera.remove(name)` | убрать её |
| `$.camera.list()` | имена вторичных камер |
| `$.camera.camCount()` | сколько рисуется: главная + вторичные |
| `$.camera.split(count)` | разложить `count` камер по окну (имена `p2`, `p3`, …) |
| `$.camera.viewAt(name, x?, y?)` | куда смотрит вторичная камера |
| `$.camera.viewZoom(name, value?)` | зум вторичной камеры |
| `$.camera.region(name, rect?)` | явный регион `{x, y, w, h}` |
| `$.camera.views()` | снимок раскладки: что и где рисуется в этом кадре |

`split(count)` **заменяет** прежнюю раскладку: после `split(2)` на `split(4)`
хвоста от прошлых камер не остаётся. Состояние камеры сохраняется, если её имя
уже было, — можно менять только раскладку.

Главная камера (`$.camera.at/zoom/follow/limits`) сплитскрин **не подменяет**:
она остаётся «камерой игрока» и попадает в свой регион первой.

---

## 3. Раскладка

| Камер | Регионы |
|---|---|
| 1 | всё окно |
| 2 | две половины по горизонтали |
| 4 | квадраты 2×2 |
| прочее `n` | `n` вертикальных полос |

Регион можно задать вручную (`region`) — раскладка тогда не участвует.

---

## 4. Ограничения (честно)

* **Спрайты пересекают границу региона.** Сциссора в проходе нет, поэтому узел
  шире половины окна (или стоящий на самой границе) виден и в соседнем регионе.
  `split(2)` даёт регионы по 400 px, и узел 200 px при зуме 2 занимает ровно
  400 px и ложится точно; узел больше — заедет на соседнюю половину;
* **фон мира рисуется на весь экран от каждой камеры.** Он закрывает предыдущий
  регион — порядок «камера 1, затем камера 2» это скрывает, но прозрачный фон
  покажет наложение;
* **пост-обработка и свет — общие на кадр**, не на камеру: разные эффекты на
  разные регионы требуют проходов и целей и сознательно не поддерживаются;
* **узлы проходят N раз**: при четырёх камерах проход мира выполняется четырежды.
  Это плата за отказ от целей и сциссора; для больших сцен её стоит мерить
  (`$.gfx.stats()`);
* **мышь/касание привязаны к главной камере**: `$.input.mouseWorld()` считает по
  ней, а не по региону под курсором. Для второй камеры преобразуйте точку сами
  через `regionCamera`-математику или держите для неё отдельный ввод.

---

## 4.1. Картинка в картинке: что есть и чего нет

Камера ПОВЕРХ основного кадра технически возможна: регион задаётся своим
прямоугольником, `add(name, { rect })` его принимает, а прозрачность прохода
(`alpha`) и отказ от фона (`bg: false`) уже поддержаны в отрисовке — иначе
второй проход закрасил бы экран наглухо.

**Готового `$.camera.pip(...)` в API НЕТ.** Я его написал, замерил и снял:
проекция считается верно, а рисуется не то.

### Что измерено (узел 100×100 в мире `(1000, 1000)`, вторая камера получает
`x = 1060, y = 956, zoom = 5`, регион `{620, 20, 160, 120}`)

| Замер | Результат |
|---|---|
| `nodeTransform` (предсказание) | экран `(100, 520)`, размер `500×500` |
| **что реально ушло в `submitSprites`** | **`(100, 520, 500, 500)`** — совпадает с предсказанием |
| что на экране (скриншот) | зелёный прямоугольник `(100, 250)`, размер ≈ `350×350` |

То есть **на стороне JS всё сходится**: и математика кадра, и запись в буфер
спрайтов. Расхождение появляется между буфером и пикселем — то есть на пути
`submitSprites → GPU`. При этом:

* камера с регионом во **весь экран** (`zoom 1`) рисует точь-в-точь правильно;
* сплитскрин (`zoom 2`, регионы-половины) тоже рисует правильно и подтверждён
  тестом по пикселям;
* ошибка проявляется при **масштабе больше 1** в отдельном регионе.

### Где искать дальше

Смотреть `r2d_batch_sprites`/`r2d_render_draw_world` в [`src/render.c`](https://github.com/Nikide/russiano2d/blob/main/src/render.c):
спрайты второго прохода едут в общий батч, и что-то там применяет другой
масштаб/смещение (возможно, зажим координат или пересчёт относительно размера
окна, а не спрайта). Проверять так: положить в буфер один спрайт с координатами
`(100, 520, 500, 500)` и одним проходом — если он нарисуется как `(100, 250,
350, 350)`, дело точно в буфере/движке, а не в камерах. Инструмент для этого
уже есть: см. «что реально ушло в `submitSprites`» выше — тот же приём.

Незачем было объявлять метод, пока это не выяснено: непроверенная функция в
публичном API хуже её отсутствия.


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

```bash
python3 tests/agent/highlevel_viewports_test.py
```

Проверяется **по пикселям**: главная камера видит один узел, вторая — другой, и
каждый попадает в свой регион; камеры независимы; регион и зум задаются;
`split(4)` и возврат к одной камере работают; картинка одиночной камеры не
испортилась.
