Что такое path tracing
Как добавить реализма в path tracing
Здравствуйте, уважаемые хабровчане. Казалось бы: куда уже реальнее, но все же, есть у меня идея. По порядку.
Введение
Path tracing — это метод создания сцен виртуальной реальности, основан на оптике.
В трехмерном пространстве из источника света испускается очень много лучей (в идеале сколько же, сколько в реальности фотонов) и прослеживаются истории каждого луча. Когда луч встречается с преградой, возможно несколько событий: поглощение, отражение, преломление. Вероятность каждого из них зависит от материала преграды и цвета луча. Те лучи, которые попали в камеру — рисуются на экране. Вообще, подробнее это можно прочитать в википедии.
Добавлю только, что в оптике время изотропно, это означает, что оба направления времени равноправны. Получается, можно следить за лучами не от источника к глазу, а от глаза к источнику. Это практичнее, path tracing и так очень затратный, нельзя тратить время на трассировку лучей, в которых нет шанса быть нарисованными.
Со школьной физики известно, что это и есть причина дисперсии света. Дисперсия света возникает, когда коэффициент преломления зависит от частоты фотона. Получается что при старом подходе невозможно реализовать это самую дисперсию. Невозможно «по честному» нарисовать, например, бриллиант.
На псевдокоде это как-то так выглядит:
Здесь, думаю, все понятно. freq — частота волны света, screen_w, screen_h — ширина и высота экрана в пикселях, samples_num — количество выборок. Истинный цвет — среднее арифметическое по всех выборках, когда число выборок стремится к бесконечности. Векторы направлений фотонов лучше сохранять в таблице (table), что-бы не рассчитывать каждый раз. Рассчитываются они по несложной формуле, в которой участвуют еще и вектора указывающие на центр экрана, левую и верхнюю границу экрана. Например если соотношение сторон 4:3, то можно выбрать
Разумеется это все зависит от реализации.
Теперь о функции trace, в ней и происходит «магия». Она принимает положение и направление луча, а также его частоту, мощность (power) должна быть равна нулю. Возвращает она новое положение и направление и мощность. Положение и направление нас не интересует и фигурирует только для того, что-бы функция trace могла вызываться рекуррентно.
Color — массив, размер которого совпадает с количеством градаций частот.
Конечно этот псевдокод не претендует ни на что. Можно например сохранять разные вероятности для преломления и зеркального отражения. Или еще как то усложнить модель. Главное здесь — вероятности и коэффициент преломления зависят от частоты и у каждого фотона она своя.
Я полистал картинки по запросу path tracing в гугле и не увидел дисперсию света. Поиск по запросу monochromatic path tracing не показал ничего интересного. Пересмотрел всю первую страницу, везде эти слова разделены другими и в самой статье нету моей идеи.
Конечно скорее всего это уже реализовано и описано в книгах, до которых я еще не дотянулся.
Трассировка лучей и трассировка путей — что это такое и в чем различия
С прошлого года Nvidia запустила новый тренд с использованием трассировки лучей для повышения реалистичности игр. Новости о лучах, видео о лучах, презентации с лучами — Nvidia настолько активно продвигала метод рендеринга, словно сама изобрела его, что не так.
Такой активный фон трассировки лучей завис в головах геймеров, что привело к неприятным конфузам, когда речь заходила об отдельной технологии трассировки путей. Многие из наших читателей неоднократно полагали, что мы ошибаемся, упоминая трассировку путей в новостях об играх с технологией. Однако трассировка лучей и трассировка путей — не одно и то же. Поэтому сейчас мы постараемся доступно объяснить, в чем суть.
Все новые трейлеры Nvidia с трассировкой лучей в играх
Трассировка Лучей
Трассировка лучей — это общий термин, который включает все методы просчета движения, отражения и преломления лучей света, исходящих от камеры и прослеживающих путь к источнику света для создания физически корректной картинки.
Суть технологии заключается в том, что вместо просчета всего света в сценах, движку необходимо рассчитывать только те лучи, которые будут попадать в камеру, так как сцену мы видим только с этой позиции. Поэтому лучи изначально генерируются из самой камеры, «отскакивая» и преломляясь от объектов, пока не находят источник света, что позволяет значительно повысить эффективность визуализации.
Технология трассировки лучей для визуализации впервые была представлена Тернером Уиттедом в работе 1979 года под названием «Улучшенная модель освещения для затененного отображения». В своей публикации исследователь отмечает, что его идея применения трассировки в графике основана на работах таких людей, как Роберт Голдстин, который использовал трассировку лучей для расчета радиационного воздействия в танках. Также он упоминает Артура Эппла, который в 1968 году описал технику прослеживания лучей между поверхностью и источником света для создания освещения с тенями — эту работу нередко упоминают, как первое применение трассировки лучей в рендеринге.
В то же время публикация Уиттеда включала не только трассировку лучей для генерирования теней, но и для вторичного влияния света от отражений и преломлений. Такой метод значительно повысил реализм, но был лишен вторичного рассеянного освещения или так называемого «глобального освещения».
В 1984 году была опубликована работа Роберта Кука «Распределенная трассировка лучей», в которой была открыта важная концепция случайного распределения лучей во времени и пространстве для достижения размытия в движении, полуглянцевых отражений, освещения по площади и глубины резкости. И все это при помощи тех же лучей, которые необходимо генерировать для работы со сглаживанием.
Проблема такого подхода в экспоненциальной природе трассировки. Можно запустить один луч и он вычислит путь к основному источнику света. Обычно это самый важный и самый значимый источник освещения в сцене. Однако для вторичных отражений необходимо запускать все больше и больше лучей. Начав с 1 луча, он становится еще 10 лучами, каждый из которых выделяет еще 10 лучей, получая уже сотню. Еще по 10 лучей и уже 1000, еще 10 и вот 10 тысяч — процесс может продолжаться бесконечно. Сложность в том, что уже на третьем «отскоке» луча от объектов необходимо использовать в 1000 раз больше производительность, даже если этот «проход» влияет на 1/100 от качества финального освещения. Другими словами, весь процесс трассировки лучей по своей сути крайне неэффективен, даже если он эффективен в сравнении с иными методами физического рендеринга. Несмотря на оптимизацию метода в работе Кука, экспоненциальная характеристика трассировки оставалась фундаментальной преградой для применения технологии.
Трассировка Путей
В 1986 году Джеймс Кажийя опубликовал «Уравнение рендеринга» и предложил новую, скоростную форму трассировки лучей, которую назвал «трассировка путей». Трассировка путей предоставляет решение проблемы экспоненциального роста количества лучей. Вместо постоянно расширяющегося древа путей, трассировка путей ведет себя подобно распределенной трассировке лучей, но выделяя лишь один луч на «отскок». Рандомизируя тип луча и направление на каждый «отскок», вместо экспоненциальной генерации лучей, значительно снижается время просчета и необходимую вычислительную мощность, позволяя работать со всеми источниками освещения, в том числе с глобальным освещением.
Таким образом, фактической разницы между трассировкой лучей и трассировкой путей нет. Это как сравнивать собаку и чихуахуа. Даже сам автор трассировки путей описывает технологию как «новый, ускоренный метод трассировки лучей».
В современных движках рендеринга с трассировкой лучей разработчики обнаружили, что трассировка путей включает свои недостатки. Особенно это касается поздних «отскоков», которые начинают требовать гораздо больше лучей для просчета. В связи с этим разработчики постепенно оптимизируют эффективность рендеринга с использованием традиционной ветвящейся системы трассировки.
Gamescom 2019: 48 минут геймплея Minecraft с трассировкой лучей
Что в итоге
Сейчас «трассировку путей» часто применяют ошибочно, ассоциируя с распределенной трассировкой лучей, чтобы отделить их использование в интегрированном глобальном освещении от таких решений как Radiosity и Photon Mapping, даже если технически это не трассировка путей.
Теперь вы знаете, что Трассировка Лучей — это общее название технологии рендеринга, которая прослеживает пути от камеры до источников света, тогда как трассировка путей — это одно из решений в рамках трассировки лучей, позволяющее оптимизировать процесс визуализации, снижая объем генерируемых лучей при каждом столкновении с объектом на пути к свету.
Трассировка пути
СОДЕРЖАНИЕ
История [ править ]
Уравнение рендеринга и его использование в компьютерной графике было представлено Джеймсом Каджией в 1986 году. [1] Трассировка пути была представлена тогда как алгоритм для поиска численного решения интеграла уравнения рендеринга. Десять лет спустя Лафортюн предложил множество усовершенствований, включая двунаправленную трассировку пути. [2]
Описание [ править ]
Kajiya в уравнении рендеринга придерживается три конкретных принципов оптики; Принцип глобального освещения, Принцип эквивалентности (отраженный свет эквивалентен испускаемому свету) и Принцип направления (отраженный свет и рассеянный свет имеют направление).
В реальном мире объекты и поверхности видны благодаря тому, что они отражают свет. Этот отраженный свет затем по очереди освещает другие объекты. Из этого простого наблюдения следуют два принципа.
I. Для данной внутренней сцены каждый объект в комнате должен освещать все остальные объекты.
II. Во-вторых, не следует делать различия между освещением, излучаемым источником света, и освещением, отраженным от поверхности.
III. Освещение, исходящее от поверхностей, должно рассеиваться в определенном направлении, которое является некоторой функцией входящего направления входящего освещения и исходящего направления, в котором производится выборка.
Уравнение Каджиа представляет собой полное резюме этих трех принципов, и трассировка пути, которая приближает решение уравнения, остается верной им при реализации. Существуют и другие принципы оптики, которые не являются фокусом уравнения Каджиа и поэтому часто сложно или неправильно моделируются алгоритмом. Трассировка пути затруднена оптическими явлениями, не содержащимися в трех принципах. Например,
Алгоритм [ править ]
Следующий псевдокод представляет собой процедуру для выполнения простой трассировки пути. Функция TracePath вычисляет одну выборку пикселя, в которой учитывается только путь сбора.
Двунаправленная трассировка пути [ править ]
Выборка интеграла может выполняться любым из следующих двух различных подходов:
Двунаправленная трассировка пути предоставляет алгоритм, который сочетает в себе два подхода и может давать меньшую дисперсию, чем любой метод по отдельности. Для каждого образца независимо отслеживаются два пути: один от источника света и один от камеры. Это дает набор возможных стратегий выборки, где каждая вершина одного пути может быть напрямую связана с каждой вершиной другого. Исходные алгоритмы Light Tracing и Backwards Path Tracing являются частными случаями этих стратегий. Для Light Tracing он соединяет вершины пути камеры непосредственно с первой вершиной пути света. Для отслеживания обратного пути, он соединяет вершины светового пути с первой вершиной пути камеры. Кроме того, есть несколько совершенно новых стратегий выборки, в которых промежуточные вершины связаны. Взвешивание всех этих стратегий выборки с использованием множественной выборки по важности создает новый сэмплер, который может сходиться быстрее, чем однонаправленная трассировка пути, даже несмотря на то, что для каждой выборки требуется больше работы. Это особенно хорошо работает для каустики или сцен, которые освещены в основном непрямым освещением.
Производительность [ править ]
Легкий транспорт Metropolis может дать изображение с меньшим шумом и меньшим количеством сэмплов. Этот алгоритм был создан для более быстрой сходимости в сценах, в которых свет должен проходить через необычные коридоры или небольшие отверстия, чтобы достичь той части сцены, которую просматривает камера. Он также показал себя многообещающим в правильной визуализации патологических ситуаций с помощью каустики. Вместо того, чтобы генерировать случайные пути, новые пути выборки создаются как небольшие изменения существующих. В этом смысле алгоритм «запоминает» успешные пути от источников света до камеры.
Функции распределения рассеяния [ править ]
Новый алгоритм трассировки пути для оптимизации работы GPU: Wavefront Path Tracing
Occupancy
Алгоритм трассировки пути на удивление прост и его можно описать всего в нескольких строках псевдокода:
Входящими данными является первичный луч, проходящий из камеры через пиксель экрана. Для этого луча мы определяем ближайшее пересечение с примитивом сцены. Если пересечений нет, то луч исчезает в пустоте. В противном случае, если луч дойдёт до источника света, то мы нашли путь прохождения света между источником и камерой. Если мы найдём что-то другое, то выполняем отражение и рекурсию, надеясь, что отражённый луч всё-таки найдёт источник освещения. Заметьте, что этот процесс напоминает (обратный) путь фотона, отражающегося от поверхностей сцены.
GPU предназначены для выполнения этой задачи в многопоточном режиме. Поначалу может показаться, что трассировка лучей идеально подходит для этого. Итак, мы используем OpenCL или CUDA для создания потока для пикселя, каждый поток выполняет алгоритм, который и в самом деле работает, как задумано, и при этом довольно быстр: достаточно взглянуть на несколько примеров с ShaderToy, чтобы понять насколько быстрой может быть трассировка лучей на GPU. Но как бы то ни было, вопрос в другом: действительно ли эти трассировщики лучей максимально быстры?
У этого алгоритма есть проблема. Первичный луч может найти источник света сразу же, или после одного случайного отражения, или спустя пятьдесят отражений. Программист для CPU заметит здесь потенциальное переполнение стека; программист для GPU должен увидеть проблему занятости (occupancy). Проблема вызвана условной хвостовой рекурсией: путь может завершиться на источнике света или продолжаться дальше. Перенесём это на множество потоков: часть потоков прекратится, а другая часть продолжит работу. Через несколько отражений у нас останется несколько потоков, которым нужно продолжать вычисления, а большинство потоков будет ждать завершения работы этих последних потоков. Понятие «занятость» — это мера части потоков GPU, выполняющих полезную работу.
Проблема занятости относится и к модели выполнения SIMT устройств GPU. Потоки упорядочены в группы, например в GPU Pascal (оборудование NVidia класса 10xx) 32 потока объединяются в warp. Потоки в warp имеют общий счётчик программы: они выполняются с фиксированным шагом, поэтому каждая инструкция программы выполняется 32 потоками одновременно. SIMT расшифровывается как single instruction multiple thread («одна инструкция, много потоков»), что хорошо описывает концепцию. Для процессора SIMT код с условиями представляет сложности. Это наглядно показано в официальной документации Volta:
Выполнение кода с условиями в SIMT.
Когда некое условие истинно для некоторых потоков в warp, ветви оператора if-сериализуются. Альтернативой подходу «все потоки выполняют одно и то же» является «некоторые потоки отключены». В блоке if-then-else средняя занятость warp будет равна 50%, если только у всех потоков не будет согласованности относительно условия.
К сожалению, код с условиями в трассировщике лучей встречается не так редко. Лучи теней испускаются, только если источник освещения не находится за точкой затенения, разные пути могут сталкиваться с разными материалами, интегрирование методом «русской рулетки» может уничтожить или оставить путь в живых, и так далее. Выясняется, что occupancy становится основным источником неэффективности, и без чрезвычайных мер её предотвратить не так уж просто.
Streaming Path Tracing
Алгоритм streaming path tracing (потоковой трассировки пути) разработан для устранения корневой причины проблемы занятости. Streaming path tracing разделяет алгоритм трассировки пути на четыре этапа:
Этап 1 («Сгенерировать») отвечает за генерацию первичных лучей. Это простое ядро, создающее начальные точки и направления лучей в количестве, равном количеству пикселей. Выходными данными этого этапа являются большой буфер лучей и счётчик, сообщающий следующему этапу количество лучей, которое нужно обработать. Для первичных лучей эта величина равна ширине экрана, умноженной на высоту экрана.
Этап 2 («Продлить») — это второе ядро. Оно выполняется только после того, как этап 1 будет завершён для всех пикселей. Ядро считывает буфер, сгенерированный на этапе 1, и пересекает каждый луч со сценой. Выходными данными этого этапа становится результат пересечения для каждого луча, сохраняемый в буфере.
Этап 3 («Затенить») выполняется после полного завершения этапа 2. Он получает результат пересечения с этапа 2 и вычисляет модель затенения для каждого пути. Эта операция может генерировать новые лучи или не создавать их, в зависимости от того, завершился ли путь. Пути, порождающие новый луч (путь «продлевается») записывает новый луч («отрезок пути») в буфер. Пути, непосредственно сэмплирующие источники освещения («явное сэмплирование освещения» или «вычисление следующего события») записывают луч тени во второй буфер.
Этап 4 («Соединить») трассирует лучи тени, сгенерированные на этапе 3. Это похоже на этап 2, но с важным отличием: лучам тени достаточно найти любое пересечение, в то время как продлевающим лучам нужно было найти ближайшее пересечение. Поэтому для этого создано отдельное ядро.
После завершения этапа 4 у нас получится буфер, содержащий лучи, продлевающие путь. Взяв эти лучи, мы переходим к этапу 2. Продолжаем делать это, пока не останется продлевающих лучей или пока не достигнем максимального количества итераций.
Источники неэффективности
Озабоченный производительностью программист увидит в такой схеме алгоритмов streaming path tracing множество опасных моментов:
Также беспокойство вызывает масштабный ввод-вывод буферов. Это и в самом деле сложность, но не такая серьёзная, как можно ожидать: доступ к данным сильно предсказуем, особенно при записи в буферы, поэтому задержка не вызывает проблем. На самом деле, для такого типа обработки данных и были в первую очередь разработаны GPU.
Ещё один аспект, с которым GPU очень хорошо справляются — это атомарные счётчики, что довольно неожиданно для программистов, работающих в мире CPU. Для z-буфера требуется быстрый доступ, и поэтому реализация атомарных счётчиков в современных GPU чрезвычайно эффективна. На практике атомарная операция записи столь же затратна, как некэшированная запись в глобальную память. Во многих случаях задержка будет замаскирована масштабным параллельным выполнением в GPU.
Остаются два вопроса: вызовы ядер и двухсторонняя передача данных для счётчиков. Последний и в самом деле представляет собой проблему, поэтому нам необходимо ещё одно архитектурное изменение: постоянные потоки (persistent threads).
Последствия
Прежде чем углубляться в подробности, мы рассмотрим последствия использования алгоритма wavefront path tracing. Во-первых, скажем про буферы. Нам нужен буфер для вывода данных этапа 1, т.е. первичных лучей. Для каждого луча нам необходимы:
64 МБ. Также нам понадобится буфер для результатов пересечения, созданных ядром «Продлить». Это ещё по 128 бит на элемент, то есть 32 МБ. Далее, ядро «Затенить» может создать до 1920×1080 продлений пути (верхний предел), и мы не можем записывать их в буфер, из которого считываем. То есть ещё 64 МБ. И, наконец, если наш трассировщик пути испускает лучи тени, то это ещё один буфер на 64 МБ. Просуммировав всё, мы получаем 224 МБ данных, и это только для алгоритма фронта волны. Или около 1 ГБ при разрешении 4K.
Здесь нам нужно привыкнуть к ещё одной особенности: памяти у нас в избытке. Может показаться. что 1 ГБ — это много, и существуют способы снижения этого числа, но если подойти к этому реалистично, то к тому времени, когда нам действительно понадобится трассировать пути в 4K, использование 1 ГБ на GPU с 8 ГБ будет меньшей из наших проблем.
Более серьёзной, чем требования к памяти, проблемой станут последствия для алгоритма рендеринга. Пока я предполагал, что нам нужно генерировать один продлевающий луч и, возможно, по одному лучу тени для каждого потока в ядре «Затенить». Но что если мы хотим выполнить Ambient Occlusion, использовав 16 лучей на пиксель? 16 лучей AO нужно хранить в буфере, но, что ещё хуже, они появятся только в следующей итерации. Похожая проблема возникает при трассировке лучей в стиле Уиттеда: испускание луча тени для нескольких источников освещения или разделение луча при столкновении со стеклом реализовать почти невозможно.
С другой стороны, wavefront path tracing решает проблемы, которые мы перечислили в разделе «Occupancy»:
Кроме того, имеется и дополнительное преимущество, которое не стоит недооценивать. Код в четырёх отдельных этапах изолирован. Каждое ядро может использовать все доступные ресурсы GPU (кэш, общую память, регистры) без учёта других ядер. Это может позволить GPU выполнять код пересечения со сценой в большем количестве потоков, потому что этот код не требует такого большого количества регистров, как код затенения. Чем больше потоков, тем лучше можно спрятать задержки.
Полная занятость, улучшенная маскировка задержек, потоковая запись: все эти преимущества непосредственно связаны с возникновением и природой платформы GPU. Для GPU алгоритм wavefront path tracing очень естественен.
Стоит ли оно того?
Разумеется, у нас возникает вопрос: оправдывает ли оптимизированная занятость ввод-вывод из буферов и затраты на вызов дополнительных ядер?
Ответ положительный, однако доказать это не так легко.
Если мы на секунду вернёмся к трассировщикам пути с ShaderToy, то увидим, что в большинстве из них используется простая и жёстко заданная сцена. Замена её на полномасштабную сцену — нетривиальная задача: для миллионов примитивов пересечение луча и сцены становится сложной проблемой, решение которой часто оставляют для NVidia (Optix), AMD( Radeon-Rays) или Intel (Embree). Ни один из этих вариантов не сможет легко заменить жёстко прописанную сцену в искусственном трассировщике лучей CUDA. В CUDA ближайший аналог (Optix) требует контроля над выполнением программы. Embree в CPU позволяет трассировать отдельные лучи из вашего собственного кода, но ценой этого становятся значительные затраты производительности: он предпочитает трассировать вместо отдельных лучей большие группы лучей.
Экран из «It’s About Time», отрендеренного с помощью Brigade 1.
Будет ли wavefront path tracing быстрее своей альтернативы («мегаядра» (megakernel), как его называют Лейн с коллегами), зависит от времени, проводимого в ядрах (большие сцены и затратные шейдеры снижают относительное превышение затрат алгоритмом фронта волны), от максимального длины пути, занятости мегаядра и разницы в нагрузке на регистры на четырёх этапах. В ранней версии оригинального трассировщика пути Brigade мы обнаружили, что даже простая сцена со смешением отражающих и ламбертовых поверхностей, запущенная на GTX480, выигрывала от использования wavefront.
Streaming Path Tracing в Lighthouse 2
В платформе Lighthouse 2 есть два трассировщика пути wavefront path tracing. Первый использует для реализации этапов 2 и 4 Optix Prime (этапы пересечения лучей и сцены); во второй для реализации той функциональности используется непосредственно Optix.
Optix Prime — это упрощённая версия Optix, которая только занимается пересечением набора лучей со сценой, состоящей из треугольников. В отличие от полной библиотеки Optix, она не поддерживает пользовательского кода пересечений, и пересекает только треугольники. Однако для wavefront path tracer именно это и требуется.
Некоторые подробности реализации optixprime_b стоят упоминания. Во-первых, лучи тени трассируются вне цикла фронта волны. Это правильно: луч тени влияет на пиксель, только если он не перекрыт, но во всех остальных случаях его результат больше нигде не нужен. То есть луч тени является одноразовым, его можно трассировать в любой момент и в любом порядке. В нашем случае мы используем это, группируя лучи тени, чтобы окончательно трассируемый батч был как можно бОльшим. У этого есть одно неприятное последствие: при N итерациях алгоритма фронта волны и X первичных лучах верхняя граница количества лучей тени равна XN.
Ещё одна деталь — это обработка различных счётчиков. Этапы «Продлить» и «Затенить» должны знать, сколько путей активно. Счётчики для этого обновляются в GPU (атомарно), а значит и используются в GPU, даже без возврата в CPU. К сожалению, в одном из случаев это невозможно: библиотеке Optix Prime необходимо знать количество трассируемых лучей. Для этого нам раз в итерацию нужно возвращать информацию счётчиков.