Основы трассировки лучей
Одна из самых интересных вещей в компьютерной графике (а возможно, и самая интересная) — отрисовка графики на экране. Чтобы как можно скорее приступить к ней, мы для начала внесём небольшие упрощения, чтобы уже
сейчас
вывести что-нибудь на экран. Разумеется, такие упрощения предполагают некоторые ограничения наших возможных действий, но в последующих главах мы шаг за шагом избавимся от этих ограничений.
Во-первых, мы будем считать, что точка обзора фиксирована. Точка обзора — это место, в котором располагается глаз в нашей аналогии, и оно обычно называется положением камеры; давайте назовём его
Во-вторых, мы будем считать, что ориентация камеры тоже фиксирована, то есть камера всегда направлена в одно и то же место. Будем считать, что она смотрит вниз по положительной оси Z, положительная ось Y направлена вверх, а положительная ось X — вправо:
Положение и ориентация камеры теперь фиксированы. Но у нас всё ещё нет «рамки» из предложенной нами аналогии, через которую мы смотрим на сцену. Будем считать, что рамка имеет размеры окном просмотра (viewport). В сущности, мы будем рисовать на холсте всё то, что видим через окно просмотра. Важно, что размер окна просмотра и расстояние до камеры определяют угол видимости из камеры, называемый областью видимости (field of view) или для краткости FOV. У людей FOV по горизонтали составляет почти
Давайте вернёмся к «алгоритму», представленному в предыдущем разделе, обозначим его шаги цифрами:
Разместить глаз и рамку в нужных местах (1) Для каждого пикселя холста Определить квадрат сетки, соответствующий этому пикселю (2) Определить цвет, видимый сквозь этот квадрат (3) Закрасить пиксель этим цветом (4)
Мы уже выполнили шаг 1 (или, если точнее, избавились от него на время). Шаг 4 тривиален (
canvas.PutPixel(x, y, color)
). Давайте вкратце рассмотрим шаг 2, а затем сосредоточимся на гораздо более сложных способах реализации шага 3.
Tracert – трассировка маршрута к заданному узлу.
Утилита трассировки маршрута до заданного узла TRACERT.EXE является одним из наиболее часто используемых инструментов сетевой диагностики. Основное ее назначение – получить цепочку узлов, через которые проходит IP-пакет, адресованный конечному узлу, имя или IP-адрес которого задается параметром командной строки.
Формат командной строки:
tracert [-d] [-h максЧисло] [-j списокУзлов] [-w таймаут] [-R] [-S адресИсточника] [-4] [-6] конечноеИмя
Параметры командной строки:
-d – не использовать разрешение в имена узлов.
-h максЧисло – максимальное число прыжков при поиске узла.
-j списокУзлов – свободный выбор маршрута по списку узлов (только IPv4).
-w таймаут – таймаут каждого ответа в миллисекундах.
-R – трассировка пути (только IPv6).
-S адресИсточника – использовать указанный адрес источника (только IPv6).
-4 – принудительное использование IPv4.
-6 – принудительное использование IPv6.
В основе трассировки заложен метод анализа ответов при последовательной отправке ICMP-пакетов на указанный адрес с увеличивающимся на 1 полем TTL. (“Время жизни” –
Time To Live). На самом деле это поле не имеет отношения к времени, а является счетчиком числа возможных переходов при передаче маршрутизируемого пакета.
Каждый маршрутизатор, получив пакет, вычитает из этого поля, сохраняемого в заголовке пакета, единицу и проверяет полученное значение счетчика TTL. Если значение стало равным нулю, такой пакет
отбрасывается и отправителю посылается ICMP-сообщение о превышении времени
жизни (сообщение “Time Exceeded”, значение 0x11 в заголовке ICMP).
Если бы не было предусмотрено включение поля TTL в IP пакетах, то при ошибках в маршрутах, могла бы возникнуть ситуация, когда пакет будет вечно циркулировать в сети, пересылаемый маршрутизаторами по кругу.
При выполнении команды tracert.exe сначала выполняется отправка ICMP пакета с полем TTL в заголовке равным 1 и первый в цепочке маршрутизатор (обычно это основной шлюз из настроек сетевого подключения) вычтя единицу из TTL получает его нулевое значение и сообщает о превышении времени жизни. Таким образом, утилита TRACERT.EXE получает IP-адрес первого маршрутизатора, участвующего в доставке пакетов конечному узлу. Эта последовательность повторяется трижды,
поэтому в строке результата, формируемой tracert.exe, после номера перехода
отображаются три значения времени отклика:
1 1 ms <1 <1 192.168.1.1
1 – номер перехода (1 – первый маршрутизатор)
1 ms <1 <1 – время его ответа для 3-х попыток (1ms и 2 ответа менее чем 1 ms)
192.168.1.1 – его адрес (или имя)
Затем процедура повторяется, но
TTL устанавливается равным 2 – первый маршрутизатор его уменьшит до 1 и
отправит следующему в цепочке, который после вычитания 1 обнулит TTL и сообщит о превышении времени жизни. Утилита TRACERT.EXE получит второй IP-адрес узла, участвующего в доставке пакета получателю и его время ответа. Процесс трассировки будет продолжаться до тех пор, пока не будет достигнут конечный узел, имя или адрес которого заданы в качестве параметра командной строки, например , tracert yandex.ru , или до
обнаружения неисправности, не позволяющей доставить пакет. По умолчанию, утилита TRACERT.EXE использует счетчик максимального числа переходов равный 30, что должно быть достаточно для достижения любого узла на планете. При необходимости, иное значение счетчика можно задать с помощью параметра -h
Пример результатов выполнения tracert google.com
tracert google.com – трассировка маршрута к узлу google.com
Результат:
Трассировка маршрута к google.com [74.125.45.100] с максимальным числом прыжков 30:
1 1 ms <1 <1 192.168.1.1
2 498 ms 444 ms 302 ms ppp83-237-220-1.pppoe.mtu-net.ru [83.237.220.1]
3 * * * .
4 282 ms * * a197-crs-1-be1-53.msk.stream-internet.net [212.188.1.113]
5 518 ms 344 ms 382 ms ss-crs-1-be5.msk.stream-internet.net [195.34.59.105]
6 462 ms 440 ms 335 ms m9-cr01-po3.msk.stream-internet.net [195.34.53.85]
7 323 ms 389 ms 339 ms bor-cr01-po4.spb.stream-internet.net [195.34.53.126]
8 475 ms 302 ms 420 ms anc-cr01-po3.ff.stream-internet.net [195.34.53.102]
9 334 ms 408 ms 348 ms 74.125.50.57
10 451 ms 368 ms 524 ms 209.85.255.178
11 329 ms 542 ms 451 ms 209.85.250.140
12 616 ms 480 ms 645 ms 209.85.248.81
13 656 ms 549 ms 422 ms 216.239.43.192
14 378 ms 560 ms 534 ms 216.239.43.113
15 511 ms 566 ms 546 ms 209.85.251.9
16 543 ms 682 ms 523 ms 72.14.232.213
17 468 ms 557 ms 486 ms 209.85.253.141
18 593 ms 589 ms 575 ms yx-in-f100.google.com [74.125.45.100]
Трассировка завершена.
В результатах трассировки могут присутствовать строки, где вместо адреса
узла отображается звездочка (узел номер 3 в примере). Это не обязательно является признаком неисправности маршрутизатора, и чаще всего, говорит о том, что настройки данного узла запрещают отправку ICMP-сообщений по соображениям безопасности и уменьшения нагрузки на канал при в случае некоторых разновидностей DDoS-атак . Например, подобные настройки используются в сетях Microsoft . Серверы корпорации не отвечают на ping и не позволяют выполнить трассировку маршрута к ним.
tracert google.com – выполнить трассировку маршрута к узлу google.com.
tracert 8.8.8.8 – выполнить трассировку маршрута к узлу с IP-адресом 8.8.8.8
tracert -d yandex.ru – выполнить трассировку маршрута к узла yandex.ru без разрешения IP-адресов в имена узлов. Трассировка в таком режиме выполняется быстрее.
tracert -d -6 ipv6.google.com – выполнить трассировку с использованием протокола IPv6.
Пример результатов трассировки с использованием протокола IPv6:
trace to ipv6.google.com (2a00:1450:4013:c00::71), 30 hops max, 40 byte packets 1 2a02:348:82::1 (2a02:348:82::1) 8.087 ms 8.063 ms 8.086 ms 2 te0-22.cr1.nkf.as49685.net (2001:4cb8:40b:1::1d01) 2.143 ms 2.129 ms 2.103 ms 3 amsix-router.google.com (2001:7f8:1::a501:5169:1) 1.379 ms 1.415 ms 1.422 ms 4 (2001:4860::1:0:87ab) 1.437 ms (2001:4860::1:0:87aa) 2.157 ms (2001:4860::1:0:87ab) 1.408 ms 5 (2001:4860::8:0:87b0) 1.494 ms 1.469 ms (2001:4860::8:0:87b2) 8.350 ms 6 (2001:4860::8:0:b1b7) 5.364 ms 5.321 ms 4.748 ms 7 (2001:4860::2:0:8651) 4.653 ms 6.994 ms (2001:4860::2:0:8652) 13.926 ms 8 ee-in-x71.1e100.net (2a00:1450:4013:c00::71) 4.732 ms 4.733 ms 4.783 ms
Весь список команд CMD Windows
Гибридный подход для современности и будущего
В марте 2022 года Microsoft объявила о выпуске нового расширения API для Direct3D 12 под названием DXR (DirectX Raytracing). Это был новый графический конвейер, дополняющий стандартные конвейеры растеризации и вычислений. Дополнительная функциональность обеспечивалась добавлением шейдеров, структур данных и так далее, но не требовала аппаратной поддержки, кроме той, которая уже была необходима для Direct3D 12.
На той же Game Developers Conference, на которой
, Electronic Arts говорила о своём
— эксперименте с 3D-движком, использующим DXR. Компания показала, что трассировку лучей можно использовать, но не для рендеринга всего кадра. В основной части работы используются традиционные техники растеризации и вычислительных шейдеров, а DXR применяется в специфических областях. То есть количество генерируемых лучей намного меньше, чем оно было бы для целой сцены.
Такой гибридный подход использовался в прошлом, хотя и в меньшей степени. Например, в Wolfenstein 3D
для рендерига кадра, однако он выполнялся с одним лучом на столбец пикселей, а не на пиксель. Это всё равно может показаться впечатляющим, если только не вспоминать, что игра работала с разрешением 640 x 480 [прим. пер.: на самом деле 320 x 200], то есть одновременно испускалось не больше 640 лучей.
Графические карты начала 2022 года наподобие AMD Radeon RX 580 или Nvidia GeForce 1080 Ti удовлетворяли требованиям DXR, но даже при их вычислительных возможностях существовали опасения, что они будут недостаточно мощны для того, чтобы использование DXR имело смысл.
Ситуация изменилась в августе 2022 года, когда Nvidia выпустила свою новейшую архитектуру GPU под кодовым названием Turing. Важнейшей особенностью этого чипа стало появление так называемых RT Cores: отдельных логических блоков для ускорения вычислений пересечения луч-треугольник и прохождения иерархии ограничивающих объёмов (BVH).
Эти два процесса — затратные по времени процедуры для определения точек взаимодействия света с треугольниками, составляющими объекты сцены. С учётом того, что RT Cores были уникальными блоками процессора Turing, доступ к ним мог выполняться только через проприетарный API Nvidia.
Первой игрой с поддержкой этой функции стала Battlefield V компании EA. Когда мы протестировали в ней DXR, то были впечатлены улучшением отражений в воды, на траве и металлах, а также соответствующим снижением производительности:
Если честно, то последующие патчи улучшили ситуацию, но снижение скорости рендеринга кадров всё равно присутствовало (и до сих пор есть). К 2022 году появились некоторые другие игры, поддерживающие этот API и выполняющие трассировку лучей для отдельных частей кадра. Мы тестировали
, столкнувшись с той же ситуацией — при активном использовании DXR заметно снижает частоту кадров.
Примерно в то же время UL Benchmarks объявила о создании теста функций DXR для 3DMark:
DXR используется в графической карте Nvidia Titan X (Pascal) — да, в результате получается 8 fps
Однако исследование игр с поддержкой DXR и теста 3DMark показало, что трассировка лучей даже в 2022 году по-прежнему остаётся очень сложной задачей для графического процессора, даже по цене в 1000 с лишним долларов. Значит ли это, что у нас нет реальных альтернатив растеризации?
Прогрессивные функции в потребительских технологиях 3D-графики часто оказываются очень дорогими, а их изначальная поддержка новых возможностей API бывает довольно фрагментарной или медленной (как мы это выяснили при дестировании Max Payne 3 на разных версиях Direct3D в 2022 году).
Однако вершинные и пиксельные шейдеры, тесселяция, HDR-рендеринг и screen space ambient occlusion тоже когда-то были затратными техниками, подходящими только для мощных GPU, а теперь они являются стандартом для игр и поддерживаются множество графических карт.
Моделирование «зеркального» отражения
Помните, как ранее я упоминал о том, что не все модели основаны на физических моделях? Ну, вот один из примеров этого. Представленная ниже модель является произвольной, но её используют, потмоу что она проста в вычислении и хорошо выглядит.
Давайте возьмём
Но нам не хватает ещё одной детали. В такой формулировке все объекты блестят одинаково. Как изменить уравнение для получения различных степеней блеска?
Не забывайте, что этот блеск — мера того, насколько быстро функция отражения уменьшается при увеличении показателем отражения, и он является свойством поверхности. Поскольку модель не основана на физической реальности, значения
Подставляя полученные ранее выражения, мы получим
и немного упростив, получаем
Моделирование диффузного отражения
Итак, луч света с направлением
и яркостью
падает на поверхность с нормалью
. Какая часть
отражается обратно сцену как функция от
Поскольку технически луч света не имеет ширины, поэтому мы будем считать, что всё происходит на бесконечно малом плоском участке поверхности. Даже если это поверхность сферы, то рассматриваемая область настолько бесконечно мала, что она почти плоская относительно размера сферы, так же как Земля выглядит плоской при малых масштабах.
Луч света с шириной
что преобразуется в
Мы почти закончили.
— это угол между
, то есть
можно выразить как
И наконец
Итак, мы получили очень простое уравнение, связывающее отражённую часть света с углом между нормалью к поверхности и направлением света.
Заметьте, что при углах больше вычитающие свет. Это не имеет никакого физического смысла; угол больше
задней части поверхности, и не вносит свой вклад в освещение освещаемой точки. То есть если
Осваиваем растеризацию
Как и в случае с преобразованиями, мы рассмотрим правила и процессы, используемые для превращения окна просмотра в сетку пикселей, на примере Direct3D. Эта таблица напоминает электронную таблицу Excel со строками и столбцами, в которой каждая ячейка содержит различные значения данных (такие как цвет, значения глубины, координаты текстур и т.п.). Обычно эта сетка называется
растровым изображением (raster)
, а процесс её генерации —
растеризацией (rasterization)
. В статье
мы упрощённо рассматривали эту процедуру:
Изображение выше создаёт впечатление, что примитивы просто разрезаются на мелкие блоки, но на самом деле операций намного больше. Самый первый этап — это определение того, обращён ли примитив в сторону камеры — например, на показанном выше изображении с пирамидой усечения примитивы, из которых состоит задняя часть серого кролика, не будут видимыми. Поэтому хотя они присутствуют в окне просмотра, рендерить их не нужно.
Мы можем приблизительно представить, как это выглядит, посмотрев на схему ниже. Куб прошёл различные преобразования для помещения 3D-модели в 2D-пространство экрана и с точки зрения камеры часть граней куба не видна. Если мы считать, что все поверхности непрозрачны, тогда часть этих примитивов можно игнорировать.
Слева направо: мировое пространство > пространство камеры > пространство проецирования > экранное пространство
В Direct3D это можно реализовать, сообщив системе, каким будет состояние рендера, и эта инструкция даст ей понять, что нужно удалить (отсечь) стороны каждого примитива, смотрящие вперёд или назад (или не отсекать совсем, например, в каркасном (wireframe) режиме).
Но как она узнает, какие из сторон смотрят вперёд или назад? Когда мы рассматривали математику обработки вершин, то видели, что треугольники (или скорее вершины) имеют векторы нормалей, сообщающие системе, в какую сторону он смотрит. Благодаря этой информации можно выполнить простую проверку, и если примитив её не пройдёт, то он удаляется из цепочки рендеринга.
Теперь настало время применения пиксельной сетки. Это снова неожиданно сложный процесс, потому что система должна понять, находится ли пиксель внутри примитива — полностью, частично или вообще не внутри. Для этого выполняется процесс проверки покрытия (coverage testing). На рисунке ниже показано, как растеризируются треугольники в Direct3D 11:
Правило довольно простое: пиксель считается находящимся внутри треугольника, если центр пикселя проходит проверку, которую Microsoft называет
. «Верхний» относится к проверке горизонтальной линии; центр пикселя должен находиться
на
этой линии. «Левый» относится к негоризонтальным линиям, и центр пикселя должен находиться слева от такой линии. Существуют и другие правила, относящиеся к непримитивам, например, простым отрезкам и точкам, а при использовании
мультисэмплирования (multisampling)
в правилах появляются дополнительные условия if.
Если внимательно присмотреться к документации Microsoft, то можно увидеть, что создаваемые пикселями фигуры не очень похожи на исходные примитивы. Так происходит потому, что пиксели слишком велики для создания реалистичного треугольника — растровое изображение содержит недостаточно данных об исходных объектах, что вызывает явление под названием алиасинг (aliasing).
Давайте рассмотрим алиасинг на примере UL Benchmark 3DMark03:
Растеризация размером 720 x 480 пикселей
На первом изображении растровое изображение имеет очень низкое разрешение — 720 на 480 пикселей. Алиасинг чётко заметен на перилах и тени, отбрасываемой оружием верхнего солдата. Сравните это с результатом, получаемым при растеризации с увеличенным в 24 раза количеством пикселей:
Растеризация размером 3840 x 2160 пикселей
Здесь мы видим, что алиасинг на перилах и тени совершенно исчез. Похоже, что следует всегда использовать большое растровое изображение, но размеры сетки должны поддерживаться монитором, на котором будет отображаться кадр. А с учётом того, что все эти пиксели нужно обработать, очевидно, что возникнет снижение производительности.
Здесь может помочь мультисэмплирование. Вот как оно работает в Direct3D:
Вместо того, чтобы проверять соответствие центра пикселя правилам растеризации, проверяются несколько точек внутри каждого пикселя (называемых субпиксельными сэмплами или
субсэмплами
), и если какие-то из них удовлетворяют требованиям, то они образуют часть фигуры. Может показаться, что здесь нет никакой выгоды и алиасинг даже усиливается, но при использовании мультисэмплирования информация о том, какие субсэмплы покрыты примитивом, и результаты обработки пикселей сохраняются в буфер в памяти.
Этот буфер затем используется для смешения данных субсэмплов и пикселей таким образом, чтобы края примитива были менее рваными. Подробнее мы рассмотрим алиасинг в другой статье, но пока этой информации нам достаточно, чтобы понять, что может делать мультисэмплирование, когда используется для растеризации слишком малого количества пикселей:
Как видите, величина алиасинга на краях разных фигур значительно снизилась. Растеризация с большим разрешением определённо лучше, но снижение производительности может подтолкнуть вас с использованию мультисэмплирования.
Также в процессе растеризации выполняется проверка перекрытия (occlusion testing). Она необходима, потому что окно просмотра будет заполнено наложенными друг на друга примитивами — например, на рисунке выше смотрящие вперёд треугольники, составляющие солдата, стоящего на переднем плане, перекрывают те же треугольники другого солдата.
Однако если ближний примитив прозрачен, то дальний останется видимым, хотя и не пройдёт проверку перекрытия. Именно поэтому почти все 3D-движки выполняют проверки перекрытия до отправки данных в GPU и вместо этого создают нечто под названием z-буфер, являющийся частью процесса рендеринга.
Здесь кадр создаётся обычным образом, но вместо сохранения готовых цветов пикселей в памяти GPU сохраняет только значения глубин. Позже их можно использовать в шейдерах для проверки видимости и с большим контролем и точностью аспектов, касающихся перекрытия объектов.
На показанном выше изображении чем темнее цвет пикселя, тем ближе объект к камере. Кадр рендерится один раз для создания z-буфера, а затем рендерится снова, но на этот раз во время обработки пикселей запускается шейдер, проверяющий их на значения в z-буфере. Если он невидим, то цвет пикселя не записывается в буфер готового кадра.
Пока нашим основным последним этапом будет интерполяция атрибутов вершин — в исходной упрощённой схеме примитив был полным треугольником, но не забывайте, что окно просмотра заполняется только углами фигур, а не самими фигурами. То есть система должна определить, какие цвет, глубина и текстура примитива должны находиться между вершинами, и эта операция называется интерполяцией. Как вы уже догадались, это ещё одно вычисление, и оно не такое уж простое.
Несмотря на то, что растеризованный экран представлен в 2D, структуры внутри него представляют собой 3D-перспективу. Если бы линии действительно были двухмерными, то для вычисления цветов и прочего мы бы могли использовать простое линейное уравнение, потому что мы переходим от одной вершины к другой.
Итак, задача выполнена — так 3D-мир вершин превращается в 2D-сетку разноцветных блоков. Но мы ещё не совсем закончили.
Отражение
У нас появились блестящие объекты. Но можно ли создать объекты, которые на самом деле ведут себя как зеркала? Конечно, и на самом деле их реализация в трассировщике лучей очень проста, но поначалу может показаться запутанной.
Давайте посмотрим, как работают зеркала. Когда мы смотрим в зеркало, то видим лучи света, отражающиеся от зеркала. Лучи света отражаются симметрично относительно нормали поверхности:
Допустим, мы трассируем луч и ближайшим пересечением оказывается зеркало. Какой цвет имеет луч света? Очевидно, то не цвет зеркала, а любой цвет, который имеет отражённый луч. Всё, что нам нужно — вычислить направление отражённого луча и выяснить, каким был цвет света, падающего из этого направления. Вот бы у нас была функция, возвращающая для заданного луча цвет света, падающего из этого направления…
О, постойте, у нас же она есть: она называется TraceRay.
Итак, мы начинаем с основного цикла TraceRay, чтобы увидеть, что «видит» луч, испущенный из камеры. Если TraceRay определяет, что луч видит отражающий объект, то он просто должен вычислить направление отражённого луча и вызвать… сам себя.
На этом этапе, я предлагаю вам перечитать последние три параграфа, пока вы их не поймёте. Если вы впервые читаете о рекурсивной трассировке лучей, то возможно вам понадобится перечитать пару раз, и немного подумать, прежде чем вы действительно поймёте.
Не торопитесь, я подожду.
Теперь, когда эйфория от этого прекрасного момента эврика! немного спала, давайте немного это формализируем.
Самое важное во всех рекурсивных алгоритмах — предотвратить бесконечный цикл. В этом алгоритме есть очевидное условие выхода: когда луч или падает на неотражающий объект, или когда он ни на что не падает. Но есть простой случай, в котором мы можем угодить в бесконечный цикл: эффект бесконечного коридора. Он проявляется, когда вы ставите зеркало напротив другого зеркала и видите в них бесконечные копии самого себя!
Есть множество способов предотвращения этой проблемы. Мы введём предел рекурсии алгоритма; он будет контролировать «глубину», на которую он сможет уйти. Давайте назовём его и отражения некоторых отражений некоторых объектов. И так далее. В общем случае, нет особого смысла уходить вглубь больше чем на 2-3 уровня, потому что на этом этапе разница уже едва заметна.Мы создадим ещё одно разграничение. «Отражаемость» не должна иметь значение «есть или нет» — объекты могут быть частично отражающими и частично цветными. Мы назначим каждой поверхности число от
TraceRay? Луч начинается с поверхности объекта, точки TraceRay у нас есть
Отражение от гладкой поверхности
Теперь мы обратим своё внимание на «блестящие» объекты. В отличие от «матовых» объектов, «блестящие» меняют свой внешний вид, когда смотришь на них под разными углами.
Возьмём бильярдный шар или только что вымытый автомобиль. В таких объектах проявляется особый шаблон распространения света, обычно с яркими областями, которые как будто движутся, когда вы ходите вокруг них. В отличие от матовых объектов, то, как вы воспринимаете поверхность этих объектов, на самом деле зависит от точки обзора.
Заметьте, что красные бильярдные шары остаются красными, если вы отойдёте на пару шагов назад, но яркое белое пятно, дающее им «блестящий» вид, похоже, двигается. Это значит, что новый эффект не заменяет диффузное отражение, а дополняет его.
Почему это происходит? Мы можем начать с того, почему это не происходит на матовых объектах. Как мы видели в предыдущем разделе, когда луч света падает на поверхнось матового объекта, он равномерно рассеивается назад в сцену во всех направлениях.
Но что будет, если поверхность не настолько неровная? Давайте возьмём другую крайность — идеально отполированное зеркало. Когда луч света падает на зеркало, он отражается в единственном направлении, которое симметрично углу падения относительно нормали зеркала. Если мы назовём направление отражённого света на источник света, то получим такую ситуацию:
В зависимости от степени «отполированности» поверхности, она более или менее похожа на зеркало; то есть мы получаем «зеркальное» отражение (specular reflection, от латинского «speculum», то есть «зеркало»).
Для идеально отполированного зеркала падающий луч света
Рендеринг с «зеркальными» отражениями
Давайте добавим в сцену «зеркальные» отражения, над которыми мы сейчас работали. Во-первых, внесём некоторые изменения в саму сцену:
sphere { center = (0, -1, 3) radius = 1 color = (255, 0, 0) # Красный specular = 500 # Блестящий
}
sphere { center = (-2, 1, 3) radius = 1 color = (0, 0, 255) # Синий specular = 500 # Блестящий
}
sphere { center = (2, 1, 3) radius = 1 color = (0, 255, 0) # Зелёный specular = 10 # Немного блестящий
}
sphere { color = (255, 255, 0) # Жёлтый center = (0, -5001, 0) radius = 5000 specular = 1000 # Очень блестящий
}В коде нам нужно изменить
ComputeLighting
, чтобы он при необходимости вычислял значение «зеркальности» и прибавлял его к общему освещению. Заметьте, что теперь ему требуются
ComputeLighting(P, N, V, s) { i = 0.0 for light in scene.Lights { if light.type == ambient { i = light.intensity } else { if light.type == point L = light.position - P else L = light.direction # Диффузность n_dot_l = dot(N, L) if n_dot_l > 0 i = light.intensity*n_dot_l/(length(N)*length(L)) # Зеркальность if s != -1 { R = 2*N*dot(N, L) - L r_dot_v = dot(R, V) if r_dot_v > 0 i = light.intensity*pow(r_dot_v/(length(R)*length(V)), s) } } } return i
}
И наконец нам нужно изменить
TraceRay
, чтобы он передавал новые параметры
ComputeLighting
очевиден; он берётся из данных сферы. Но как насчёт
— это вектор, указывающий от объекта в камеру. К счастью, в
TraceRay
у нас уже есть вектор, направленный из камеры к объекту — это
, направление трассируемого луча! То есть
— это просто
Вот новый код TraceRay с «зеркальным» отражением:
TraceRay(O, D, t_min, t_max) { closest_t = inf closest_sphere = NULL for sphere in scene.Spheres { t1, t2 = IntersectRaySphere(O, D, sphere) if t1 in [t_min, t_max] and t1 < closest_t closest_t = t1 closest_sphere = sphere if t2 in [t_min, t_max] and t2 < closest_t closest_t = t2 closest_sphere = sphere } if closest_sphere == NULL return BACKGROUND_COLOR P = O closest_t*D # Вычисление пересечения N = P - closest_sphere.center # Вычисление нормали сферы в точке пересечения N = N / length(N) return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular)
}И вот наша награда за всё это жонглирование векторами:
Исходный код и рабочее демо >>
Рендеринг с отражением
Давайте добавим к коду трассировщика лучей отражение.
Как и ранее, в первую очередь мы изменяем сцену:
sphere { center = (0, -1, 3) radius = 1 color = (255, 0, 0) # Красный specular = 500 # Блестящий reflective = 0.2 # Немного отражающий
}
sphere { center = (-2, 1, 3) radius = 1 color = (0, 0, 255) # Синий specular = 500 # Блестящий reflective = 0.3 # Немного более отражающий
}
sphere { center = (2, 1, 3) radius = 1 color = (0, 255, 0) # Зелёный specular = 10 # Немного блестящий reflective = 0.4 # Ещё более отражающий
}
sphere { color = (255, 255, 0) # Жёлтый center = (0, -5001, 0) radius = 5000 specular = 1000 # Очень блестящий reflective = 0.5 # Наполовину отражающий
}Мы используем формулу «луча отражения» в паре мест, поэтому может избавиться от неё. Она получает луч
и нормаль
, возвращая
, отражённый относительно
ReflectRay(R, N) { return 2*N*dot(N, R) - R;
}
Единственным изменением в
ComputeLighting
является замена уравнения отражения на вызов этого нового
ReflectRay
В основной метод внесено небольшое изменение — нам нужно передать TraceRay верхнего уровня предел рекурсии:
color = TraceRay(O, D, 1, inf, recursion_depth)
Константе
recursion_depth
можно задать разумное значение, например, 3 или 5.
Единственные важные изменения происходят ближе к концу TraceRay, где мы рекурсивно вычисляем отражения:
TraceRay(O, D, t_min, t_max, depth) { closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max) if closest_sphere == NULL return BACKGROUND_COLOR # Вычисление локального цвета P = O closest_t*D # Вычисление точки пересечения N = P - closest_sphere.center # Вычисление нормали к сфере в точке пересечения N = N / length(N) local_color = closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular) # Если мы достигли предела рекурсии или объект не отражающий, то мы закончили r = closest_sphere.reflective if depth <= 0 or r <= 0: return local_color # Вычисление отражённого цвета R = ReflectRay(-D, N) reflected_color = TraceRay(P, R, 0.001, inf, depth - 1) return local_color*(1 - r) reflected_color*r
}
Пусть результаты говорят сами за себя:
Исходный код и рабочее демо >>
Чтобы лучше понять предел глубины рекурсии, давайте ближе рассмотрим рендер с
Как вы видите, разница заключается в том, видим ли мы отражения отражений отражений объектов, или только отражения объектов.




