Написание базового трассировщика лучей
Это финальная часть цикла статей "Введение в трассировку лучей", но не последняя на тему создания 3D изображений. Исходный код доступен для скачивания. В этой версии трассировщика мы сделали свет видимым (сферой), чтобы его отражение появлялось в отражающих шарах. Иногда трудно разглядеть, когда стеклянные сферы прозрачные (белые), поэтому в нашем примере мы слегка подкрасили их (красным). В реальном мире прозрачное стекло не обязательно видно. Это зависит от окружающей среды. Обратите внимание, что полученное изображение должно быть более точным. Тень под прозрачной красной сферой не должна быть полностью непрозрачной. На следующих уроках мы узнаем, как быстро исправить эту визуальную неточность. Мы также внедрили другие функции, такие как имитация Френеля (используя технику, называемую соотношением сторон) и преломление света. Все эти вещи будут изучены позже, так что не стесняйтесь, если вам нужна помощь в их четком понимании. По крайней мере, теперь у вас есть небольшая программа, с которой можно поиграть.
Чтобы скомпилировать программу, загрузите исходный код на свой жесткий диск. Вам понадобится компилятор c++ (например, clang++). Этой программе не нужно ничего особенного для компиляции. Возьмите терминал (например, GitBash для Windows или просто терминал под Linux или macOS) и введите следующую команду (если вы используете gcc), в которой находится ваш файл:
c++ -O3 -o raytracer raytracer.cpp
Если вы используете clang, вместо этого используйте следующую команду:
clang++ -O3 -o raytracer raytracer.cpp
Чтобы создать изображение, запустите программу, набрав ./raytracer
в командной строке. Подождите несколько секунд. Когда программа выполнится, у вас на диске должен быть файл с именем untitled.ppm
. Вы можете открыть этот файл с помощью Photoshop, Preview (на Mac) или Gimp.
Вот одна из возможных реализаций классического рекурсивного алгоритма трассировки лучей в псевдокоде:
#define MAX_RAY_DEPTH 3
color Trace(const Ray &ray, int depth)
{
Object *object = NULL;
float minDist = INFINITY;
Point pHit;
Normal nHit;
for (int k = 0; k < objects.size(); ++k) {
if (Intersect(objects[k], ray, &pHit, &nHit)) {
// начало луча = положению глаз наблюдателя
float distance = Distance(ray.origin, pHit);
if (distance < minDistance) {
object = objects[i];
minDistance = distance;
}
}
}
if (object == NULL)
return 0;
// если материал объекта - стекло, разделите луч на отраженный
// и преломлённый
if (object->isGlass && depth < MAX_RAY_DEPTH) {
// вычисление отражения
Ray reflectionRay;
reflectionRay = computeReflectionRay(ray.direction, nHit);
// рекурсивно
color reflectionColor = Trace(reflectionRay, depth + 1);
Ray refractioRay;
refractionRay = computeRefractionRay(
object->indexOfRefraction,
ray.direction,
nHit);
// рекурсивно
color refractionColor = Trace(refractionRay, depth + 1);
float Kr, Kt;
fresnel(
object->indexOfRefraction,
nHit,
ray.direction,
&Kr,
&Kt);
return reflectionColor * Kr + refractionColor * (1-Kr);
}
// объект - это рассеянный непрозрачный объект
// расчитываем освещение
Ray shadowRay;
shadowRay.direction = lightPosition - pHit;
bool isShadow = false;
for (int k = 0; k < objects.size(); ++k) {
if (Intersect(objects[k], shadowRay)) {
// точка попадания находится в тени, так что возвращаем 0
return 0;
}
}
// точка освещена
return object->color * light.brightness;
}
// для каждого пикселя изображения
for (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
// вычислияем направление основного луча
Ray primRay;
computePrimRay(i, j, &primRay);
pixels[i][j] = Trace(primRay, 0);
}
}
Минимальный трассировщик лучей
Алгоритм трассировки лучей Хекберта
Много лет назад исследователь Пол Хекберт написал трассировщик лучей, который мог бы "поместиться на визитной карточке". Идея состояла в том, чтобы написать минимальный трассировщик лучей на C / C++, настолько маленький, что он мог бы распечатать его на обратной стороне своей визитной карточки (более подробную информацию об этой идее можно найти в статье, которую он написал в Graphics Gems IV). С тех пор многие программисты пробовали это упражнение по кодированию. Ниже вы можете найти версию, написанную Эндрю Кенслером. Изображение выше - результат работы его программы. Обратите внимание на эффект глубины резкости (объекты на расстоянии становятся размытыми). Создание достаточно сложного изображения с помощью такого небольшого количества строк кода - это фантастика.
// minray > minray.ppm
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
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<p)t=p,n=v(0,0,1),m=1;for(i k=19;k--;)for(i j=9;j--;)if(G[j]&1<<k){v p=o+v(-k,0,-j-4);f b=p%d,c=p%p-1,q=b*b-c;if(q>0){f s=-b-sqrt(q);if(s<t&&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);}}
Чтобы запустить программу, скопируйте/вставьте код в текстовый файл (переименуйте файл, например, minray.cpp
или все, что вам нравится), затем скомпилируйте код (c++ -O3 -o minray minray.cpp
или clang++ -O3 -o minray minray.cpp
если вы предпочитаете компилятор clang) и запустите это с помощью командной строки: minray > minray.ppm
. Вместо записи окончательных данных изображения на диск, что сделало бы код длиннее, данные просто записываются в стандартный вывод (оболочку, из которой вы запускаете программу), который мы можем перенаправить (со знаком >) в файл. Файл PPM можно прочитать с помощью Photoshop.
Эта программа представлена здесь только для того, чтобы показать, что алгоритм трассировки лучей может быть реализован с помощью очень небольшого количества строк кода. Многие методы, используемые в коде, объясняются в следующих уроках этого раздела.