Всю прошлую статью эталоном был луч, пущенный в сцену. Он отвечал на вопрос «что видно отсюда» из любой точки — с пола, со стены, из пробы, — а не только из камеры, и все приближения непрямого света мы сравнивали с ним: константу, буфер глубины, кубические карты, запечённые текстуры. В этой статье луч перестаёт быть эталоном для сравнения и становится способом рисовать.
Растеризация — это способ спроецировать сцену на экран. Быстрый, отлаженный за сорок лет, с конвейером, который мы прошли по частям: команда, пространства, треугольник в пиксели, текстура, свет, тень, стекло, пост-обработка. Но у проекции есть свойство, которое мы всё это время обходили: она знает только то, что попало на экран. Отражение показывает то, чего на экране нет. Тень — вопрос о видимости из лампы, а не из камеры. Непрямой свет — вопрос о видимости из точки на стене. Каждый раз растеризатору нечем было ответить, и каждый раз мы строили обходной путь: карту глубины со стороны света, марш по буферу, пробу, запечённую ночью.
Трассировка лучей отвечает на все эти вопросы одним и тем же действием. Из любой точки в любом направлении: что там? Дорого, шумно, долго было невозможно в реальном времени — и ровно поэтому вся дуга «Путь кадра» состоит из способов обойтись без этого вопроса.
Разберём:
- луч против растра: два способа ответить «что видно», одна и та же сцена, один и тот же ответ — и где ответы расходятся;
- пересечения и BVH: как найти треугольник на пути луча, не перебирая миллионы, — и почему это дерево оказалось в железе;
- трассировку путей: уравнение рендеринга, решённое случайными лучами; откуда шум и как убывает; выборка по косинусу и по лампе;
- шум: почему в реальном времени хватает одного пути на пиксель, если рядом стоит правильный фильтр, — размытие по нормалям и глубине, накопление, что делают нейросети;
- и итог дуги: список приёмов, которые мы разобрали за тринадцать сезонов, как список допущений — с областью применимости у каждого.
Такие разборы — с рабочим кодом — выходят в канале каждую неделю.
Луч против растра: два вопроса, один ответ
Вопрос «что видно из камеры» можно задать с двух сторон. Растеризатор задаёт его от геометрии: берёт треугольник и спрашивает, какие пиксели он накрывает; так для каждого треугольника, а спор за пиксель решает z-буфер. Трассировщик задаёт его от пикселя: проводит через пиксель луч из камеры и спрашивает, какой треугольник он встретит первым. Так для каждого пикселя.
Идея не новая. Артур Аппель в 1968 году описал расчёт видимости и теней лучами — ray casting, — когда до первых игр с текстурами оставалось четверть века. А в 1980-м Тёрнер Уиттед в статье «An Improved Illumination Model for Shaded Display» пустил лучи дальше первого пересечения: от точки попадания — луч к источнику (есть ли тень), отражённый луч (что в зеркале), преломлённый (что за стеклом), и так рекурсивно, деревом лучей. Тень, отражение и преломление получились одним механизмом, без отдельной техники под каждое. Рекурсивную трассировку до сих пор называют по его имени — Whitted-style.
Один и тот же кадр
Для первого пересечения оба способа обязаны дать один ответ. Треугольник, который накрыл пиксель и выиграл z-буфер, — это тот же треугольник, который луч через центр пикселя встретил первым; расходиться им негде, пока оба спрашивают про одну и ту же точку — центр пикселя. В демке ниже сцена из прошлой статьи собрана из треугольников — стены, ящик, шар из икосферы, лампа, — и отрисована двумя способами через одну и ту же функцию затенения точки.
Растр против луча: одна сцена, два вопроса
Разница — ноль пикселей: оба способа опрашивают центр пикселя, и ответ у них общий. Зато счётчики разные. Растеризатор обрабатывает каждый треугольник и каждый накрытый пиксель, включая перекрытые: детализируйте шар, и работа растёт вместе с числом треугольников. Трассировщик перебором проверял бы каждый луч с каждым треугольником — число, которое растёт как произведение; с деревом коробок, о котором следующая часть, тестов на луч остаётся несколько десятков при любой детализации.
Где ответы расходятся
Пока вопрос один — «что видно из камеры», — выигрывает растеризация: конвейер сделан ровно под него, треугольники обрабатываются пачками, пиксели закрашиваются квадами, память читается подряд. Луч на каждый пиксель — это миллионы независимых запросов в структуру данных, и соседние лучи после первого отскока разлетаются в разные стороны памяти.
Но есть нюанс. Вопросы бывают и другие. Есть ли тень в этой точке — вопрос о видимости из лампы. Что отражается в этом полу — вопрос о видимости из точки пола в отражённом направлении. Сколько света пришло в угол — вопрос о видимости из угла во все стороны. Для растеризатора каждый такой вопрос — новый рендер сцены с другой точки или обходной путь по уже нарисованному: карта глубины со стороны света, марш по буферу, пробы. Для трассировщика это тот же самый запрос с другой точки и другим направлением. Ничего нового строить не надо.
Собственно, в этом и вся разница. Растеризация отвечает на один вопрос очень быстро. Трассировка отвечает на любой вопрос одинаково — и одинаково дорого. Поэтому в играх они живут вместе: первое пересечение рисует растеризатор, а лучи пускают там, где нужен другой вопрос — за отражением, за тенью, за непрямым светом. Так был устроен и первый игровой рендер с аппаратной трассировкой: в Battlefield V в ноябре 2018-го лучами считались отражения, остальное — как раньше.
Пересечения и BVH: не перебирать
Чтож, вопрос «какой треугольник встретит луч» состоит из двух: как проверить один треугольник и как не проверять их все.
Луч и треугольник
Луч — точка и направление: . Треугольник — вершина и два ребра: , где точка внутри, если , и . Пересечение — три уравнения на три неизвестных . Томас Мёллер и Бен Трамбор в 1997 году записали их решение так, чтобы не хранить и не считать уравнение плоскости треугольника: сразу смена базиса, пара векторных произведений, и на выходе расстояние вдоль луча и барицентрические координаты — те же, что растеризатор получает из edge-функций. Барицентрики нужны не только для проверки «внутри»: по ним интерполируются нормали и текстурные координаты в точке попадания.
Möller–TrumboreОсторожно! Математика!
Приравниваем точку луча точке треугольника:
Система 3×3 решается по правилу Крамера, а определители записываются через смешанные произведения. С обозначениями , , :
Знаменатель близок к нулю, когда луч параллелен плоскости — такой треугольник пропускается. Проверки и делаются по ходу, до вычисления : большинство треугольников отсеивается на первой же.
Один тест дёшев — пара десятков умножений. Проблема в количестве. Сцена из миллиона треугольников, кадр из двух миллионов пикселей, по лучу на пиксель: два триллиона тестов на кадр только для первого пересечения. Перебор не годится в принципе, и с этого начинается вся практическая трассировка.
Коробки
Мысль простая: если луч прошёл мимо коробки, он прошёл мимо всего, что в ней. Проверить коробку — дёшево: метод плит, три пары плоскостей, интервал параметра , на котором луч внутри. Тимоти Кей и Джим Каджия в 1986-м, в статье «Ray Tracing Complex Scenes», предложили и такие ограничивающие объёмы из плит, и главное — их иерархию.
Bounding Volume Hierarchy — дерево, в каждом узле которого коробка, накрывающая всё содержимое поддерева; в листьях — по нескольку примитивов. устроено как матрёшка: коробка всей сцены, внутри — две коробки половин, внутри каждой — ещё по две, и так до листьев с несколькими треугольниками. Луч спускается по дереву: коробку мимо — поддерево пропускается целиком, коробку насквозь — проверяются обе дочерние. До треугольников доходит только там, где луч действительно проходит рядом, и число тестов растёт с размером сцены не линейно, а примерно как логарифм: удвоили сцену — добавили один уровень дерева.
Как делить сцену на половины — отдельная наука. Медиана по самой длинной стороне работает, но не лучшим образом: хочется, чтобы коробки были маленькими и редко пересекались. Голдсмит и Салмон в 1987-м предложили оценивать качество разбиения через площадь поверхности коробок — вероятность, что случайный луч попадёт в коробку, пропорциональна её площади, — а Макдональд и Бут в 1990-м оформили это в эвристику, которую строители деревьев используют до сих пор: surface area heuristic, SAH.
BVH на плоскости: мимо коробки — мимо всего
Луч из центра, отрезки вместо треугольников — плоская модель того же дерева. При переборе ржавым подсвечены все отрезки: каждый проверен. С деревом ржавых — единицы, а teal-коробки показывают, куда луч вошёл; прибавьте отрезков втрое, и число тестов почти не изменится. Это и есть логарифм на практике.
Дерево в железе
Именно эта пара операций — луч против коробки и луч против треугольника, плюс обход дерева — и попала в аппаратные блоки трассировки: RT-ядра в видеокартах NVIDIA начиная с архитектуры Turing 2018 года считают их отдельно от шейдерных ядер. Шейдер формулирует запрос — точка, направление — и получает ответ; спуск по BVH происходит в железе. Программный интерфейс к этому появился в марте 2018-го: DirectX Raytracing, DXR, объявленный на GDC; аппаратура — в сентябре того же года, с выходом GeForce RTX 20-й серии.
Но есть нюанс, и он про динамику. Дерево строится по геометрии — а геометрия в игре двигается. Персонаж согнул руку — все треугольники руки переехали, и их коробки в дереве больше не накрывают их. Дерево либо перестраивают, либо подгоняют коробки под новые позиции, не меняя структуры (refit), — второе дешевле, но дерево постепенно портится: коробки растут, пересекаются, и лучей в тестах становится больше. Растеризатору всё это не нужно: он просто рисует новые треугольники.
Трассировка путей: интеграл случайными лучами
Луч умеет отвечать «что видно» из любой точки. Дерево делает ответ дешёвым. Осталось задать правильные вопросы — и здесь возвращается уравнение рендеринга. Свет, уходящий из точки, — интеграл по всем направлениям от света, приходящего в неё, а приходящий свет — уходящий из других точек. Уиттед пускал из точки два-три луча по правилам: к лампе, зеркально, преломлённо. Каджия в 1986-м предложил другое: пускать лучи случайно и усреднять.
Монте-Карло
Интеграл можно оценить, не вычисляя его: взять случайное направление, посчитать, что пришло оттуда, и умножить на «сколько таких направлений». Одна оценка — грубая, но правильная в среднем. Много оценок — среднее сходится к интегралу. Путь из камеры: первое пересечение, оттуда случайный отскок, второе пересечение, ещё отскок, и так до лампы или до обрыва; яркость, принесённая путём, умножается на альбедо каждой поверхности по дороге. Пиксель — среднее по многим путям. Это Path tracing — метод Монте-Карло для уравнения рендеринга: яркость пикселя оценивается средним по случайным путям света из камеры в сцену., и она не упрощает ничего: любые материалы, любая геометрия, сколько угодно отскоков.
Цена — шум. Пока путей мало, соседние пиксели получают разные случайные ответы, и картинка зерниста. Шум убывает, но медленно: среднее из оценок имеет разброс в раз меньше одной. Вчетверо больше путей — вдвое меньше шума. В сто раз больше — только в десять. Это свойство метода, и обойти его нельзя; можно только уменьшить разброс одной оценки.
Выборка по важности
Разброс одной оценки зависит от того, куда мы пускаем лучи. Если пускать равномерно по полусфере, большинство лучей уходят туда, откуда света мало, а редкий луч, попавший в маленькую яркую лампу, приносит огромное значение — и вот оно, зерно. Идея выборки по важности: пускать лучи чаще туда, где интеграл больше, и компенсировать это меньшим весом.
Первый кандидат — косинус. В интеграле стоит множитель : свет под скользящим углом даёт мало. Если выбирать направления с плотностью, пропорциональной косинусу, множитель из веса исчезает, и разброс от него — тоже. Для непрямого света, размазанного по всей полусфере, это помогает. Для маленькой лампы — почти нет: вероятность попасть в неё всё равно мала, а попадание всё равно яркое.
Второй кандидат — сама лампа. Раз мы знаем, где она, не будем ждать, пока в неё попадёт случайный луч: в каждой точке пути пустим отдельный луч прямо к случайной точке на лампе и посчитаем прямой свет явно, с проверкой видимости; случайные отскоки оставим для непрямого. Это Next event estimation — явная выборка источника света в каждой точке пути вместо ожидания случайного попадания в него., и она снижает шум в разы при том же числе путей: ровно так был устроен прямой свет в наших демках весь прошлый сезон.
Почему косинус не спасает от маленькой лампыОсторожно! Математика!
Пусть лампа занимает малый телесный угол под углом к нормали, яркость , поверхность матовая с альбедо . Оценка одним лучом — редкое событие «попали» с вкладом , где — плотность выборки, поэтому разброс определяется произведением вероятности попадания на квадрат вклада.
Равномерная выборка: , вероятность попасть , вклад :
Косинусная: , вероятность , вклад :
Отношение : косинус выигрывает, только пока лампа ближе чем на 60° к нормали, а для стены, освещённой лампой сбоку под скользящим углом, равномерная выборка даже лучше. Выборка лампы убирает редкое событие вовсе — луч к лампе идёт всегда, и разброс остаётся только от видимости и от расстояния.
Третья ручка — длина пути. Обрывать путь на фиксированной глубине — значит терять свет, который пришёл бы дальше. Русская рулетка (Арво и Кирк, 1990) обрывает случайно: с некоторой вероятностью путь заканчивается, а выжившие пути получают вес побольше, чтобы среднее не сдвинулось. Средняя длина пути падает, ответ остаётся несмещённым.
Трассировка путей: откуда шум и как его меньше
Три способа выбирать направление, один и тот же средний свет. Смотрите на разброс одного сэмпла: у выборки лампы он в разы меньше, и шум кадра при том же числе сэмплов — тоже. Косинус против равномерной выборки в этой комнате — почти вничью: лампа маленькая, и её редкое попадание держит разброс в обоих случаях.
Из офлайна в кадр
Трассировка путей десятилетиями была методом кино: часы на кадр, сотни и тысячи путей на пиксель. В игру она пришла через железо из прошлой части и через одно наблюдение из следующей — что одного пути на пиксель хватит, если рядом правильный фильтр. Вехи короткие. Ноябрь 2018-го — Battlefield V, лучи для отражений поверх обычного растра. Июнь 2019-го — Quake II RTX от NVIDIA: игра 1997 года, освещённая целиком трассировкой путей; выросла она из Q2VKPT Кристофа Шида — того же автора, что у SVGF из следующей части. Апрель 2023-го — режим Overdrive в Cyberpunk 2077: трассировка путей в большом открытом мире современной игры, пока как технологический превью.
Шум: чем размыть, чтобы не размыть
Чтож, один путь на пиксель за кадр — это зернистая каша, в которой угадывается комната. Убрать зерно размытием — первое, что приходит в голову, и первое, что не работает: вместе с зерном уходят кромки, ящик расплывается в стену, тень — в пол. Шум и деталь для обычного фильтра неотличимы, оба — перепад между соседними пикселями.
Размывать только своих
Отличить их можно, если посмотреть не на цвет, а на буферы, которые у нас и так есть с первого пересечения: нормаль и глубина. Два соседних пикселя на одной стене имеют одну нормаль и близкую глубину — разница цвета между ними шум, их можно усреднять. Пиксель стены и пиксель ящика рядом — разные нормали, скачок глубины — кромка, усреднять нельзя. Фильтр взвешивает соседей по похожести нормалей и глубин, и размытие останавливается на кромках само.
Даммертц с соавторами в 2010-м предложили делать это À-trous («с дырками») — свёртка, у которой между отсчётами ядра пропущены пиксели: шаг 1, 2, 4, 8… Радиус растёт вдвое на каждой итерации, число чтений — нет.-вейвлетом: маленькое ядро пять на пять, которое на каждой итерации раздвигает свои отсчёты вдвое. Три итерации с шагами 1, 2 и 4 накрывают радиус в четырнадцать пикселей за 75 чтений на пиксель — обычное ядро такого радиуса стоило бы 841, — и на каждом шаге веса по нормалям и глубине. Семь лет спустя Шид с соавторами построили на этой основе SVGF — spatiotemporal variance-guided filtering: к пространственному фильтру добавилось накопление по кадрам, а сила размытия стала управляться оценкой разброса в каждом пикселе — где шума много, фильтр шире, где картинка уже сошлась, — уже. Заявка их статьи — стабильная последовательность кадров из одного пути на пиксель, и ровно с этой заявки началась трассировка путей в реальном времени.
Накопление и его цена
Вторая половина слова «spatiotemporal» — то же накопление по кадрам, что у TAA: история, репроекция по векторам движения, зажим. Десять кадров истории — это десять путей на пиксель почти бесплатно. И те же артефакты: призраки за движущимися объектами, запаздывание — свет включился, а тень ещё несколько кадров догоняет, — и мягкость там, где история размазала деталь.
Отдельная ветка — не фильтровать результат, а лучше выбирать сэмплы. ReSTIR Биттерли с соавторами (2020) переиспользует удачные выборки ламп между соседними пикселями и между кадрами: если сосед уже нашёл источник, который сюда светит, зачем искать заново. По оценке авторов, при равной ошибке это от 6 до 60 раз быстрее прежних методов — и выигрыш тем больше, чем больше в сцене источников.
И наконец нейросети. Фильтр по нормалям и глубине — это набор правил, которые человек придумал и настроил; нейросеть учится тем же правилам по парам «шумный кадр — чистый», и, судя по тому, что делает индустрия, учится лучше. В сентябре 2023-го NVIDIA заменила в DLSS 3.5 набор ручных шумодавов одной обученной моделью — Ray Reconstruction, — и в тех же Cyberpunk и Alan Wake 2 трассированный свет стал чище при том же числе лучей.
Шум и деноизинг: чем размыть, чтобы не размыть
Слева от счётчиков — то, что видит фильтр: один путь на пиксель. Размытие по Гауссу снимает зерно и кромки вместе, и ошибка к эталону может даже вырасти. Фильтр по нормалям и глубине усредняет только соседей на той же поверхности: кромки ящика и тени целы, зерно внутри стен ушло. Добавьте итераций — радиус растёт вдвое на каждой, и остаточный шум крупных пятен уходит тоже.
Выводы: тринадцать сезонов как список допущений
Если оставить от статьи одну мысль, пусть будет такая: растеризация — это быстрый способ спроецировать сцену, а не увидеть её. Она отвечает на один вопрос — что видно из камеры — и всё, что мы строили тринадцать сезонов поверх неё, было ответами на другие вопросы подручными средствами. Трассировка задаёт любой вопрос напрямую и платит за это шумом; фильтр приближает уже ответ, а не вопрос.
С этой высоты дуга «Путь кадра» читается как одна таблица. В каждом сезоне был приём, у приёма — допущение, у допущения — область, где оно держится:
- RGB — три числа вместо спектра; держится, пока свет не начинает интерферировать и раскладываться в радугу;
- 8 бит и гамма — 256 уровней, распределённых по восприятию; держится, пока математику не начали делать в закодированных значениях;
- draw call и конвейер — команда дорога сама по себе; держится, пока объектов сотни, а не тысячи с разными состояниями;
- проекция — точка перемножается с матрицей и делится на w; держится, пока вопрос про камеру;
- растеризация — покрытие решается по центру пикселя; держится, пока края не движутся и не мельче пикселя;
- текстура — цвет между текселями придумывает фильтр; держится, пока частота деталей ниже частоты пикселей, а мипы посчитаны;
- N·L и блик — свет как функция угла; держится, пока не спрашивают, куда ушёл остальной свет;
- микрофасеты — поверхность как статистика наклонов; держится, пока Roughness не упирается в ноль и алиасинг;
- карта теней — видимость из лампы через сравнение глубин; держится, пока карта достаточно мелкая, а запас достаточно точный;
- альфа — доля пикселя вместо толщины; держится, пока слои упорядочены и не пересекаются;
- пост-обработка — кадр как данные; держится, пока эффект не спрашивает о том, чего в кадре нет;
- ambient, AO, пробы, лайтмапы — непрямой свет константой, геометрией, снимком и заранее; держится, пока сцена похожа на ту, что была при запекании;
- луч — прямой ответ на любой вопрос; держится ровно столько, сколько путей вы можете себе позволить, и дальше вступает фильтр.
Ни один приём из списка не был обманом. Каждый — точное решение задачи, которую реально можно решить за шестнадцать миллисекунд, с ясно очерченной областью, где решение совпадает с правдой. Артефакт — это всегда одно и то же: приём применили там, где его допущение уже не выполняется, и не заметили. Разница между «подкрутил, стало лучше» и «знаю, что чиню» — в том, чтобы для каждого приёма помнить его допущение. Ровно этим ремесло графики и отличается от подбора параметров.
Чего мы не разобрали
Преломление и дисперсию. У Уиттеда преломлённый луч был с самого начала; стекло с показателем преломления, зависящим от длины волны, — то, с чего в первом сезоне начинался разговор о спектре, и трассировкой оно считается напрямую.
Среды. Туман, дым, кожа, воск — свет рассеивается внутри объёма, и путь ветвится внутри материала, а не на поверхности.
Двунаправленные методы. Пути от лампы навстречу путям от камеры и их склейка — способы искать редкие пути к свету в сценах, где обычная трассировка почти не сходится.
Устройство GPU-трассировщика. Как пути раскладывают по потокам, чтобы ядра не простаивали, и чем шейдерные стадии DXR отличаются от привычного конвейера.
Дуга «Путь кадра» пройдена: от спектра, который мы свернули в три числа, до луча, который эти числа собирает обратно из сцены. Дальше — прикладные темы поверх этого фундамента: анимация и скиннинг, частицы на GPU, вода, компьют-шейдеры, профилирование кадра — то, как измерить, где именно ваш кадр вышел за область применимости своих приёмов.
Надеюсь, статья была полезна. Заходите в телеграм-канал, такие разборы выходят там каждую неделю, и буду рад дополнениям в комментариях: у трассировки нюансов заметно больше, чем влезло в четыре части.
Разборы графики с кодом — каждую неделю в канале.
