Mesh shaders и Nanite
Механизм: от пер-вершины к виртуализации
Пер-вершинное горло классики
20 лет конвейер начинался с input assembler, гнавшего вершины по одной (индексные буферы помогали, но модель фундаментально пер-вершинная), а culling треугольников шёл после вершинной обработки — на массивной геометрии ты тратил vertex-работу на невидимые и пиксель-мелкие треугольники ещё до того, как узнавал, что они не нужны.
Mesh shaders — GPU сам решает, что обрабатывать
Mesh shaders выкинули жёсткий front-end и заменили его двумя compute-подобными стадиями:
- Task (amplification) шейдер — решает, сколько mesh-групп запустить, и куллит на грубом уровне (кластерами).
- Mesh-шейдер — работает как compute: группа потоков берёт один meshlet (64–128 вершин + его треугольники), куллит невидимые meshlet'ы/треугольники на месте и эмитит примитивы прямо в растеризатор — без input assembler, без geometry-шейдера, без пер-вершинного ассемблинга.
Это GPU-driven rendering: не CPU выдаёт по draw-call'у на объект (см. draw-call горло), а GPU сам решает, какую геометрию и с какой детализацией обрабатывать. Железо — Turing+ (RTX 20-серия), Xbox Series, PS5.
Nanite — виртуализированная геометрия
Nanite — это «mipmaps + виртуальная память, но для полигонов». Меш препроцессится в иерархию кластеров (meshlet'ов ≤128 треугольников) на многих уровнях детализации (кластерный DAG). В рантайме GPU per-pixel выбирает тот уровень кластера, что даёт ~1 треугольник на пиксель — не больше (лишняя детализация невидима), не меньше (не мылит). Точно как mipmaps выбирают разрешение текстуры под дистанцию. Ключевые трюки:
- Две фазы: visibility-проход растеризует ID кластеров (что видно в каждом пикселе), затем material-проход шейдит — как deferred, но для геометрии.
- Софтверный растеризатор для крошечных треугольников: аппаратный растеризатор тратит на треугольник целый квад 2×2, и для пиксель-мелких это чудовищно неэффективно — Nanite растеризует их в compute, вручную. Знаменитый трюк.
- Стриминг: подгружаются только видимые кластеры нужного уровня — как виртуальная память тянет только тронутые страницы.
Стоимость отвязана от исходника
Главное следствие. Цель ~1 треугольник на пиксель значит, что число отрисованных треугольников ограничено экраном, а не ассетом:
4K — это ~8.3 млн пикселей, значит и потолок отрисованной геометрии ~несколько миллионов треугольников независимо от исходника: ассет на 10 млн и на 100 млн полигонов рендерятся примерно за одну цену (обрезаны экранным разрешением). Ты платишь за видимую детализацию на экране, а не за сложность модели — это и есть виртуализация. Отсюда «конец ручных LOD»: раньше художник лепил LOD0/1/2/3 и движок их переключал (pop-in); Nanite делает LOD непрерывным и автоматическим per-pixel — киношный ассет кидаешь как есть.
Честные границы
Nanite не бесплатен: visibility-буфер и софт-растеризатор имеют накладные, и ниже порога плотности геометрии классический рендер дешевле; первые версии плохо дружили с прозрачностью/альфа-листвой, скиннинг-анимация приехали позже (UE5.5), нужен современный GPU. Это инструмент под плотную статичную геометрию, а не универсальный тумблер — знать его профиль пригодности важнее, чем включать везде.
🕹 Во что поиграть — и что заметить
Nanite виден по отсутствию pop-in: геометрия не «морфит» ступеньками при приближении.
Витрины Nanite: гигантские города и статуи из миллионов треугольников без единого видимого LOD-перехода. Подходишь вплотную — детализация просто есть, без ступенек и подмены меша.
🎮 Сделай: в Matrix Awakens (или UE5-игре) медленно подойди к детализированному объекту и ищи pop-in — момент, когда меш скачком становится детальнее. У Nanite его нет: LOD непрерывен per-pixel. Сравни с до-Nanite-игрой, где деревья/камни явно «переодеваются» при приближении.
Зашипленные UE5-игры на Nanite: плотная киношная геометрия окружений тянется без ручных LOD-цепочек и без pop-in даже на консолях.
🎮 Сделай: включи (если есть) Nanite-визуализацию в UE-игре/редакторе — оверлей покажет плотность треугольников/кластеров per-pixel. Заметь, что на далёких объектах треугольников меньше (крупный кластер), вблизи — ~пиксельные. Это выбор LOD в реальном времени, а не переключение заранее сделанных мешей.
Любая игра прошлого поколения: деревья, камни, персонажи вдали — низкополигональные и заметно «щёлкают» на более детальные при приближении. Это ручные LOD и их переключение — ровно проблема, которую Nanite убивает.
🎮 Сделай: в старой открытой игре иди к дереву/скале и лови ступенчатую подмену меша (LOD-pop). Затем оцени, сколько ручного труда художника — сделать 4 версии каждого ассета. Nanite виртуализирует этот труд, как mipmaps виртуализировали разрешение текстур.
Хардкор · архитектура Nanite: кластерный DAG, visibility-буфер, софт-растеризаторможно пропустить
Иерархия кластеров (DAG)
Меш офлайн бьётся на кластеры по ≤128 треугольников, а те рекурсивно упрощаются и группируются в родительские кластеры меньшей детализации — получается направленный ациклический граф уровней. В рантайме обход выбирает срез DAG, где ошибка проекции кластера ≤ ~пикселя: далеко берётся грубый родитель, близко — мелкие листья. Границы кластеров сшиваются заранее, чтобы не было щелей между разными LOD соседних кластеров (locked borders).
Visibility buffer вместо G-буфера
Проход 1 растеризует не цвет, а ID (кластер, треугольник) в экранный буфер — компактно. Проход 2 по этому буферу восстанавливает атрибуты и шейдит материал. Разделение «что видно» и «как покрасить» (как deferred) убирает overdraw-шейдинга по геометрии: материал считается ровно раз на видимый пиксель.
Почему софтверный растеризатор
Аппаратный растеризатор оптимизирован под треугольники в несколько пикселей: он всегда обрабатывает квады 2×2 (для производных текстур), поэтому на субпиксельном треугольнике 3 из 4 фрагментов — брак. При Nanite-плотности (~1 треугольник/пиксель) это катастрофа, поэтому мелкие треугольники Nanite растеризует сам, compute-шейдером (атомарные записи в visibility-буфер), а крупные отдаёт железу. Гибрид soft/hardware по размеру треугольника — ядро производительности Nanite.
Хардкор · модель mesh-шейдеров и GPU-driven cullingможно пропустить
Mesh shaders — это не «ещё стадия», а смена того, кто управляет геометрией.
Task → Mesh
Task-шейдер (amplification) запускается на группы работы и решает, сколько mesh-групп породить (может 0 — целиком закуллить ветку), передавая им payload. Mesh-шейдер — группа из десятков потоков кооперативно строит один meshlet: вершины в shared-памяти, per-primitive culling (backface, frustum, small-triangle), эмит компактного набора вершин+индексов прямо растеризатору. Нет отдельного input assembler и geometry-шейдера (который был медленным из-за неупорядоченного вывода).
GPU-driven против CPU-driven
Классика: CPU перебирает объекты, куллит, выдаёт draw-call'ы — CPU-bound на больших сценах (draw-call горло). GPU-driven: вся сцена — это буферы кластеров в GPU-памяти, а compute/task-шейдер сам куллит и формирует список работы (DrawIndirect), CPU почти не участвует. Это снимает пер-объектный CPU-оверхед и позволяет сцены с сотнями тысяч инстансов. Nanite — предельная форма: вся видимая геометрия обрабатывается несколькими indirect-диспатчами, а не миллионом draw-call'ов.
ML / AI (твой домен): «плати за нужную детализацию, не за исходник» = адаптивный/условный компьют: mixture-of-depths, early-exit и каскады (дешёвая модель первой, дорогая — только на трудных входах), coarse-to-fine инференс. GPU-driven culling (GPU сам решает свою работу) ⇄ data-dependent графы и MoE-роутинг (модель маршрутизирует свой компьют). «Отвязать стоимость от сложности исходника, ограничить выходом» ⇄ инференс, ограниченный бюджетом токенов/разрешением, а не размером модели. Софт-растеризатор-для-мелких-треугольников (железо неэффективно ниже порога → делаем в compute) ⇄ специализация горячего пути под размер (мелкие matmul → другой kernel). А сама виртуализация = паттерн, объединяющий виртуальную память, mipmaps, стриминг — и в ML это PagedAttention (vLLM) (KV-кэш страницами, как виртуальная память!) и gradient checkpointing (пересчёт vs хранение). Meshlet ⇄ patch/tile снова (тайлы → ViT-патчи).
Графика / стриминг: LOD, mipmaps, virtual texturing, потоковая загрузка по видимости — одна семья приёмов «детализация по требованию».
Системы: виртуальная память и пейджинг, ленивая загрузка, CDN-edge-кэш нужного разрешения — плати за тронутое, а не за всё.
Принцип: не храни и не считай в полной детализации то, что не будет видно/использовано в ней. Виртуализируй ресурс по уровням и трать пропорционально нужному выходу.
Что именно Nanite «виртуализирует»?
Зачем Nanite отдельный софтверный растеризатор для мелких треугольников?
Значит, с Nanite геометрия «бесплатна» — можно лить полигоны без счёта?
«Конец LOD» — это правда или маркетинг?
Почему пер-вершинная модель была узким местом, раз индексные буферы переиспользуют вершины?
- Brian Karis, «Nanite: A Deep Dive» (SIGGRAPH 2021) — первоисточник по кластерному DAG и софт-растеризатору.
- NVIDIA, «Mesh Shading» whitepaper + Vulkan/DX12 Ultimate mesh shader specs.
- UE5 Nanite docs (virtualized geometry, visualizations) — практика и границы применимости.
- «A Deep Dive into Nanite Virtualized Geometry» — разборы двухфазного рендера и LOD-выбора.
- Модуль 8, «Mesh shaders and the post-vertex pipeline» (
08-technical-deep-dives.md).