Главная » Условно-съедобные грибы » Трассировка лучей в реальном времени. Методы трассировки лучей

Трассировка лучей в реальном времени. Методы трассировки лучей

Недавно в интернете я наткнулся на трассировщик лучей на визитке Пола Гекберта. Для тех, кто не в курсе: это очень известная задача, изначально предложенная Полом Гекбертом 4-ого мая 1984 на comp.graphics. Ее суть в том, чтобы написать демонстрацию метода бросания лучей, которая бы… умещалась на визитной карточке (больше об этом читайте в разделе «Трассировка лучей» из книги «Графические драгоценности IV»)!

Версия Эндрю Кенслера - одна из самых потрясающих и красивых реализаций этой задачи, которые я видел. Из любопытства я решил разобраться в ней. В этой статье я напишу все, что смог понять сам.

Обратная сторона визитки

Вот так выглядит сам код:

#include // card > aek.ppm #include #include typedef int i;typedef float f;struct v{ f x,y,z;v operator+(v r){return v(x+r.x ,y+r.y,z+r.z);}v operator*(f r){return v(x*r,y*r,z*r);}f operator%(v r){return x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r. y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v operator!(){return*this*(1/sqrt(*this%* this));}};i G={247570,280596,280600, 249748,18578,18577,231184,16,16};f R(){ return(f)rand()/RAND_MAX;}i T(v o,v d,f &t,v&n){t=1e9;i m=0;f p=-o.z/d.z;if(.01 0){f s=-b-sqrt(q);if(s.01)t=s,n=!(p+d*t),m=2;}}return m;}v S(v o,v d){f t ;v n;i m=T(o,d,t,n);if(!m)return v(.7, .6,1)*pow(1-d.z,4);v h=o+d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n*(n%d*-2);f b=l% n;if(b<0||T(h,l,t,n))b=0;f p=pow(l%r*(b >0),99);if(m&1){h=h*.2;return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1,1):v(3,3,3))*(b *.2+.1);}return v(p,p,p)+S(h,r)*.5;}i main(){printf("P6 512 512 255 ");v g=!v (-6,-16,0),a=!(v(0,0,1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g;for(i y=512;y--;) for(i x=512;x--;){v p(13,13,13);for(i r =64;r--;){v t=a*(R()-.5)*99+b*(R()-.5)* 99;p=S(v(17,16,8)+t,!(t*-1+(a*(R()+x)+b *(y+R())+c)*16))*3.5+p;}printf("%c%c%c" ,(i)p.x,(i)p.y,(i)p.z);}}

Код выше выглядит… пугающе, но компилируется и запускается без проблем! Вы можете сохранить его на рабочем столе как card.cpp , открыть консоль и ввести:

C++ -O3 -o card card.cpp ./card > card.ppm

Через 27 секунд на экране появится следующее изображение:

Возможности визитки-трассировщика лучей

Возможности просто поражают!

  • мир, состоящий из строго организованных сфер;
  • текстурированный пол;
  • небо с градиентом;
  • мягкие тени;
  • OMG, глубина резкости! Вы шутите?!

И все это на одной стороне визитной карточки! Посмотрим, как это работает.

Класс Vector

Рассмотрим первую часть кода:

#include // card > aek.ppm #include #include typedef int i;typedef float f;struct v{ f x,y,z;v operator+(v r){return v(x+r.x ,y+r.y,z+r.z);}v operator*(f r){return v(x*r,y*r,z*r);}f operator%(v r){return x*r.x+y*r.y+z*r.z;}v(){}v operator^(v r){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r. y-y*r.x);}v(f a,f b,f c){x=a;y=b;z=c;}v operator!(){return*this*(1/sqrt(*this%* this));}};

Главная хитрость здесь - это сокращение ключевых слов типов int и float до i и f с помощью typedef . Другой ход, с помощью которого можно можно уменьшить количество кода - это класс v , используемый не только в качестве вектора, но и для обработки пикселей.

#include // card > aek.ppm #include #include typedef int i; // Экономим место с помощью сокращения int до i typedef float f; // Экономим еще больше места с f вместо float // Класс вектора с конструктором и операторами struct v{ f x,y,z; // Три координаты вектора v operator+(v r){return v(x+r.x,y+r.y,z+r.z);} // Сумма векторов v operator*(f r){return v(x*r,y*r,z*r);} // Масштабирование векторов f operator%(v r){return x*r.x+y*r.y+z*r.z;} // Скалярное произведение векторов v(){} // Пустой конструктор v operator^(v r){return v(y*r.z-z*r.y,z*r.x-x*r.z,x*r.y-y*r.x);} // Векторное произведение векторов v(f a,f b,f c){x=a;y=b;z=c;} // Конструктор v operator!(){return *this*(1 /sqrt(*this%*this));} // Нормализация вектора };

Rand() и данные для генерации мира

i G={247570,280596,280600, 249748,18578,18577,231184,16,16};f R(){ return(f)rand()/RAND_MAX;}

Следующий код также экономит много места с помощью объявления функции R , которая возвращает случайное значение от 0 до 1 типа float. Это полезно при стохастическом сэмплировании, использующемся для blur-эффекта и мягких теней.

Массив G содержит в себе закодированное целыми числами положение сфер в мире. Совокупность всех чисел - это битовый вектор из 9 строк и 19 столбцов.

Вот код, приведенный выше, но отформатированный и с комментариями:

// Набор позиций сфер, описывающий мир // Все эти числа, по сути, являются по сути битовым вектором i G={247570,280596,280600,249748,18578,18577,231184,16,16}; // Генератор случайных чисел, возвращающий число с плавающей точкой в диапазоне 0-1 f R(){return(f)rand()/RAND_MAX;}

Главный метод

i main(){printf("P6 512 512 255 ");v g=!v (-6,-16,0),a=!(v(0,0,1)^g)*.002,b=!(g^a)*.002,c=(a+b)*-256+g;for(i y=512;y--;) for(i x=512;x--;){v p(13,13,13);for(i r =64;r--;){v t=a*(R()-.5)*99+b*(R()-.5)* 99;p=S(v(17,16,8)+t,!(t*-1+(a*(R()+x)+b *(y+R())+c)*16))*3.5+p;}printf("%c%c%c" ,(i)p.x,(i)p.y,(i)p.z);}}

Главный метод использует простой известный основанный на тексте формат изображений PPM. Изображение состоит из заголовка вида P6 [Ширина] [Высота] [Максимальное значение] , за которым следует RGB-значение каждого пикселя.

Для каждого пикселя на изображении программа сэмплирует (S) цвет 64 лучей, аккумулирует результат и выводит его в stdout .

Также этот код немного изменяет каждую координату начала луча и его направление. Это делается затем, чтобы создать эффект глубины резкости.

Вот код, приведенный выше, но отформатированный и с комментариями:

// Главная функция. Выводит изображение. // Использовать программу просто: ./card > erk.ppm i main(){ printf("P6 512 512 255 "); // Заголовок PPM // Оператор "!" осуществляет нормализацию вектора v g=!v(-6,-16,0), // Направление камеры a=!(v(0,0,1)^g)*.002, // Вектор, отвечающий за высоту камеры... b=!(g^a)*.002, // Правый вектор, получаемый с помощью векторного произведения c=(a+b)*-256+g; // WTF? Вот здесь https:// news.ycombinator.com/item?id=6425965 написано про это подробнее. for(i y=512;y--;) // Для каждого столбца for(i x=512;x--;){ // Для каждого пикселя в строке // Используем класс вектора, чтобы хранить цвет в RGB v p(13,13,13); // Стандартный цвет пикселя - почти черный // Бросаем по 64 луча из каждого пикселя for(i r=64;r--;){ // Немного меняем влево/вправо и вверх/вниз координаты начала луча (для эффекта глубины резкости) v t=a*(R()-.5)*99+b*(R()-.5)*99; // Назначаем фокальной точкой камеры v(17,16,8) и бросаем луч // Аккумулируем цвет, возвращенный в переменной t p=S(v(17,16,8)+t, // Начало луча!(t*-1+(a*(R()+x)+b*(y+R())+c)*16) // Направление луча с небольшим искажением // ради эффекта стохастического сэмплирования)*3.5+p; // +p для аккумуляции цвета } printf("%c%c%c",(i)p.x,(i)p.y,(i)p.z); } }

Сэмплер

v S(v o,v d){f t ;v n;i m=T(o,d,t,n);if(!m)return v(.7, .6,1)*pow(1-d.z,4);v h=o+d*t,l=!(v(9+R(),9+R(),16)+h*-1),r=d+n*(n%d*-2);f b=l% n;if(b<0||T(h,l,t,n))b=0;f p=pow(l%r*(b >0),99);if(m&1){h=h*.2;return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1,1):v(3,3,3))*(b *.2+.1);}return v(p,p,p)+S(h,r)*.5;}

Сэмплер S - это функция, возвращающая цвет пикселя по данным координатам точки начала луча о и его направлению d . Если она натыкается на сферу, то она вызывает себя рекурсивно, а в ином случае (если луч не имеет препятствий на своем пути) в зависимости от направления возвращает либо цвет неба, либо цвет пола (базируясь на его клетчатой текстуре).

Обратите внимание на вызов функции R при расчете направления света. Таким образом создается эффект «мягких теней».

Вот код, приведенный выше, но отформатированный и с комментариями:

// (S)эмплируем мир и возвращаем цвет пикселя по // по лучу, начинающемуся в точке o (Origin) и имеющему направление d (Direction) v S(v o,v d){ f t; v n; // Проверяем, натыкается ли луч на что-нибудь i m=T(o,d,t,n); if(!m) // m==0 // Сфера не была найдена, и луч идет вверх: генерируем цвет неба return v(.7,.6,1)*pow(1-d.z,4); // Возможно, луч задевает сферу v h=o+d*t, // h - координата пересечения l=!(v(9+R(),9+R(),16)+h*-1), // "l" = направление света (с небольшим искажеем для эффекта мягких теней) r=d+n*(n%d*-2); // r = полувектор // Расчитываем коэффицент Ламберта f b=l%n; // Рассчитываем фактор освещения (коэффицент Ламберта > 0 или находимся в тени)? if(b<0||T(h,l,t,n)) b=0; // Рассчитываем цвет p (с учетом диффузии и отражения света) f p=pow(l%r*(b>0),99); if(m&1){ // m == 1 h=h*.2; // Сфера не была задета, и луч уходит вниз, в пол: генерируем цвет пола return((i)(ceil(h.x)+ceil(h.y))&1?v(3,1,1):v(3,3,3))*(b*.2+.1); } // m == 2 Была задета сфера: генерируем луч, отскакивающий от поверхности сфера return v(p,p,p)+S(h,r)*.5; // Ослабляем цвет на 50%, так как он отскакивает от поверхности (* .5) }

Трэйсер

i T(v o,v d,f &t,v&n){t=1e9;i m=0;f p=-o.z/d.z;if(.01 0){f s=-b-sqrt(q);if(s.01)t=s,n=!(p+d*t),m=2;}}return m;}

Функция T (Tracer) отвечает за бросание луча из данной точки (o) в данном направлении (d). Она возвращает целое число, которое является кодом для результата бросания луча. 0 - луч ушел в небо, 1 - луч ушел в пол, 2 - луч наткнулся на сферу. Если была задета сфера, то функция обновляет переменные t (параметр, используемый для вычисления дистанции пересения) и n (полу-вектор при отскакивании от сферы).

Вот код, приведенный выше, но отформатированный и с комментариями:

// Тест на пересечение для линии // Возвращаем 2, если была задета сфера (а также дистанцию пересечения t и полу-вектор n). // Возвращаем 0, если луч ничего не задевает и идет вверх, в небо // Возвращаем 1, если луч ничего не задевает и идет вниз, в пол i T(v o,v d,f& t,v& n){ t=1e9; i m=0; f p=-o.z/d.z; if(.010){ // Да. Считаем расстояние от камеры до сферы f s=-b-sqrt(q); if(s.01) // Это минимальное расстояние, сохраняем его. А также // вычиваем вектор отскакивающего луча и записываем его в "n" t=s, n=!(p+d*t), m=2; } } return m; }

Число Leet

Многие программисты пытались сократить код еще больше. Сам автор остановился на версии, предоставленной в этой статье. Знаете, почему?

Fabien$ wc card.cpp 35 95 1337 card.cpp - много математики, но все очень подробно и ясно объясняется.

Предисловие

Закон Мура, говорящий об экспоненциальном росте со временем вычислительной мощности, дает основание полагать, что рано или поздно, методы на основе трассировки лучей, использующиеся для создания высоко реалистичных изображений в трёхмерных редакторах, можно будет использовать и в реальном времени в компьютерных играх.

Но на самом деле, законы, принятые депутатами, вкусы избирателей, то есть, пользователей, и научно-технические достижения в далеких областях повлияют на перспективы трассировки лучей в гораздо большей степени.

Вступление

Кратко осветим суть метода (обратной) трассировки лучей. В использующемся в современной графике реального времени растеризационном методе, для рисования объекта находится проекция на плоскость экрана составляющих объект треугольников. И они рисуются по пикселям, с заполнением буфера глубины, то есть, расстояния до плоскости экрана. Буфер глубины требуется, чтобы ближние к наблюдателю треугольники зарисовали дальние, а не наоборот. И все остальные эффекты делаются на основе растеризации.

В методе обратной трассировки лучей построение изображения, наоборот, идет от пикселей экрана, а не от объектов. Через каждую точку экрана в направлении от наблюдателя проводится воображаемый луч. Он имитирует луч света, пришедший к наблюдателю из данного направления. И для каждого луча смотрится, с каким объектом он первым пересекается. И цвет области объекта, соответствующий точке пересечения, задаст цвет данного пикселя. Но дальше начинается самое интересное. После пересечения с объектом, луч начинает свое путешествие по сцене. Проводятся лучи в направлении источников света, чтобы проверить, затеняется ли данная точка этого объекта, можно провести отраженный луч, если объект имеет зеркальные свойства, можно провести преломленный луч, если объект полупрозрачен.

В данном случае, точка объекта напрямую освещается только одним источником света, второй заслонен другим объектом.

Таким образом, происходит некоторая симуляция распространения света. Метод имеет множество сложных модификаций, но в их основе лежит «трассировка луча», то есть, нахождение пересечения луча (света) с объектами сцены.

Проблема

Пусть метод трассировки и позволяет отрисовать сцену с эффектами освещения, прозрачностью и отражениями, он вычислительно крайне затратен. Операция нахождения пересечения произвольного луча с объектами сложной сцены весьма нетривиальна. И она не может быть так же легко ускорена специальными (достаточно простыми) «ускорителями», как математически простая операция растеризации треугольников. Поэтому, в игровой графике используется растеризационный метод, который позволяет быстро нарисовать геометрию, то есть, фигуры объектов и текстуры со всевозможными шейдерами. А освещение почти всей сцены статическое. Только для отдельных движущихся моделей используются частные методы рисования теней. Они тоже основаны на растеризации: тени, по сути, просто рисуются.

Самый простой пример: рисуется в отдельный буфер силуэт объекта, с точки зрения источника света и далее, содержимое этого буфера, как текстура, накладывается на поверхность под объектом. Получаются такие динамические бегающие тени. Их можно видеть во многих компьютерных играх уже давно. Метод допускает усовершенствования, можно проецировать этот силуэт на стены, на кривые поверхности. Текстуру этого силуэта можно размыть, получив таким образом рисунок с оттенками серого, а не просто черно-белый четкий силуэт. И тогда, при наложении получится мягкий переход от темноты к свету, так называемая мягкая тень. Это будет не совсем корректная физически тень, но смотрится похоже.

Построенная с помощью трассировки лучей мягкая тень будет более реалистичной, но её рисовать гораздо вычислительно затратнее. И самый первый вопрос, заметит ли геймер в азарте, например, компьютерной стрелялки, разницу между сильно приближенно нарисованной тенью и более физически корректной? Тут мы подходим к вопросу субъективного восприятия людьми, то есть, геймерами, графики. Ведь картинка на экране монитора только грубо приближает реальность. И если пользоваться различными критериями, мера этого приближения будет меняться.

Оказалось, и это очевидно, что для большинства решающий критерий приближения это геометрическая детализация. Далее, с некоторым отрывом, качественное текстурирование. В части текстурирования у метода трассировки лучей примерный паритет с растеризационным, мы не будем особенно рассматривать вопросы текстурирования и шейдеров материалов.

Но геометрию сцены трассировкой лучей рисовать не выгодно, хотя это зависит от сцены. Сцены определенного плана эффективнее рисовать трассировкой, но сцены из современных игр далеки от этого класса.

Далее в статье мы предметно рассмотрим различные проекты в области трассировки, но, например, в свое время Intel демонстрировала рендеринг уровней из Quake III с помощью трассировки. Низкополигональные уровни в низком разрешении медленно рисовались на очень дорогой и продвинутой системе, далекой от потребительского рынка. Фишка была в том, что можно рисовать динамические тени и сложные отражения.

Но человеческое зрение и восприятие так устроено, что оно очень адаптивно к освещению. Собственно, всякие тени только мешают человеческому глазу выделять необходимые ему объекты. Например, при охоте, типичном занятии наших предков, в тенях деревьев могла скрываться добыча. Надо тени убрать с виртуального изображения, формирующегося в мозге.

Другой момент заключается в том, что реальное освещение одной сцены может быть невероятно многообразно, в зависимости от отражающих свойств поверхностей и свойств среды, воздуха в частности, а так же от свойств источника света. Имеются в виду не зеркальные отражения, а рассеивание света объектами. Как мы видим, в самом углу темнее, чем ближе к окну, потому что в самый угол темный попадает в конечном итоге меньшее количество фотонов света, путешествующих по комнате. Сам воздух тоже может различным образом рассеивать свет. И для приближенной упрощенной модели освещения, используемой во многих играх, можно подобрать реалистичные параметры отражающих свойств поверхностей, свойства воздуха, источника света, чтобы в реальности примерно воспроизвести освещение игровых сцен.

В играх часто также используется пререндрённое освещение для сцены, которое заранее рассчитывается тем же методом трассировки лучей и записывается в текстуры материалов объектов. Хорошо, в большинстве случаев мы в реальной жизни наблюдаем статический свет. Солнце медленно идет по небу, когда же мы входим в помещение, то включаем свет, если он там ещё не горит. Потом берем автомат и расстреливаем лампочки, свет выключается. Все это можно заранее рассчитать и поместить в специальные текстуры, которые называются lightmap (для экономии места, они меньшего разрешения, чем текстуры материалов, так как освещение меняется плавно и его можно качественно интерполировать для каждой точки, используя малоразмерные текстуры). Или рассчитать освещение для каждой вершины треугольников высоко детализированной сцены, а тени от движущихся моделей рисовать приближенно одним из частных методов.

Я знаю, это немного разочаровывает. Где отражения, тени и красивый внешний вид? Мы всё это получим, ведь мы пока только начали. Но это хорошее начало - сферы выглядят как круги, а это лучше, чем если бы они выглядели как кошки. Они не выглядят как сферы потому, что мы упустили важный компонент, позволяющий человеку определять форму объекта - то, как он взаимодействует со светом.

Освещение

Первым шагом для добавления «реализма» нашему рендерингу сцены будет симуляция освещения. Освещение - это безумно сложная тема, поэтому я представлю очень упрощённую модель, достаточную для наших целей. Некоторые части этой модели даже не являются приближением к физическим моделям, они просто быстры и хорошо выглядят.

Мы начнём с некоторых упрощающих допущений, которые облегчат нам жизнь.

Во-первых, мы объявим, что всё освещение имеет белый цвет. Это позволит нам охарактеризовать любой источник освещения единственным действительным числом i, называемым яркостью освещения. Симуляция цветного освещения не так сложна (необходимо только три значения яркости, по одному на канал, и вычисление всех цветов и освещения поканально), но чтобы сделать нашу работу проще, я не буду его делать.

Во-вторых, мы избавимся от атмосферы. Это значит, что освещение не становятся менее яркими, независимо от их дальности. Затухание яркости света в зависимости от расстояния реализовать тоже не слишком сложно, но для ясности мы пока его пропустим.

Источники освещения

Свет должен откуда-то поступать. В этом разделе мы зададим три различных типа источников освещения.

Точечные источники

Точечный источник испускает свет из фиксированной точки в пространстве, называемой его позицией . Свет испускается равномерно во всех направлениях; именно поэтому его также называют всенаправленным освещением . Следовательно, точечный источник полностью характеризуется его позицией и яркостью.

Лампа накаливания - хороший пример из реального мира того, приближением чего является точечный источник освещения. Хотя лампа накаливания не испускает свет из одной точки и он не является совершенно всенаправленным, но приближение достаточно хорошее.

Давайте зададим вектор как направление из точки P в сцене к источнику освещения Q. Этот вектор, называемый световым вектором , просто равен . Заметьте, что поскольку Q фиксирована, а P может быть любой точкой сцены, то в общем случае будет разным для каждой точки сцены.

Направленные источники

Если точечный источник является хорошей аппроксимацией лампы накаливания, то что может служить аппроксимацией Солнца?

Это хитрый вопрос, и ответ зависит от того, что вы хотите отрендерить.

В масштабах Солнечной системы Солнце можно приблизительно считать точечным источником. В конце концов, оно испускает свет из точки (хотя и довольно большой) и испускает его во всех направлениях, то есть подходит под оба требования.

Однако если в вашей сцене действие происходит на Земле, то это не слишком хорошее приближение. Солнце находится так далеко, что каждый луч света будет на самом деле иметь одинаковое направление (Примечание: эта аппроксимация сохраняется в масштабе города, но не на более дальних расстояниях - на самом деле. древние греки смогли с удивительной точностью вычислить радиус Земли на основании разных направлений солнечного света в различных местах.). Хотя это можно аппроксимировать это с помощью точечного источника, сильно удалённого от сцены, это расстояние и расстояние между объектами в сцене настолько отличаются по величине, что могут появиться ошибки точности чисел.

Для таких случаев мы зададим направленные источники освещения . Как и точечные источники, направленный источник имеет яркость, но в отличие от них, у него нет позиции. Вместо неё у него есть направление . Можно воспринимать его как бесконечно удалённый точечный источник, светящий в определённом направлении.

В случае точечных источников нам нужно вычислять новый световой вектор для каждой точки P сцены, но в этом случае задан. В сцене с Солнцем и Землёй будет равен .

Окружающее освещение

Можно ли смоделировать любое освещение реального мира как точечный или направленный источник? Почти всегда да (Примечание: но это необязательно будет просто; зональное освещение (представьте источник за рассеивателем) можно аппроксимировать множеством точечных источников на его поверхности, но это сложно, более затратно по вычислениям, а результаты оказываются неидеальными.). Достаточно ли этих двух типов источников для наших целей? К сожалению, нет.

Представьте, что происходит на Луне. Единственным значимым источником освещения поблизости является Солнце. То есть «передняя половина» Луны относительно Солнца получает всё освещение, а «задняя половина» находится в полной темноте. Мы видим это с разных углов на Земле, и этот эффект создаёт то, что мы называем «фазами» Луны.

Однако ситуация на Земле немного отличается. Даже точки, не получающие освещения непосредственно от источника освещения, не находятся полностью в темноте (просто посмотрите на пол под столом). Как лучи света достигают этих точек, если «обзор» на источники освещения чем-то перекрыт?

Как я упомянул в разделе Цветовые модели , когда свет падает на объект, часть его поглощается, но остальная часть рассеивается в сцене. Это значит, что свет может поступать не только от источников освещения, но и от других объектов, получающих его от источников освещения и рассеивающих его обратно. Но зачем останавливаться на этом? Рассеянное освещение в свою очередь падает на какой-нибудь другой объект, часть его поглощается, а часть снова рассеивается в сцене. При каждом отражении свет теряет часть своей яркости, но теоретически можно продолжать ad infinitum (Примечание: на самом деле нет, потому что свет имеет квантовую природу, но достаточно близко к этому.).

Это значит, что нужно считать источником освещения каждый объект . Как можно представить, это сильно увеличивает сложность нашей модели, поэтому мы не пойдём таким путём (Примечание: но вы можете хотя бы загуглить Global Illumination и посмотреть на прекрасные изображения.).

Но мы всё равно не хотим, чтобы каждый объект был или освещён напрямую, или был полностью тёмным (если только мы не рендерим модель Солнечной системы). Чтобы преодолеть эту преграду, мы зададим третий тип источников освещения, называемый окружающим освещением , которое характеризуется только яркостью. Считается, что оно носит безусловный вклад освещения в каждую точку сцены. Это очень сильное упрощение чрезвычайно сложного взаимодействия между источниками освещения и поверхностями сцены, но оно работает.

Освещённость одной точки

В общем случае, в сцене будет один источник окружающего освещения (потому что окружающее освещение имеет только значение яркости, и любое их количество будет тривиально сочетаться в единый источник окружающего освещения) и произвольное количество точечных и направленных источников.

Для вычисления освещённости точки нам просто нужно вычислить количество света, вносимое каждым источником и сложить их, чтобы получить одно число, представляющее общее количество полученного точкой освещения. Затем мы можем умножить цвет поверхности в этой точке на это число, чтобы получить правильно освещённый цвет.

Итак, что произойдёт, когда луч света с направлением из направленного или точечного источника падает на точку P какого-нибудь объекта в нашей сцене?

Интуитивно мы можем разбить объекты на два общих класса, в зависимости от того, как они ведут себя со светом: «матовые» и «блестящие». Поскольку большинство окружающих нас предметов можно считать «матовыми», то с них мы и начнём.

Диффузное рассеяние

Когда луч света падает на матовый объект, то из-за неровности его поверхности на микроскопическом уровне, он отражает луч в сцену равномерно во всех направлениях, то есть получается «рассеянное» («диффузное») отражение.

Чтобы убедиться в этом, внимательно посмотрите на какой-нибудь матовый объект, например, на стену: если двигаться вдоль стены, её цвет не меняется. То есть, видимый вами свет, отражённый от объекта, одинаков вне зависимости от того, в какое место объекта вы смотрите.

С другой стороны, количество отражённого света зависит от угла между лучом света и поверхностью. Интуитивно это понятно - энергия, переносимая лучом, в зависимости от угла должна распределиться по меньшей или большей поверхности, то есть энергия на единицу площади, отражённая в сцену, будет соответственно выше или ниже:

Чтобы выразить это математически, давайте охарактеризуем ориентацию поверхности по её вектору нормали . Вектор нормали, или просто «нормаль» - это вектор, перпендикулярный поверхности в какой-то точке. Также он является единичным вектором, то есть его длина равна 1. Мы будем называть этот вектор .

Моделирование диффузного отражения

Итак, луч света с направлением и яркостью падает на поверхность с нормалью . Какая часть отражается обратно сцену как функция от , и ?

Для геометрической аналогии давайте представим яркость света как «ширину» луча. Его энергия распределяется по поверхности размером . Когда и имеют одно направление, то есть луч перпендикулярен поверхности, , а это значит, что энергия, отражённая на единицу площади равна падающей энергии на единицу площади; < . С другой стороны, когда угол между и приближается к , приближается к , то есть энергия на единицу площади приближается к 0; . Но что происходит в промежутках?

Ситуация отображена на схеме ниже. Мы знаем , и ; я добавил углы и , а также точки , и , чтобы сделать связанные с этой схемой записи проще.

Поскольку технически луч света не имеет ширины, поэтому мы будем считать, что всё происходит на бесконечно малом плоском участке поверхности. Даже если это поверхность сферы, то рассматриваемая область настолько бесконечно мала, что она почти плоская относительно размера сферы, так же как Земля выглядит плоской при малых масштабах.

Луч света с шириной падает на поверхность в точке под углом . Нормаль в точке равна , а энергия, переносимая лучом, распределяется по . Нам нужно вычислить .

Один из углов равен , а другой - . Тогда третий угол равен . Но нужно заметить, что и тоже образуют прямой угол, то есть тоже должны быть . Следовательно, :

Давайте рассмотрим треугольник . Его углы равны , и . Сторона равна , а сторона равна .

И теперь… тригонометрия спешит на помощь! По определению ; заменяем на , а на , и получаем


что преобразуется в
Мы почти закончили. - это угол между и , то есть можно выразить как
И наконец
Итак, мы получили очень простое уравнение, связывающее отражённую часть света с углом между нормалью к поверхности и направлением света.

Заметьте, что при углах больше значение становится отрицательным. Если мы не задумываясь используем это значение, то в результате получим источники света, вычитающие свет. Это не имеет никакого физического смысла; угол больше просто означает, что свет на самом деле достигает задней части поверхности, и не вносит свой вклад в освещение освещаемой точки. То есть если становится отрицательным, то мы считаем его равным .

Уравнение диффузного отражения

Теперь мы можем сформулировать уравнение для вычисления полного количества света, полученного точкой с нормалью в сцене с окружающим освещением яркостью и точечных или направленных источников света с яркостью и световыми векторами или известными (для направленных источников), или вычисленными для P (для точечных источников):
Стоит снова повторить, что члены, в которых не должны прибавляться к освещённости точки.

Нормали сферы

Здесь только отсутствует единственная мелочь: откуда берутся нормали?

Этот вопрос намного хитрее, чем кажется, как мы увидим во второй части статьи. К счастью, для разбираемого нами случая есть очень простое решение: вектор нормали любой точки сферы лежит на прямой, проходящей через центр сферы. То есть если центр сферы - это , то направление нормали в точки равно :

Почему я написал «направление нормали», а не «нормаль»? Кроме перпендикулярности к поверхности, нормаль должна быть единичным вектором; это было бы справедливо, если бы радиус сферы был равен , что не всегда верно. Для вычисления самой нормали нам нужно разделить вектор на его длину, получив таким образом длину :


Это представляет в основном теоретический интерес, потому что записанное выше уравнение освещения содержит деление на , но хорошим подходом будет создание «истинных» нормалей; это упростит нам работу в дальнейшем.

Рендеринг с диффузным отражением

Давайте переведём всё это в псевдокод. Во-первых, давайте добавим в сцену пару источников освещения:

Light { type = ambient intensity = 0.2 } light { type = point intensity = 0.6 position = (2, 1, 0) } light { type = directional intensity = 0.2 direction = (1, 4, 4) }
Заметьте, что яркость удобно суммируется в , потому что из уравнения освещения следует, что никакая точка не может иметь яркость света выше, чем единица. Это значит, что у нас не получатся области со «слишком большой выдержкой».

Уравнение освещения довольно просто преобразовать в псевдокод:

ComputeLighting(P, N) { 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)) } } return i }
И единственное, что осталось - использовать ComputeLighting в TraceRay . Мы заменим строку, возвращающую цвет сферы

Return closest_sphere.color
на этот фрагмент:

P = O + closest_t*D # вычисление пересечения N = P - closest_sphere.center # вычисление нормали сферы в точке пересечения N = N / length(N) return closest_sphere.color*ComputeLighting(P, N)
Просто ради интереса давайте добавим большую жёлтую сферу:

Sphere { color = (255, 255, 0) # Yellow center = (0, -5001, 0) radius = 5000 }
Мы запускаем рендерер, и узрите - сферы наконец начали выглядеть как сферы!

Но постойте, как большая жёлтая сфера превратилась в плоский жёлтый пол?

Этого и не было, просто она настолько велика относительно других трёх, а камера настолько к ней близка, что она выглядит плоской. Так же, как наша планета выглядит плоской, когда мы стоим на её поверхности.

Отражение от гладкой поверхности

Теперь мы обратим своё внимание на «блестящие» объекты. В отличие от «матовых» объектов, «блестящие» меняют свой внешний вид, когда смотришь на них под разными углами.

Возьмём бильярдный шар или только что вымытый автомобиль. В таких объектах проявляется особый шаблон распространения света, обычно с яркими областями, которые как будто движутся, когда вы ходите вокруг них. В отличие от матовых объектов, то, как вы воспринимаете поверхность этих объектов, на самом деле зависит от точки обзора.

Заметьте, что красные бильярдные шары остаются красными, если вы отойдёте на пару шагов назад, но яркое белое пятно, дающее им «блестящий» вид, похоже, двигается. Это значит, что новый эффект не заменяет диффузное отражение, а дополняет его.

Почему это происходит? Мы можем начать с того, почему это не происходит на матовых объектах. Как мы видели в предыдущем разделе, когда луч света падает на поверхнось матового объекта, он равномерно рассеивается назад в сцену во всех направлениях. Интуитивно понятно, что так происходит из-за неровности поверхности объекта, то есть на микроскопическом уровне она похожа на множество мелких поверхностей, направленных в случайных направлениях:

Но что будет, если поверхность не настолько неровная? Давайте возьмём другую крайность - идеально отполированное зеркало. Когда луч света падает на зеркало, он отражается в единственном направлении, которое симметрично углу падения относительно нормали зеркала. Если мы назовём направление отражённого света и условимся, что указывает на источник света, то получим такую ситуацию:

В зависимости от степени «отполированности» поверхности, она более или менее похожа на зеркало; то есть мы получаем «зеркальное» отражение (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 >
И наконец нам нужно изменить 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 and t1 < closest_t closest_t = t1 closest_sphere = sphere if t2 in 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) }
И вот наша награда за всё это жонглирование векторами:

Тени

Там, где есть свет и объекты, должны быть и тени. Так где же наши тени?

Давайте начнём с более фундаментального вопроса. Почему должны быть тени? Тени появляются там, где есть свет, но его лучи не могут достичь объекта, потому что на их пути есть другой объект.

Вы заметите, что в предыдущем разделе нас интересовали углы и вектора, но мы рассматривали только источник света и точку, которую нам нужно раскрасить, и полностью игнорировали всё остальное, что происходит в сцене - например, попавшийся на пути объект.

Вместо этого нам нужно добавить немного логики, говорящей "если между точкой и источником есть объект, то не нужно добавлять освещение, поступающее от этого источника ".

Мы хотим выделить два следующих случая:

Похоже, что у нас есть все необходимые для этого инструменты.

Давайте начнём с направленного источника. Мы знаем ; это точка, которая нас интересует. Мы знаем ; это часть определения источника освещения. Имея и , мы можем задать луч, а именно , который проходит из точки до бесконечно отдалённого источника освещения. Пересекает ли этот луч другой объект? Если нет, то между точкой и источником ничего нет, то есть мы можем вычислить освещённость от этого источника и прибавить его к общей освещённости. Если пересекает, то мы игнорируем этот источник.

Мы уже знаем, как вычислить ближайшее пересечение между лучом и сферой; мы используем его для трассировки лучей от камеры. Мы снова можем использовать его для вычисления ближайшего пересечения между лучом света и остальной сценой.

Однако параметры немного отличаются. Вместо того, чтобы начинаться с камеры, лучи испускаются из . Направление равно не , а . И нас интересуют пересечения со всем после на бесконечное расстояние; это значит, что и .

Мы можем обрабатывать точечные источники очень похожим образом, но с двумя исключениями. Во-первых, не задан , но его очень просто вычислить из позиции источника и . Во-вторых, нас интересуют любые пересечения, начиная с , но только до (в противном случае, объекты за источником освещения могли бы создавать тени!); то есть в этом случае и .

Существует один пограничный случай, который нам нужно рассмотреть. Возьмём луч . Если мы будем искать пересечения, начиная с , то мы, вероятнее всего, найдём саму при , потому что действительно находится на сфере, и ; другими словами, каждый объект будет отбрасывать тени на самого себя (Примечание: если точнее, то мы хотим избежать ситуации, при которой точка, а не весь объект, отбрасывает тень на саму себя; объект с более сложной чем сфера формой (а именно любой вогнутый объект) может отбрасывать истинные тени на самого себя!

Простейший способ справиться с этим - использовать в качестве нижней границы значений вместо малое значение . Геометрически, мы хотим сделать так, чтобы луч начинается немного вдали от поверхности, то есть рядом с , но не точно в . То есть для направленных источников интервал будет , а для точечных - .

Рендеринг с тенями

Давайте превратим это в псевдокод.

В предыдущей версии TraceRay вычислял ближайшее пересечение луч-сфера, а затем вычислял освещение в пересечении. Нам нужно извлечь код ближайшего пересечения, поскольку мы хотим использовать его снова для вычисления теней:

ClosestIntersection(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 and t1 < closest_t closest_t = t1 closest_sphere = sphere if t2 in and t2 < closest_t closest_t = t2 closest_sphere = sphere } return closest_sphere, closest_t }
В результате TraceRay получается гораздо проще:

TraceRay(O, D, t_min, t_max) { closest_sphere, closest_t = ClosestIntersection(O, D, t_min, t_max) if closest_sphere == NULL return BACKGROUND_COLOR P = O + closest_t*D # Compute intersection N = P - closest_sphere.center # Compute sphere normal at intersection N = N / length(N) return closest_sphere.color*ComputeLighting(P, N, -D, sphere.specular) }
Теперь нам нужно добавить в 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 t_max = 1 } else { L = light.direction t_max = inf } # Проверка тени shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max) if shadow_sphere != NULL continue # Диффузность 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 .

Итак, мы начинаем с основного цикла TraceRay , чтобы увидеть, что «видит» луч, испущенный из камеры. Если TraceRay определяет, что луч видит отражающий объект, то он просто должен вычислить направление отражённого луча и вызвать… сам себя.

На этом этапе, я предлагаю вам перечитать последние три параграфа, пока вы их не поймёте. Если вы впервые читаете о рекурсивной трассировке лучей, то возможно вам понадобится перечитать пару раз, и немного подумать, прежде чем вы действительно поймёте .

Не торопитесь, я подожду.

Теперь, когда эйфория от этого прекрасного момента эврика! немного спала, давайте немного это формализируем.

Самое важное во всех рекурсивных алгоритмах - предотвратить бесконечный цикл. В этом алгоритме есть очевидное условие выхода: когда луч или падает на неотражающий объект, или когда он ни на что не падает. Но есть простой случай, в котором мы можем угодить в бесконечный цикл: эффект бесконечного коридора . Он проявляется, когда вы ставите зеркало напротив другого зеркала и видите в них бесконечные копии самого себя!

Есть множество способов предотвращения этой проблемы. Мы введём предел рекурсии алгоритма; он будет контролировать «глубину», на которую он сможет уйти. Давайте назовём его . При , то видим объекты, но без отражений. При мы видим некоторые объекты и отражения некоторых объектов. При мы видим некоторые объекты, отражения некоторых объектов и отражения некоторых отражений некоторых объектов . И так далее. В общем случае, нет особого смысла уходить вглубь больше чем на 2-3 уровня, потому что на этом этапе разница уже едва заметна.

Мы создадим ещё одно разграничение. «Отражаемость» не должна иметь значение «есть или нет» - объекты могут быть частично отражающими и частично цветными. Мы назначим каждой поверхности число от до , определяющее её отражаемость. После чего мы будем смешивать локально освещённый цвет и отражённый цвет пропорционально этому числу.

И наконец, нужно решить, какие параметры должен получать рекурсивный вызов TraceRay ? Луч начинается с поверхности объекта, точки . Направление луча - это направление света, отразившегося от ; в TraceRay у нас есть , то есть направление от камеры к , противоположное движению света, то есть направление отражённого луча будет , отражённый относительно . Аналогично тому, что происходит с тенями, мы не хотим, чтобы объекты отражали сами себя, поэтому . Мы хотим видеть объекты отражёнными вне зависимости от того, насколько они отдалены, поэтому . И последнее - предел рекурсии на единицу меньше, чем предел рекурсии, в котором мы находимся в текущий момент.

Рендеринг с отражением

Давайте добавим к коду трассировщика лучей отражение.

Как и ранее, в первую очередь мы изменяем сцену:

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 }
Пусть результаты говорят сами за себя:

Чтобы лучше понять предел глубины рекурсии, давайте ближе рассмотрим рендер с :

А вот тот же увеличенный вид той же сцены, на этот раз отрендеренный с :

Как вы видите, разница заключается в том, видим ли мы отражения отражений отражений объектов, или только отражения объектов.

Произвольная камера

В самом начале обсуждения трассировки лучей мы сделали два важных допущения: камера фиксирована в и направлена в , а направлением «вверх» является . В этом разделе мы избавимся от этих ограничений, чтобы можно было располагать камеру в любом месте сцены и направлять её в любом направлении.

Давайте начнём с положения. Вы наверно заметили, что используется во всём псевдокоде только один раз: в качестве начальной точки лучей, исходящих из камеры в методе верхнего уровня. Если мы хотим поменять положение камеры. то единственное , что нужно сделать - это использовать другое значение для .

Влияет ли изменение положения на направление лучей? Ни в коей мере. Направление лучей - это вектор, проходящий из камеры на плоскость проекции. Когда мы перемещаем камеру, плоскость проекции двигается вместе с камерой, то есть их относительные положения не изменяются.

Давайте теперь обратим внимание на направление. Допустим, у нас есть матрица поворота, которая поворачивает в нужном направлении обзора, а - в нужное направление «вверх» (и поскольку это матрица поворота, то по определению она должна делать требуемое для ). Положение камеры не меняется, если вы просто вращаете камеру вокруг. Но направление меняется, оно просто подвергается тому же повороту, что и вся камера. То есть если у нас есть направление и матрица поворота , то повёрнутый - это просто .

Меняется только функция верхнего уровня:

For x in [-Cw/2, Cw/2] { for y in [-Ch/2, Ch/2] { D = camera.rotation * CanvasToViewport(x, y) color = TraceRay(camera.position, D, 1, inf) canvas.PutPixel(x, y, color) } }
Вот как выглядит наша сцена при наблюдении из другого положения и при другой ориентации:

Куда двигаться дальше

Мы закончим первую часть работы кратким обзором некоторых интересных тем, которые мы не исследовали.

Оптимизация

Как сказано во введении, мы рассматривали наиболее понятный способ объяснения и реализации различных возможностей. Поэтому трассировщик лучей полностью функционален, но не особо быстр. Вот некоторые идеи, которые можно изучить самостоятельно для ускорения работы трассировщика. Просто ради интереса попробуйте замерить время выполнения до и после их реализации. Вы очень удивитесь!

Параллелизация

Наиболее очевидный способ ускорения работы трассировщика лучей - трассировать несколько лучей одновременно. Поскольку каждый луч, исходящий из камеры, независим от всех остальных, а большинство структур предназначено только для чтения, мы можем трассировать по одному лучу на каждое ядро центрального процессора без особых затруднений и сложностей из-за проблем с синхронизацией.

На самом деле, трассировщики лучей относятся к классу алгоритмов, называемому чрезвычайно параллелизуемым именно потому что, сама их природа позволяет очень просто их распараллеливать.

Кэширование значений

Рассмотрим значения, вычисляемые IntersectRaySphere , на который трассировщик лучей обычно тратит большинство времени:

K1 = dot(D, D) k2 = 2*dot(OC, D) k3 = dot(OC, OC) - r*r
Некоторые из этих значений постоянны для всей сцены - как только вы узнаете, как расположены сферы, r*r и dot(OC, OC) больше не меняются. Можно вычислить их один раз во время загрузки сцены и хранить их в самих сферах; вам просто нужно будет пересчитать их, если сферы должны переместиться в следующем кадре. dot(D, D) - это константа для заданного луча, поэтому можно вычислить его в ClosestIntersection и передать в IntersectRaySphere .

Оптимизации теней

Если точка объекта находится в тени относительно источника освещения, потому что на пути обнаружен другой объект, то высока вероятность того, что соседняя с ней точка из-за того же объекта тоже находится в тени относительно источника освещения (это называется согласованностью теней ):

То есть когда мы ищем объекты между точкой и источником освещения, можно сначала проверить, не накладывает ли на текущую точку тень последний объект, накладывавший тень на предыдущую точку относительно того же источника освещения. Если это так, то мы можем закончить; если нет, то просто продолжаем обычным способом проверять остальные объекты.

Аналогично, при вычислении пересечения между лучом света и объектами в сцене на самом деле нам не нужно ближайшее пересечение - достаточно знать, что существует по крайней мере одно пересечение. Можно использовать специальную версию ClosestIntersection , которая возвращает результат, как только найдёт первое пересечение (и для этого нам нужно вычислять и возвращать не closest_t , а просто булево значение).

Пространственные структуры

Вычисление пересечения луча с каждой сферой - довольно большая лишняя трата ресурсов. Существует множество структур данных, позволяющих одним махом отбрасывать целые группы объектов без необходимости вычисления отдельных пересечений.

Подробное рассмотрение таких структур не относится к тематике нашей статьи, но общая идея такова: предположим, что у нас есть несколько близких друг к другу сфер. Можно вычислить центр и радиус наименьшей сферы, содержащей все эти сферы. Если луч не пересекает эту граничную сферу, то можно быть уверенным, что он не пересекает ни одну содержащуюся в нём сферу, и сделать это можно за одну проверку пересечения. Разумеется, если он пересекает сферу, то нам всё равно нужно проверять, пересекает ли он какую-нибудь из содержащихся в ней сфер.

Подробнее об этом можно узнать, прочитав о иерархии ограничивающих объёмов .

Субдискретизация

Вот простой способ сделать трассировщик лучей в раз быстрее: вычислять в раз пикселей меньше!

Предположим, мы трассируем лучи для пикселей и , и они падают на один объект. Можно логически предположить, что луч для пикселя тоже будет падать на тот же объект, пропустить начальный поиск пересечений со всей сценой и перейти непосредственно к вычислению цвета в этой точке.

Если сделать так в горизонтальном и вертикальном направлениях, то можно выполнять максимум на 75% меньшей первичных вычислений пересечений луч-сцена.

Разумеется, так можно запросто пропустить очень тонкий объект: в отличие от рассмотренных ранее, это «неправильная» оптимизация, потому что результаты её использования не идентичны тому, что бы мы получили без неё; в каком-то смысле, мы «жульничаем» на этой экономии. Хитрость в том, как догадаться сэкономить правильно, обеспечив удовлетворительные результаты.

Другие примитивы

В предыдущих разделах мы использовали в качестве примитивов сферы, потому что ими удобно манипулировать с математической точки зрения. Но добившись этого, можно достаточно просто добавить и другие примитивы.

Заметьте, что с точки зрения TraceRay может подойти любой объект, пока для него нужно вычислять только два значения: значение для ближайшего пересечения между лучом и объектом, и нормаль в точке пересечения. Всё остальное в трассировщике лучей не зависит от типа объекта.

Хорошим выбором будут треугольники. Сначала нужно вычислить пересечение между лучом и плоскостью, содержащей треугольник, и если пересечение есть, то определить, находится ли точка внутри треугольника.

Конструктивная блочная геометрия

Есть очень интересный тип объектов, который реализовать относительно просто: булева операция между другими объектами. Например, пересечение двух сфер может создать что-то похожее на линзу, а при вычитании маленькой сферы из большей сферы можно получить что-то напоминающее Звезду Смерти.

Как это работает? Для каждого объекта можно вычислить места, где луч входит и выходит из объекта; например, в случае сферы луч входит в и выходит в . Предположим, что нам нужно вычислить пересечение двух сфер; луч находится внутри пересечения, когда находится внутри обеих сфер, и снаружи в противоположном случае. В случае вычитания луч находится внутри, когда он находится внутри первого объекта, но не внутри второго.

В более общем виде, если мы хотим вычислить пересечение между лучом и (где - любой булевый оператор), то сначала нужно по отдельности вычислить пересечение луч- и луч- , что даёт нам «внутренний» интервал каждого объекта и . Затем мы вычисляем , который находится во «внутреннем» интервале . Нам нужно просто найти первое значение , которое находится и во «внутреннем» интервале и в интервале , которые нас интересуют:

Нормаль в точке пересечения является или нормалью объекта, создающего пересечение, или её противоположностью, в зависимости от того, глядим ли мы «снаружи» или «изнутри» исходного объекта.

Разумеется, и не обязаны быть примитивами; они сами могут быть результатами булевых операций! Если реализовать это чисто, то нам даже не потребуется знать, чем они являются, пока мы можем получить из них пересечения и нормали. Таким образом, можно взять три сферы и вычислить, например, .

Прозрачность

Не все объекты обязаны быть непрозрачными, некоторые могут быть частично прозрачными.

Реализация прозрачности очень похожа на реализацию отражения. Когда луч падает на частично прозрачную поверхность, мы, как и ранее, вычисляем локальный и отражённый цвет, но ещё и вычисляем дополнительный цвет - цвет света, проходящего сквозь объект, полученный ещё одним вызовом TraceRay . Затем нужно смешать этот цвет с локальным и отражённым цветами с учётом прозрачности объекта, и на этом всё.

Преломление

В реальной жизни, когда луч света проходит через прозрачный объект, он меняет направление (поэтому при погружении соломинки в стакан с водой она выглядит «сломанной»). Смена направления зависит от коэффициента преломления каждого материала в соответствии со следующим уравнением:
Где и - это углы между лучом и нормалью до и после пересечения поверхности, а и - коэффициенты преломления материала снаружи и внутри объектов.

Например, приблизительно равен , а приблизительно равен . То есть для луча, входящего в воду под углом получаем




Остановитесь на мгновение и осознайте: если реализовать конструктивную блочную геометрию и прозрачность, то можно смоделировать увеличительное стекло (пересечение двух сфер), которое будет вести себя как физически правильное увеличительное стекло!

Суперсэмплинг

Суперсэмплинг является приблизительной противоположностью субдискретизации, когда мы стремимся к точности вместо скорости. Предположим, что лучи, соответствующие двум соседним пикселям, падают на два различных объекта. Нам нужно раскрасить каждый пиксель в соответствующий цвет.

Однако не забывайте об аналогии, с которой мы начинали: каждый луч должен задавать «определяющий» цвет каждого квадрата «сетки», через которую мы смотрим. Используя по одному лучу на писель, мы условно решаем, что цвет луча света, проходящего через середину квадрата, определяет весь квадрат, но это может быть и не так.

Решить эту проблему можно трассированием нескольких лучей на пиксель - 4, 9, 16, и так далее, а затем усредняя их, чтобы получить цвет пикселя.

Разумеется, при этом трассировщик лучей становится в 4, 9 или 16 раз медленнее, по той же причине, по которой субдискретизация делает его в раз быстрее. К счастью, существует компромисс. Мы можем предположить, что свойства объекта вдоль его поверхности меняются плавно, то есть испускание 4 лучей на пиксель, которые падают на один объект в немного отличающихся точках, не слишком улучшит вид сцены. Поэтому мы можем начать с одного луча на пиксель и сравнивать соседние лучи: если они падают на другие объекты или их цвет отличается больше, чем на переделённое пороговое значение, то применяем к обоим подразделение пикселей.

Псевдокод трассировщика лучей

Ниже представлена полная версия псевдокода, созданного нами в главах о трассировке лучей:

CanvasToViewport(x, y) { return (x*Vw/Cw, y*Vh/Ch, d) } ReflectRay(R, N) { return 2*N*dot(N, R) - R; } 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 t_max = 1 } else { L = light.direction t_max = inf } # Проверка теней shadow_sphere, shadow_t = ClosestIntersection(P, L, 0.001, t_max) if shadow_sphere != NULL continue # Диффузность 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 = ReflectRay(L, N) 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 } ClosestIntersection(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 and t1 < closest_t closest_t = t1 closest_sphere = sphere if t2 in and t2 < closest_t closest_t = t2 closest_sphere = sphere } return closest_sphere, closest_t } 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 } for x in [-Cw/2, Cw/2] { for y in [-Ch/2, Ch/2] { D = camera.rotation * CanvasToViewport(x, y) color = TraceRay(camera.position, D, 1, inf) canvas.PutPixel(x, y, color) } }
А вот сцена, использованная для рендеринга примеров:

Viewport_size = 1 x 1 projection_plane_d = 1 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 # Наполовину отражающий } light { type = ambient intensity = 0.2 } light { type = point intensity = 0.6 position = (2, 1, 0) } light { type = directional intensity = 0.2 direction = (1, 4, 4) }

Теги: Добавить метки

В данной статье речь пойдёт о применении метода обратной трассировки лучей для визуализации изображений в компьютерных играх. Рассматриваются его преимущества и недостатки по сравнению с традиционной технологией. Рассказывается о концептуальной 3D игре, в которой впервые используется графический движок, полностью построенный на принципе обратной трассировки лучей. Также затрагиваются вопросы развития игровых видео ускорителей.

Традиционная технология

Для тех, кто не знаком с теорией 3D графики, я коротко поясню, в чём заключается метод обратной трассировки лучей, и каково его отличие от традиционного метода игровой графики. В традиционном методе визуализации изображения в компьютерных играх сцена или, если угодно, игровой мир, представляется набором треугольников. Для каждого треугольника задаются текстуры и степень освещённости. Далее треугольники скопом заталкиваются в 3D ускоритель и отрисовываются, примерно, как художник чертит на листе бумаги сплошной треугольник. Отличие состоит в использовании буфера глубины. Буфер глубины требуется, что бы не рисовать треугольники, которые закрыты другими объектами сцены. При отрисовке точек нового треугольника проверяется соответствующее значение буфера глубины. В буфере глубины, или ещё его называют Z-буфер, хранится дальность от наблюдателя до уже нарисованного изображения. Если дальность до точки нового треугольника меньше записанного в Z-буфере значения, то эта точка не накрыта точками более близко расположенных треугольников, и её можно рисовать, при этом так же обновляется значение буфера глубины. Этот метод позволяет построить изображение состоящей из треугольников сцены произвольной сложности. Одно из достоинств этого метода состоит в том, что его можно было реализовать - то есть, визуализировать достаточно содержательную игровую сцену в реальном времени и в высоком разрешении - на "древних" процессорах поколения i386, i486.

Различные способы построения изображения могут отличаться скоростью работы, а так же качеством, реалистичностью или красивостью построенного изображения. Естественно, методы, позволяющие нарисовать более реалистичное изображение, требуют и больших вычислительных ресурсов. Мы, конечно, не рассматриваем заведомо плохие методы, которые и работают медленно, и рисуют плохо. На заре развития индустрии компьютерных игр, когда персональные компьютеры были относительно маломощные, естественно, был выбран самый быстрый, не требовательный к вычислительным ресурсам метод отрисовки, выше упомянутый метод Z-буфера.

Однако, трёхмерная сцена состоит не только из одних геометрических деталей, она не мыслима без света, поскольку иначе мы её просто бы не увидели. А метод Z-буфера позволяет нарисовать только геометрию сцены. Что же делать? Точная физическая модель распространения света очень сложна, речь может идти о неких приближениях к естественному освещению. Требуется, чтобы в затенённых местах, куда не попадают прямые световые лучи, было темно, рядом с источниками света - светло. Для создания реалистичного, с точки зрения освещённости, изображения сцены стали использовать заранее просчитанные текстуры, так называемые lightmap, содержащие значения освещения статических объектов сцены. Такая текстура накладывается в месте с обычной текстурой материала и затемняет её в зависимости от положения объекта на сцене, его освещённости. Естественно, при этом требуется полная статичность сцены и источников света, поскольку просчёт этих lightmap происходит крайне долго. Эта технология используется в компьютерных играх уже много лет, и её использование привело к тому, что трёхмерные игры в части графического движка стали отличаться лишь количеством треугольников и текстур на уровне. Как не было динамических источников света и возможности разрушать уровень, так её и нет, поскольку нет динамического расчёта освещённости-затенённости. Если вы передвинете светильник или закроете окно, освещённость сцены никто не изменит, поэтому такой возможности в играх нет. Есть только так называемые fake решения, когда что-то можно сделать в определённом месте, потому что эта возможность заранее предусмотрена и всё заранее рассчитано.

Только недавно стали появляться тени от динамических моделей, всяких монстров, ботов. Мы ещё затронем тему, каким образом эти тени рассчитываются, но часто они выглядят не естественно, поскольку, например, источников света много, а тень идёт только от одного, резкая и не красивая.
Прогресс игровой графики уже много лет связывают исключительно с появлением новых поколений графических ускорителей. Действительно, оказалось очень удобным переложить работу по рисованию треугольников на акселератор. Задача растеризации и текстурирования треугольника лежит в основе игровой графики, поэтому естественно, что эту очень частную и специфическую операцию удалось кардинально ускорить созданием специально оптимизированного железа. Однако, применение ускорителей привело лишь к улучшению качества изображения, качественным режимам наложения текстур, три линейной и анизотропной фильтрации, возможности использовать большие разрешения и полноэкранное сглаживание изображения. В части расчёта освещённости и динамичности сцены до сих пор ничего не поменялось. Отсутствие динамического света делает уровни современной игры скучными. Статичность освещения и сцены постепенно может надоедать. Как будто время остановилось, и играющие бегают в этом остановленном времени. Сейчас при исследовании возможностей новых ускорителей любят рассматривать экран под лупой, выискивая очередное уже незначительное увеличение качества изображения, которое в игре различить крайне трудно.

Метод трассировки лучей


Интересно, а каким же методом рассчитывают реалистичное освещение при рендеренге реалистичных сцен, мультиков, анимационных сцен, какой принцип лежит в основе построения тех же lightmap? В этой области получил распространение метод трассировки лучей и его модификации.

В обзорах процессоров часто упоминаются результаты тестирования в пакетах 3D графики, таких как 3DMax, LightWave и другие. Замеряется время отрисовки какой-нибудь сложной сцены с реалистичным освещением, отражением и преломлением света. Вот как раз сцена и рисуется с помощью метода трассировки лучей.

В отличие от метода Z-буфера, метод трассировки лучей изначально рассчитан на построение реалистичного изображения со сложной моделью освещения. Принцип обратной трассировки лучей состоит в том, что через каждую точку экрана как бы проводится обратный луч света до пересечения с ближайшим объектом сцены, далее из этой точки проводится луч в направлении источника света, таким образом, моделируется распространение света. Если луч, выпушенный на источник света, ничего не пересекает на своём пути, значит данная точка освещена, иначе она лежит в тени. Если луч попадает на зеркальную поверхность, то, в соответствии с законами оптики, выпускается отражённый луч, что даёт возможность построить отражение. В зависимости от свойств среды, через которую проходит луч, он может преломляться, что позволяет моделировать сложные реалистичные световые эффекты. Этот метод позволяет получить не только тени от объектов, но и рассчитать вторичное освещение, когда отражённый тусклый свет попадает в непосредственно затенённые области и размывает тени.

Однако, легко понять, что этот метод крайне вычислительно сложен. Можно обратить внимание в тестах процессоров в программах 3D моделирования, как долго считается даже 1 кадр. Никаким реальным временем тут и не пахнет. Тем более, раньше на персональных компьютерах он выполнялся ещё медленнее, что не оставляло возможностей для его применения в компьютерных играх.
Но за последние несколько лет мощность персональных компьютеров серьёзно возросла и позволила осуществлять метод трассировки лучей почти в реальном времени, правда, с большими ограничениями по качеству изображения и разрешению.

Поскольку для каждой точки экрана нужно осуществить очень сложную процедуру трассировки луча, то скорость трассировки очень сильно зависит от разрешения экрана, от его площади. То есть, построение изображения размером 1024x768 будет занимать в 10 раз больше времени, чем отрисовка изображения в разрешении 320x240. Реализовать метод трассировки лучей в реальном времени можно, весь вопрос, в каком разрешении и с каким качеством изображения.


До последнего времени трассировка лучей в реальном времени на PC была уделом небольших демо - программ, рисующих красивые изображения, но работающих с низкой скоростью и в низких разрешениях. Таких программ полным полно на www.scene.org . Однако, мне удалось, временно пожертвовав многими прелестями метода трассировки лучей, создать полноценный 3D-движок и на его основе первую компьютерную игру, использующую трассировку лучей в реальном времени.

Concept game с 3D движком на основе метода обратной трассировки лучей

На разнообразных автомобильных выставках демонстрируются так называемые concept-car, реальные прототипы будущих серийных автомобилей. Они крайне дороги, не отлажены с потребительской точки зрения, но олицетворяют собой новые идеи. Я же создал concept-game. Что же получилось реализовать, что бы работало в реальном времени на современных персональных компьютерах?
Для движка на трассировки лучей было изначально установлено два главных требования: чтобы расчёт всей освещённости сцены происходил в реальном времени, и чтобы не использовалась никакая заранее рассчитанная информация об уровне. Всё это должно позволить произвольно изменять уровень в динамике. То, что не могут обеспечить современные движки.
Вкупе с динамическим расчётом освещения отсутствие предварительной информации позволяет довольно просто рисовать бесконечные миры, поскольку нужно хранить только не очень объёмную информацию о геометрии уровня.

Выполнение этих строгих требований на современных процессорах потребовало введения других серьёзных ограничений, к счастью, не принципиальных. Однако, с ростом доступной вычислительной мощности эти ограничения будут сниматься, а суть - оставаться.
В первую очередь, я отказался от моделирования именно земной реальности в пользу инопланетных миров. Это позволило отказаться от использования не очень удобного для рейтрейсинга треугольника в качестве основного примитива для конструирования сцены. Инопланетный мир не обязан быть угловатым, пусть он будет круглым. В качестве примитива для построения сцены была выбрана сфера. Поскольку современные игры должны работать в высоких разрешениях, таких, как 1024x768, пришлось отказаться от расчёта отражений и преломлений, поскольку это очень сильно усложняло обработку соответствующего точке экрана луча. Но с ростом вычислительной мощности можно будет расширить как множество примитивов, так и глубину трассировки луча, то есть, добавить отражения, преломления и т.п.

Итак, каковы основные характеристики VirtualRay - 3D движка, построенного на методе трассировки лучей? На самых современных процессорах для персональных компьютеров он работает с более-менее приемлемой скоростью в разрешении 1024x768x32. Будем исходить, что используется именно это разрешение, поскольку если использовать меньшее разрешение, то параметры производительности могут быть другими.

Отрисовка сцен, состоящих из, может быть, тысяч пересекающихся сфер. В реальности сцена может быть бесконечна, имеется в виду только видимая область.

Покадровый расчёт всей освещённости и затенённости. Все источники света - динамические (даже статические), поскольку они на самом деле динамические, только не изменяющие положение от кадра к кадру.

Попиксельный расчёт освещённости и попиксельное наложение теней, естественно, динамических.

Рендеринг мягких теней на основе физического приближения объёмных источников света. То есть, граница тени не резкая, а сильно размытая, степень размытости можно регулировать. Правда, это не совсем настоящие физически достоверные мягкие тени, а приближённые.

До 8 источников света может освещать одну сферу, соответственно, одна сфера может отбрасывать до 8 теней. Это не принципиальное ограничение, просто, когда в одной области много источников света, всё, конечно, сильно замедляется.

Поддержка точечных источников света и бесконечно удалённых источников света типа солнца. Как правило, сцену освещает один "солнечный" источник света, и несколько локальных.

Полностью динамическая сцена, то есть, положение объектов может меняться произвольным образом.

Наложение и билинейная фильтрация текстур.

Ограниченное использование прозрачных сфер с динамическим коэффициентом прозрачности.

Не очень изысканное изображение поверхности планеты в виде одной большой сферы, образующей эффект горизонта, когда объекты, находящиеся далеко, скрываются за линией горизонта.

К локальным недостаткам движка в первую очередь можно отнести то, что он скуп на "дешёвые" эффекты, вроде спрайтовых вспышек и т.п., которые так замечательно делают современные видео ускорители.
Какую же игру оказалось возможным создать с использованием движка VirtualRay? Вообще, на нём можно сделать великое множество игр, начиная от космического симулятора и заканчивая многопользовательской онлайновой вселенной. Кстати, в последнем типе особенно проявляются преимущества движка по реализации динамического изменения сцены. В качестве "концептуальной игры" я создал проект под названием AntiPlanet - простенький 3D shooter с прямолинейными монстрами с поведением в духе Doom. Уровни к игре представляют собой разного размера куски инопланетной местности, освещаемой местными солнцами. Кстати, Солнце совершает движение по небу в соответствии с которым меняется освещение и затенение сцены. Всего в текущей версии игры доступно 5 уровней, один из которых - indoor, лабиринт из пещер. Остальные, в основном, открытые. Движок достаточно универсален, чтобы без специальных оптимизаций рисовать как открытые, так и закрытые сцены.

Есть 5 видов охотящихся за играющим монстров, монстры отличаются типом используемого оружия, скоростью и силой. Кстати в распоряжении играющего - десять видов оружия, стреляющего разнообразными снарядами, ракетами и бомбами. Сферическая природа оружия делает его несколько однообразным, зато снаряды при взрыве разлетаются на кучу осколков. Есть 3 основных вида игры - просто охота на монстров, когда игроку требуется за определённое время уничтожить определённое количество монстров. Второй вид игры состоит в нахождении спрятанных на уровне специальных артефактов. И в третьем случае играющему просто предстоит выжить определённое время на неизвестной планете. При выборе игры можно самому установить количество аптечек, оружия и монстров на уровне, они будут расставлены в случайные места. Конечно, если задать очень большое количество монстров, игра будет работать медленно.

К сожалению, игра не раскрывает весь потенциал движка, поскольку мы с моделлером просто не успели это сделать. Например, нет разрушения уровня, только отдельные динамические части, поскольку тогда тупые монстры дороги не найдут, с другой стороны, это не предусмотрено идей игры. Не полностью реализованы возможности движка по части анимации моделей. Движок позволяет произвольное независимое изменение моделей на каждом кадре, что делает возможным реализацию самой изощрённой анимации.
Я решил не приводить ни каких скриншотов из игры, поскольку они совершенно не передают достоинства движка, как-то динамическое освещение и мягкие тени. Скачайте демо-версию, она занимает всего несколько мегабайт. А так представьте себе сюрреалистическую инопланетную местность, состоящую из огромного количества шаров, монстров из небольших сфер, которые при взрыве разлетаются на мелкие кусочки. Скачать текущую демо-версию можно по этой ссылке .

Для игры требуется Windows95 и выше, желательно больше 128 мегабайт памяти, иначе отключите музыку, DirectX, видео карта с поддержкой 32-битного цвета, и, самое главное, процессор помощнее. Например, процессор Intel Pentium 4 с поддержкой технологии Hyper-Threading, или новый AthlonXP. Игра должна запускаться на любом процессоре с технологией MMX, однако для полной функциональности нужна поддержка SSE, то есть, процессор начиная с Pentium-III. Видео ускоритель не требуется. Кстати, движок поддерживает многопроцессорность, в том числе, и технологию Hyper-Threading. Не вся программа использует несколько потоков для успешного использования Hyper-Threading, но главный цикл трассировки лучей распараллелен, и достигается выигрыш в несколько десятков процентов. А на многопроцессорной системе выигрыш пропорционален количеству процессоров.

Развитие движка VirtualRay

На сегодняшний момент движок позволяет отображать фантастические сцены. Но использование в качестве примитивов сфер не есть принципиальное ограничение, просто на текущий момент использование более сложных примитивов проблематично с точки зрения скорости. С ростом производительности можно реализовать обработку эллипсов вместо сфер, что позволит обогатить сцены.

И сейчас можно реализовать обработку треугольников вместе со сферами. Но чтобы отобразить нечто содержательное с помощью треугольников, их нужно очень много, а обрабатываются они со скоростью сфер. Несложно расширить множество примитивов за счёт введения сфер с вырезами, сферических сегментов и сферических треугольников. Но это тоже негативно скажется на скорости.
Есть различные методы повышения производительности рейтрейсинга за счёт ухудшения качества изображения. Трассируются не все лучи, а только наиболее важные, а для построения недостающей части изображения использования интерполяция. Однако, такой подход применим не ко всем сценам, иногда он может давать более-менее качественные результаты, а иногда сильно портить изображение.

Кстати, о качестве. Тут есть большой резерв для улучшения. Дело в том, что процедура текстурирования выполняется всего один раз на точку и не отнимает много времени. Сейчас на текстурирование тратится около 10% времени рендеренга. Так что, для улучшения качества текстурирования планируется реализовать попиксельную трилинейную фильтрацию, это не должно существенно понизить скорость.

Рейтрейсинг и современные 3D-ускорители

Последнее время индустрия 3D-ускорителей совершает переход к повсеместному использованию так называемых пиксельных и вертексных шейдеров. При растеризации треугольника для каждого фрагмента изображения ускоритель выполняет заранее заданную программку, которая изменяет сложным образом цвет фрагмента. Она может ещё много чего делать, например, какие-то промежуточные вычисления записывать в текстуры, которые потом будут читаться и использоваться при отрисовке чего-то другого. Типичным примером современного пиксельного, или как его ещё называют, fragment шейдера является шейдер, вычисляющий освещение данной точки треугольника. Он устроен следующим образом: берётся вектор - глобальная позиция источника, берётся текущая координата точки треугольника в трёхмерном пространстве, которая вычисляется в чипе ускорителя при растеризации треугольника, и нормаль к треугольнику в данной точке. Далее вычисляется вектор из данной точки в направлении на источник света и в зависимости от угла, который он образовывает с нормальным перпендикулярным вектором, высчитывается освещённость. Чем под большим углом падает свет, тем меньше его интенсивность.
Как мы видим, современный шейдер может быть содержательной геометрической программой. Сейчас принято тестировать новые ускорители путём измерения скорости выполнения таких вот и более сложных шейдеров. Производительность получается очень большой. Шейдер, выполняющий попиксельное освещение работает в разрешении 1024x768 со скоростью 100-200 кадров в секунду на последних акселераторах, таких, как Radeon9700 или GeForceFX. Имеется ввиду только время работы непосредственно шейдера. В связи с этим давно уже появилась мысль использовать такую немалую вычислительную мощность в самых разнообразных целях, даже далёких от 3D графики. И, в том числе, попытаться использовать для реализации метода трассировки лучей.

Однако, если рассмотреть эту мощность с точки зрения количества скалярных и векторных вычислений с плавающей точкой в единицу времени, то она оказывается сравнимой с вычислительной мощностью современных процессоров. Возьмём самый новый на сегодняшний день ускоритель GeForceFX5900Ultra, он имеет частоту 450MHz, 4 пиксельных процессора, каждый из которых может совершать 1 векторную операцию за такт. На самом деле, операций за такт может быть больше, но нам интересны только вычисления с полной точностью float32, поскольку вычисления с меньшей точностью имеют смысл в основном для вычисления цвета, диапазон которого всё равно ограничен не очень большим цветовым разрешением монитора. А для геометрических расчётов требуется хорошая точность. Получается 450Mx4=1800 миллионов векторных операций в секунду как грубая оценка производительности. Если же взять Pentium 4, то при использовании SSE можно достичь одной векторной операции за полтора такта, то есть при частоте 2700MHz получим те же 1800 миллионов векторных операций в секунду. В обоих случаях имеет в виду, естественно, пиковая производительность, когда весь код только и состоит из вычислений.
Видно, что превосходства в вычислительной мощности у VPU нет. Его преимущество в графике заключается в умении параллельно с вычислениями шейдера производить сопутствующие вычисления, необходимые для растеризации треугольника. Как-то вычислять значение буфера глубины, интерполировать по поверхности треугольника заданные в вершинах значения, и осуществлять за такт выборку и фильтрацию текстур. Всё это осуществляется различными параллельно работающими блоками видео ускорителя.

Так что никакого особого преимущества в реализации трассировки лучей от использования видео ускорителя мы, естественно, не получим, поскольку ускоритель полностью оптимизирован и построен с точки зрения оптимизации рисования треугольников.
Относительно оптимизации трассировки лучей с помощью видео ускорителя есть ещё такая идея: нарисовать всю геометрию на VPU, а расчёт освещения методом трассировки лучей выполнить посредством CPU, а затем скомбинировать результат. Но толку от этого особого не будет, потому, что основные вычислительные сложности приходятся как раз на вычисление освещения. Причём, чем сложнее будет сцена и, соответственно, больше выигрыш от использования VPU, тем больше ресурсов потребуется для расчёта освещения сложной сцены, и рисование сцены будет занимать значительно меньше времени относительно времени расчёта освещения.

Расчёт освещения с помощью современных ускорителей

Хорошо, а каким же образом предлагается рассчитывать затенённость сцены в новых играх с динамическими источниками света, такими, как Doom III? Неужели мы теперь навсегда обречены видеть в компьютерных играх заранее рассчитанное статическое освещение? Нет, давно известны интересные методы расчёта теней на основе использования стандартного метода рисования текстурированых треугольников с помощью z-буфера. Известны они давно, но они такие требовательные к вычислительным ресурсам, что их применение в компьютерных играх, и то, ограниченное, стало возможным только недавно с появлением нового поколения видео ускорителей.

Рассмотрим для начала метод, с помощью которого рисуются динамические тени в вышеупомянутой игре Doom III. Игре, которую очень ждут многие геймеры. Этот метод называется методом Теневых объёмов, или методом отрисовки теней с помощью стенсил - буфера. Вот принципиальная схема его работы: сначала рисуется не освещённая сцена, далее для каждого отбрасывающего тень объекта сцены строится его теневой объём. Теневой объём - это фигура, ограничивающая теневую область, ту область пространства, в которую не попадает свет, которая затенена данным объектом. Мы как бы представляем простирающуюся за объектом черноту в виде тела. Теневой объём можно даже в реальности увидеть, если осветить резким светом комнату, в которой летают частички пыли. Не затенённые частицы будут светиться, а затенённые будут образовывать черную область за загораживающим свет объектом. Следующий шаг состоит в отрисовке треугольников, составляющих границу этого теневого объёма. Путём сравнения значения буфера глубины с глубиной передней и задней стенок теневого объёма определяется, лежит ли данная точка в теневом объёме и, таким образом, затеняется, или нет. Вот при сравнении глубины стенок теневого объёма и глубины изображения используется стенсил буфер - отвечающий экранным пикселям массив значений. В нём хранятся промежуточные результаты сравнения глубины стенок теневого объёма с глубиной изображения. Этот метод "хорош" тем, что вовсю использует fillrate ускорителя, поскольку теневые объёмы, как правило, имеют большую площадь на экране, чем отбрасывающий тень объект. Метод был доступен для реализации ещё на ускорителях Riva TNT2, но он такой требовательный, что его применение стало возможным только недавно.

С другой стороны, построение оптимальных теневых объёмов для сложных не выпуклых объектов является непростой с вычислительной точки зрения задачей. Решение "в лоб" приведёт к возникновению большого количества лишних стенок теневого объёма, отрисовка которых потребует дополнительных ресурсов. Время нахождения эффективного объёма очень быстро растёт со степенью детализации модели. Возможно, именно благодаря этому модели монстров в NewDoom менее детализированы, чем ожидалось.
Но это ещё не все недостатки. У многих небольших объектов площадь стенок теневого объёма может достигать гигантской величины. Например, у расчески. Её теневая область не велика, но очень извилиста. Далее, метод не очень хорошо совместим с прозрачными поверхностями. Например, если в теневой объём попадает прозрачная поверхность, тогда находящийся за ней объект не оставляет своей информации в буфере глубины, поскольку эта информация затёрлась глубиной прозрачной поверхности. И определить, лежит ли объект в теневом объёме, невозможно. Все случаи такого рода придётся обрабатывать отдельно, что будет приводить к увеличению количества проходов рендеренга.

Данный метод трудно усовершенствовать для получения размытых теней. Те, кто смотрел предварительную версию Doom III, могли обратить внимание на резкость теней. И, собственно, этот метод годится только для рисования теней, вторичное освещение с его помощью не рассчитаешь, преломление и отражение света тоже. Просто в лоб рисуется теневой конус объекта и всё.

Другой популярный способ изображения динамических теней в современных играх заключается в использовании проективного наложения текстур. Современные ускорители научились проецировать текстуру на объект, как диапроектор проецирует слайд на экран. Просто при рисовании объекта вычисляется, какая точка текстуры проецируется в данную точку объекта. Теперь можно, смотря из источника света, нарисовать объект чёрным цветом в текстуру, получится теневой силуэт. Всё равно, что тень от предмета на вертикальной белой стене. И эту текстуру с тенью называют теневой маской, её можно спроецировать на затеняемые объекты.

Именно этот метод используется в новых играх для изображения теней от динамических объектов, монстров, машин. С помощью него можно рисовать размытые тени, для этого исходная текстура с тенью размывается, превращаясь из чёрно-белой в бело-серую.

Я даже не знаю, какой из выше описанных методов более требователен к fillrate ускорителя. Дело в том, что для получения хорошего качества тени, требуется, чтобы теневая текстура была очень большого разрешения. В новых играх, вроде Splinter Cell, используются текстуры размером несколько тысяч пикселей. Причина заключается в том, что при проецировании самые мелкие детали многократно увеличиваются в размере. Становятся видны составляющие изображение пиксели. Таким образом, этот метод можно использовать только для наложения теней на близко расположенные объекты. Вторым недостатком этого метода является невозможность самозатенения объекта, требуется точно выделять отбрасывающий тень объект, и его части не будут отбрасывать тень друг на друга. И в дополнение, естественно, никакого обобщения для расчёта вторичной освещённости, отражений и преломления света, этот метод не предполагает.

И, наконец, рассмотрим самый, на мой взгляд, перспективный для использования в современных играх метод построения теней. Он является развитием предыдущего проективного метода. Только вместо силуэта объекта в теневую текстуру записывается расстояние от точек объекта до источника света. Далее, при проецировании теневой текстуры эта информация используется для определения, лежит ли точка потенциально затеняемого объекта дальше, или ближе к источнику света, чем затенитель. Преимущество этого метода состоит в корректном самозатенении объекта. А недостатки у него аналогичны предыдущему методу. Этот способ построения динамических теней не пользуется популярностью у разработчиков игр. "Вина" метода заключается в том, что он требует специфических возможностей видеокарты, которые впервые появились в GeForce3 - Geforce4, но были изъяты из Geforce4MX - сокращённой версии Geforce4. Без поддержки железа метод реализовать невозможно, так что приходится использовать способ, осуществимый на всех популярных видео картах.

Преимущество всех выше названных методов заключается в хорошей совместимости с существующим "железом". Для них, по сути, ничего не надо, кроме fillrate и простейших операций. В итоге, можно сделать вывод о том, что видео ускорители даже сейчас далеки от расчёта освещения сцены в реальном времени. И ничего революционного не предвидится. Появились тени от некоторых динамических объектов, ограниченный динамический свет в новом Doom III, вот эти технологии будут осваиваться в течение долгого периода времени.

Развитие ускорителей с точки зрения рейтрейсинга

Как я уже упоминал, современные ускорители становятся всё более и более программируемыми и мощность их неуклонно растёт. Производители видеокарт даже используют термин "Визуальный процессор" применительно к новым изделиям. Действительно, по своим возможностям, ускорители всё больше и больше напоминают обычные процессоры для персональных компьютеров. Вот именно с увеличением степени программируемости VPU связываются надежды по реализации интеллектуальных методов построения изображений, таких, как метод трассировки лучей. Что бы ускоритель можно было перепрограммировать подходящим образом.

Оценим перспективы развития ускорителей в этом направлении. Сейчас новейшие ускорители работают на частотах около 500MHz, как процессоры пятилетней давности, и имеют 4-8 параллельно работающих конвейеров. Сейчас большинство шейдерных векторных операций, сложение, скалярное произведение, выполняется за такт. Многие вспомогательные операции, вроде интерполяции значений по поверхности треугольника, тоже выполняются за такт. Вычисление тригонометрических функций, таких, как sin и cos, правда приближённое, выполняется тоже за такт. При этом используются выборки из таблиц с заранее просчитанными значениями, но, всё равно, производительность удивительна. Тем более, странно, что современные CPU для персональных компьютеров ничего подобного не умеют. Наоборот, наблюдается тенденция по избавлению от сложных команд и замене их несколькими простыми. Эти меры требуются для возможности наращивания частоты. Не вдаваясь в технические тонкости, можно сказать, что всё более и более уменьшающийся с ростом частоты процессорный такт требует более коротких команд. Сложные инструкции всё равно расщепляются внутри современных процессоров на микро операции. Это расщепление - тоже отдельная проблема, ей занимаются целые блоки процессора.

А что же видео ускорители? Вероятно, что для увеличения частоты придётся серьёзно переработать архитектуру современных VPU. Но это ещё полбеды. Для истинной программируемости требуется исполнение процессором ветвлений, то есть, команд управления выполнением программы. А с этим - всегда самые большие проблемы. Как современные процессоры страдают от непредсказуемых условных переходах в программах? Вот вершинные шейдеры в GeForceFX получили команды условных переходов, вы можете посмотреть свежие тесты, как сильно "просела" производительность. И это на сравнительно невысокой частоте ниже 500MHz. А с ростом частоты потери от условных переходов только увеличатся, да и сама их реализация - труднее. Кстати, фантастическая производительность акселераторов достигается при исполнении так называемых потоковых операций, когда данные идут сплошной полосой и обрабатываются по жёстко определённой схеме, никаких тебе случайных условных переходов и т.п. Все эти факты говорят о том, что увеличения частоты видео ускорителей ожидать в ближайшее время не приходится.

Важным параметром видеокарты является количество пиксельных процессоров. Они параллельно закрашивают пиксели, поэтому, чем их больше, тем лучше. На самых новых Radeon их аж восемь. От новых ускорителей ожидают всё большего количества fragment процессоров. Но не всё так просто. Дело в том, что когда размеры треугольника соизмеримы с количеством пиксельных конвейеров, они не могут все работать вместе. В маленьком треугольнике им не хватает места. В том числе, по этой причине производители видео ускорителей так любят режимы анти-алиасинга с отрисовкой всей сцены с удвоенным разрешением. Тогда маленькие треугольники становятся больше. Действительно, если сцену из больших треугольников разбить на более мелкие без изменения формы, то производительность выполнения пиксельных шейдеров существенно снизится, хотя общая площадь треугольников останется прежней.

Развитие современных ускорителей игровой графики и так сопряжено с большими трудностями, и идёт практически только за счёт усовершенствования технологического процесса производства видео чипов. Вся NVIDIA и ATI думают о том, как сделать эффективно простые динамические тени. Хорошего решения нет - им не до рейтрейсинга.

Специализированный ускоритель для рейтрейсинга

Если современные игровые VPU изначально проектировались для ускорения стандартного алгоритма рисования треугольников и мало пригодны для реализации трассировки лучей, то, может быть, имеет смысл изначально строить ускоритель для реализации рейтрейсинга? Увы, ускорять трассировку лучей - неблагодарное занятие.


Алгоритм трассировки лучей такой сложный, что ускоритель рейтрейсинга это почти что универсальный процессор. Аппаратному ускорению хорошо поддаются потоковые алгоритмы без случайных ветвлений, а рейтрейсинг совершенно не такой. То есть, сделать ускоритель трассировки лучей всё равно, что создать настоящий CPU.


Зато у трассировки лучей есть другое достоинство - она хорошо распараллеливается. Каждый луч можно рассчитывать независимо, что позволяет эффективно реализовать алгоритм на мультипроцессорных системах. В качестве дешевого ускорителя трассировки лучей можно рассматривать знаете что? Систему на четырёх Celeron частотой от 3 гигагерц, или четырёх AthlonXP с урезанным кэшем. Алгоритм трассировки лучей при правильной оптимизации не требователен к большому размеру кэша, так что получится дёшево и многофункционально. Совокупная вычислительная мощность будет намного превосходить текущие настольные компьютеры. Но этого не будет, поскольку многопроцессорные системы предназначены для другого рынка, не для домашних систем.

Заключение

На основании всего вышесказанного, можно сделать вывод, что реалистичная визуализация сцен в реальном времени на персональных компьютерах очень сложна, причём многие проблемы носят принципиальный характер. Должно пройти достаточно времени, что бы игровая компьютерная графика вышла на качественно новый уровень. И какие методы будут использоваться для визуализации в будущих графических приложениях сейчас сказать очень сложно.

Ссылки


http://www.art-render.com/

Сайт производителей "ускорителей рейтрейсинга" для оптимизации рендеренга в 3DMax и других графических редакторах. Ускоритель - это набор нескольких, от 8, оптимизированных для рейтрейсинга процессоров. Они умеют выполнять типичную для трассировки операцию - находить пересечение луча с треугольником - за один такт. Но, по-видимому, работают на не очень высокой частоте. Ускорение достигается за счёт параллельной работы. Сейчас на сайте трудно найти цены, но раньше я их видел, они совсем не маленькие.


http://www.acm.org/tog/resources/RTNews/html

Обширный список разнообразных ресурсов на тему трассировки лучей.


http://www.realstorm.com/

Движок на основе трассировки лучей. Позволяет рисовать в реальном времени большое количество типичных для трассировки эффектов, отражения и преломления света, например. Но работает в небольших разрешениях и использует аппроксимацию. На основе движка построена игра - симулятор боулинга.


http://www.kge.msu.ru/workgroups/compcenter/dmitri/projects/sphericworld/index.htm

http://www.kge.msu.ru/workgroups/compcenter/dmitri/projects/polyworld/index.htm

Ещё один проект, посвящённый методу трассировки лучей. Реализован сферический и полигональный рейтрейсер, строящий очень качественные реалистичные изображения, но медленно в больших разрешениях.


http://www.virtualray.ru/

Это, собственно, сайт, посвящённый предмету статьи - движку VirtualRay и игре AntiPlanet - первому 3D shooter на основе ray trace движка.

Прямая трассировка . В методе прямой трассировки генерируется пучок лучей, выходящих из источника во всевозможных направлениях.

Большинство лучей, испущенных источником, не попадает в приемник, а значит, и не влияет на формируемое в нем изображение. Лишь очень малая часть лучей после всех отражений и преломлений в конце концов попадает в приемник, создавая изображение сцены в его рецепторах. На шероховатых поверхностях возникает множество диффузно отраженных лучей. Все их нужно программно генерировать и отслеживать, что лавинообразно усложняет задачу трассировки.

Прохождение луча в неидеальной среде сопровождается рассеянием и поглощением световой энергии на ее микрочастицах. Эти физические процессы чрезвычайно трудно адекватно моделировать на ЭВМ с ее конечными вычислительными ресурсами. На практике ограничиваются применением коэффициента затухания энергии луча на единицу пройденного им расстояния. Аналогично вводятся коэффициенты уменьшения энергии луча при его отражении и преломлении на поверхности раздела сред. С учетом этих коэффициентов отслеживается уменьшение энергии всех первичных и вторичных лучей в процессе их блуждания в пространстве сцены. Как только энергия некоторого луча становится меньше заданного абсолютного уровня или уменьшается в заданное число раз, трассировка данного луча прекращается.

Таким образом, главными недостатками метода прямой трассировки являются его большая трудоемкость и малая эффективность. При реализации метода большая часть работы по расчету пересечений лучей с объектами оказывается проделанной впустую.

Обратная трассировка. Метод обратной трассировки разработан в 80-х годах. Основополагающими считаются работы Уиттеда и Кея.

Для отсекания лучей, не попавших в приемник, достаточно рассматривать наблюдателя в качестве источника обратных лучей. Первичным лучом будет считаться луч V от наблюдателя к какой-либо точке на поверхности объекта.

По рассмотренным выше методикам рассчитываются вторичные, третичные и т.д. лучи. В результате для каждого первичного луча строится дерево трассировки, ветви которого составляют вторичные лучи. Ветвление трассы заканчивается, если:

● луч выходит за пределы сцены,

● луч встречается с непрозрачным телом, поглощающим свет,

● луч попадает в источник света,

● интенсивность луча падает ниже порога чувствительности,

● число расщеплений первичного луча становится слишком большим для имеющихся машинных ресурсов.

Результирующая прямая световая энергия (цвет и интенсивность), попавшая в приемник из направления V , слагается из энергий терминальных вершин дерева с учетом их потерь при распространении в оптических средах.


Метод обратной трассировки фактически аккумулирует все лучи, в действительности приходящие в приемник из определенного направления независимо от их начала. Это позволяет видеть и изображать на экране:

● непрозрачные объекты, поглощающие обратные лучи;

● прозрачные объекты, через которые благодаря преломлению наблюдателю видны другие объекты;

● отражения объектов на зеркальных поверхностях, в том числе и блики, соответствующие попаданию обратных лучей в источник света;

● тени, образующиеся в точках поверхности, заслоненных от источника другими объектами;

● другие разнообразные оптические эффекты.

Количество "зондирующих" обратных лучей, подвергаемых трассировке, ограничено числом точек на поверхностях объектов сцены, видимых из точки расположения наблюдателя и перебираемых с конечным шагом, зависящим от разрешения экрана. Благодаря этому объем вычислительных затрат в методе обратной трассировки существенно уменьшается по сравнению с методом прямой трассировки. Возможно комбинирование обоих методов для оптимизации алгоритмов и снижения их трудоемкости.

Алгоритмы трассировки носят характер рекурсивной процедуры, которая вызывает саму себя при появлении вторичного луча (анализируемый луч отражается или преломляется). Большая часть вычислений при реализации методов трассировки приходится на расчет пересечений лучей с поверхностями, в связи с чем они применяются для изображения оптических эффектов в сценах с небольшим числом объектов.

При практической реализации метода обратной трассировки вводят нижеприведенные ограничения. Некоторые из них необходимы, чтобы можно было в принципе решить задачу синтеза изображения, а некоторые ограничения позволяют значительно повысить быстродействие трассировки.

Ограничения метода обратной трассировки:

1. Среди всех типов объектов выделим источники света. Они могут только излучать свет, но не могут его отражать или преломлять. Обычно рассматриваются точечные источники.

2. Свойства отражающих поверхностей описываются суммой двух компонентов: диффузного и зеркального.

3. Зеркальность, в свою очередь, также описывается двумя составляющими. Первая (reflection) учитывает отражение от других объектов, не являющихся источниками света. Строится только один зеркально отраженный луч r для дальнейшей трассировки. Вторая компонента (specular) означает световые блики от источников света. Для этого направляются лучи на все источники определяются углы, образуемые этими лучами с зеркально отраженным лучом обратной трассировки (r ). При зеркальном отражении цвет точки поверхности определяется цветом того, что отражается. В простейшем случае зеркало не имеет собственного цвета поверхности.

4. При диффузном отражении учитываются только лучи от источников света. Лучи от зеркально отражающих поверхностей игнорируются. Если луч, направленный на данный источник света, закрывается другим объектом, значит, данная точка объекта находится в тени. При диффузном отражении цвет освещенной точки поверхности определяется собственным цветом поверхности и цветом источников света.

5. Для прозрачных (transparent) объектов обычно не учитывается зависимость коэффициента преломления от длины волны. Иногда прозрачность вообще моделируют без преломления, т.е. направление преломленного луча t совпадает с направлением падающего луча.

6. Для учета освещенности объектов светом, рассеиваемым другими объектами, вводится фоновая составляющая (ambient).

7. Для завершения трассировки вводят некоторое пороговое значение освещенности, которое уже не должно вносить вклад в результирующий цвет, либо ограничивают число итераций.

Положительные черты метода обратной трассировки:

● универсальность, применимость для синтеза изображений достаточно сложных пространственных сцен. Воплощает многие законы оптики. Просто реализуются разнообразные проекции;

● даже усеченные варианты данного метода позволяют получить достаточно реалистичные изображения. Например, если ограничиться только первичными лучами (из точки проецирования), то это дает удаление невидимых точек. Трассировка уже одного-двух вторичных лучей дает тени, зеркальность, прозрачность;

● все преобразования координат (если таковые есть) линейны, поэтому достаточно просто работать с текстурами;

● для одного пиксела растрового изображения можно трассировать несколько близко расположенных лучей, а потом усреднять их цвет для устранения эффекта ступенчатости;

● поскольку расчет отдельной точки изображения выполняется независимо от других точек, то это может быть эффективно использовано при реализации данного метода в параллельных вычислительных системах, в которых лучи могут трассироваться одновременно.

Недостатки метода обратной трассировки:

● проблемы с моделированием диффузного отражения и преломления;

● для каждой точки изображения необходимо выполнять много вычислительных операций. Трассировка лучей относится к числу самых медленных алгоритмов синтеза изображений.



Предыдущая статья: Следующая статья:

© 2015 .
О сайте | Контакты
| Карта сайта