# Z-буфер и псевдо-3D — `$.gfx.depth`

Глубина в движке нужна псевдо-3D: меш персонажа пишет настоящий z, а спрайты
сцены проверяются по нему — поэтому плоский спрайт не рисуется поверх
выпуклости, а выпуклость не «уезжает» под фон.

```js
$.gfx.depth();             // true — тест глубины включён (по умолчанию)
$.gfx.depth(false);        // выключить: прежнее поведение, порядок отрисовки
$.gfx.depth(true);         // вернуть
$.debug.render().depth;    // { enabled, texture, pipeline, … } — факты z-буфера
```

---

## 1. Как это сделано

* **Текстура глубины** формата `D32_FLOAT` создаётся под размер кадра один раз
  и пересоздаётся при смене размера (`r2d_render_depth_target`). Она
  подключается к проходу сцены целью глубины с очисткой в `1.0` — дальняя
  плоскость, ближе значит меньше.
* **Тест глубины** включён в конвейерах (`LESS_OR_EQUAL`), запись включена.
* **Спрайты пишут z = 0** (их вершинный шейдер не изменился). Это главное
  свойство: между спрайтами порядок отрисовки сохраняется, и вся прежняя
  отрисовка выглядит ровно как раньше — это проверяется тестом кадра.
* **Меш пишет свою глубину**: у него свой формат вершины (позиция `float3` +
  `uv` + цвет) и свой вершинный шейдер, а рисуется он **первым** в проходе
  сцены, чтобы успеть записать z до спрайтов.

## 2. Зачем сначала меш

Меш рисуется первым, спрайты после. Тогда спрайт с `z = 0` проходит тест только
там, где меш не записал меньшую глубину, — так часть меша перекрывает спрайт,
а часть нет, без сортировки на стороне игры.

## 3. Выключение

`$.gfx.depth(false)` освобождает текстуру глубины и возвращает прежний путь
(чистая прозрачность по порядку). Это нужно интерфейсу и пост-обработке, где
порядок и так задан явно.

## 4. Меш псевдо-3D: работает

Вершины в меш уходят из подсистем (`$.mesh`, cels и части персонажа); сама
отрисовка — нативный вызов, доступный модулям движка:

```js
engine.submitMesh(new Float32Array([    // внутри src/highlevel/mesh.js
    300, 200, 0.5,  0, 0,  1, 0, 0,     // x, y, z, u, v, r, g, b
    500, 200, 0.5,  1, 0,  0, 1, 0,
    400, 400, 0.5,  0.5, 1, 0, 0, 1,
]));
```

8 float на вершину: `x`, `y` — **экранные** пиксели (камера на меш не влияет),
`z` — глубина `0..1`, `u`/`v` — текстурные координаты (`0..1`), `r`/`g`/`b` —
цвет **`0..255`** (как у `drawRect`; в доке раньше стояло «0..1» — врало, меш
выходил почти чёрным).

Третий аргумент `engine.submitMesh` — **id текстуры** (или ничего). С текстурой
`u`/`v` сэмплят её, а цвет вершин умножается: `255` — «как есть». Раньше меш
всегда биндил белую текстуру, поэтому `u`/`v` были мертвы и текстурированный
псевдо-3D был невозможен.
Треугольники собираются своим батчем, рисуются **первыми** в проходе сцены:
меш пишет глубину, спрайты потом по ней проверяются.

### Причина, по которой отрисовка была отключена

**В `r2d_render_draw_mesh` не вызывался `SDL_BindGPUIndexBuffer`.** Меш рисуется
первым в проходе сцены, а индексный буфер привязывают участки спрайтов и
треугольников — то есть **позже**. `SDL_DrawGPUIndexedPrimitives` уходил с
непривязанным индексным буфером, и Metal падал с SIGSEGV (`-11`).

Лечится одной привязкой в начале `r2d_render_draw_mesh`:

```c
SDL_GPUBufferBinding ib;
SDL_zero(ib);
ib.buffer = r->index_buffer;
if (!ib.buffer) return;
SDL_BindGPUIndexBuffer(pass, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
```

### Почему «пробы» не находили это раньше

В функции стоял **ранний `return` до кода отрисовки**. Поэтому «падает с
записью глубины» и «работает без записи» означали одно и то же — отрисовки не
было. Восемь проб из прошлых проходов были несостоятельны, и выводы из них
(таблица «ALWAYS работает, LESS падает», комментарий про `GREATER` в
`render.c`) убраны из кода и документации.

Отдельно: `$.gfx.depth(false)` «спасал» не потому, что дело в глубине, а потому
что `r2d_render_draw_mesh` начинается с `if (!r->depth_enabled) return;` — при
выключенном режиме меш просто не рисуется.

### Что проверено (tests/agent/highlevel_mesh_test.py)

| Проверка | Результат |
|---|---|
| квадрат `100..300 × 100..300` | нарисован ровно там, bbox совпадает с вершинами |
| ближний (`z = 0.2`) **первым**, дальний (`z = 0.8`) вторым | дальний **отсечён** — z-буфер работает, а не painter's algorithm |
| обратный порядок | результат тот же: z решает, порядок не важен |
| 100 треугольников (300 вершин), 10 кадров | без падения |

### Ограничение: спрайты всегда поверх меша

Спрайтовый вершинный шейдер пишет `z = 0` — «ближе всего». Поэтому **спрайт
перекрывает меш всегда**, каким бы близким меш ни был; z-буфер сортирует только
треугольники меша между собой. Чтобы спрайт мог оказаться ЗА выпуклостью
персонажа, спрайтам нужна своя глубина (например, из y-сортировки) — это
отдельная работа, и она не сделана.

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

* **меш ещё не рисуется на экране**: конвейер, формат вершины, заливка буфера и
  отрисовка написаны и вызываются, но проверка показала, что кадр не меняется.
  Инструмент отладки (`engine.depthInfo()`) и счётчики (`meshDraws`,
  `meshBatches`, `pending`) добавлены именно для этого и остаются в движке;
  довести меш — отдельная задача (§4 в `docs/TASKS.md`);
* **у спрайтов нет своей глубины**: они все пишут `z = 0`. Сортировать спрайты
  между собой по-прежнему нужно порядком отрисовки (`$.gfx.layer`);
* **нет трафарета**: формат только глубина;
* **нет глубины в пост-обработке и свечении**: их проходы идут без цели глубины;
* **нет чтения глубины из игры**: буфер не выгружается обратно, поэтому
  «найти ближайший объект» через него нельзя.

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

```bash
# глубина включена, управляется, не меняет вид спрайтовой сцены
python3 tests/agent/highlevel_depth_test.py
```
