Мы узнали всё, что нужно для перехода к практике! Теперь мы готовы написать наш первый трассировщик лучей. Вы уже должны быть в состоянии догадаться, как работает алгоритм трассировки лучей.
Прежде всего, найдите минутку, чтобы заметить, что распространение света в природе - это всего лишь бесчисленное количество лучей, испускаемых источниками света, которые отражаются от поверхностей и попадают в наш глаз. Таким образом, трассировка лучей элегантна в том смысле, что она основана непосредственно на том, что происходит вокруг нас. Если не учитывать тот факт, что у нас лучи следуют по пути света в обратном порядке, это не что иное, как идеальный симулятор природы.
Алгоритм трассировки лучей создает изображение, состоящее из пикселей. Для каждого пикселя на изображении он посылает основной луч в сцену. Направление этого первичного луча определяется путем проведения линии от глаза к центру этого пикселя. Как только мы зададим направление этого основного луча, мы проверим каждый объект в сцене, чтобы увидеть, пересекается ли он с каким-либо из них. В некоторых случаях основной луч будет пересекать более одного объекта. Когда это происходит, мы выбираем объект, точка пересечения которого находится ближе всего к глазу. Затем мы направляем теневой луч от точки пересечения к источнику света (рис. 1).
Точка попадания подсвечивается, если в этот луч не пересекается с объектом на пути к свету. Если он пересекается с другим объектом, этот объект отбрасывает на него тень (рис. 2).
Если мы повторим эту операцию для каждого пикселя, то получим двумерное представление нашей трехмерной сцены (рис. 3).
Вот реализация алгоритма в псевдокоде:
for (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
// вычисляем направление основного луча
Ray primRay;
computePrimRay(i, j, &primRay);
// выстреливаем лучём в сцену и ищем пересечение
Point pHit;
Normal nHit;
float minDist = INFINITY;
Object object = NULL;
for (int k = 0; k < objects.size(); ++k) {
if (Intersect(objects[k], primRay, &pHit, &nHit)) {
float distance = Distance(eyePosition, pHit);
if (distance < minDistance) {
object = objects[k];
minDistance = distance; // обновляем минимальную дистанцию
}
}
}
if (object != NULL) {
// расчитываем освещение
Ray shadowRay;
shadowRay.direction = lightPosition - pHit;
bool isShadow = false;
for (int k = 0; k < objects.size(); ++k) {
if (Intersect(objects[k], shadowRay)) {
isInShadow = true;
break;
}
}
}
if (!isInShadow)
pixels[i][j] = object->color * light.brightness;
else
pixels[i][j] = 0;
}
}
Как можно видеть, прелесть трассировки лучей в том, что код занимает всего несколько строк; базовый трассировщик лучей можно было бы написать за 200 строк. В отличие от других алгоритмов, таких как средство визуализации scanline, реализация трассировки лучей не требует особых усилий.
Артур Аппель впервые описал эту технику в 1969 году в статье, озаглавленной "Некоторые методы затенения машинных изображений твердых тел". Итак, если этот алгоритм такой замечательный, почему он не заменил все остальные алгоритмы рендеринга? Главной причиной в то время (и даже сегодня в какой-то степени) была скорость. Как упоминает Аппель в своей статье:
Этот метод очень трудоемкий, обычно для получения желаемых результатов требуется в несколько тысяч раз больше времени на вычисления, чем при составлении каркасного чертежа. Примерно половина этого времени отводится на определение точечного соответствия проекции и сцены.
Другими словами, это медленно (как однажды сказал Каджия - один из самых влиятельных исследователей всей истории компьютерной графики: "трассировка лучей не медленная - это компьютеры медленные"). Поиск пересечения между лучами и геометрией отнимает невероятно много времени. На протяжении десятилетий скорость алгоритма была главным недостатком трассировки лучей. Однако по мере того, как компьютеры становятся быстрее, это становится все меньшей и меньшей проблемой. Хотя все же следует сказать одну вещь: по сравнению с другими методами, такими как алгоритм z-buffer, трассировка лучей все еще намного медленнее. Однако сегодня, благодаря быстрым компьютерам, мы можем вычислить кадр, который раньше занимал один час, за несколько минут или меньше. Отслеживание лучей в реальном времени и в интерактивном режиме - актуальная тема.
Подводя итог, важно помнить, что процедуру рендеринга можно рассматривать как два отдельных процесса. Один шаг определяет, видна ли точка на поверхности объекта с определенного пикселя (часть видимости), а второй затеняет эту точку (часть затенения). К сожалению, оба шага требуют дорогостоящих и трудоемких проверок на пересечение геометрии лучей. Алгоритм элегантный и мощный, но вынуждает нас обменивать время рендеринга на точность, и наоборот. С тех пор как Аппель опубликовал свою статью, было проведено много исследований, направленных на ускорение процедур пересечения лучей с объектами. По мере того как компьютеры становились более мощными и сочетались с этими методами ускорения, трассировка лучей стала использоваться в повседневных производственных средах и, по сегодняшним стандартам, является методом де-факто, используемым большинством, если не всеми, автономных программ для рендеринга. Движки видеоигр по-прежнему используют алгоритм растеризации. Однако трассировка лучей в реальном времени также доступна благодаря недавнему появлению трассировки лучей с ускорением на графическом процессоре (2017-2018) и технологии RTX. Хотя в некоторых видеоиграх уже предусмотрены режимы, в которых можно включить трассировку лучей, она ограничена только простыми эффектами, такими как резкие отражения и тени.