Написание базового трассировщика лучей

Это финальная часть цикла статей "Введение в трассировку лучей", но не последняя на тему создания 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); 
    } 
}
Рисунок 1: Результат работы нашего алгоритма трассировки лучей.
Рисунок 1: Результат работы нашего алгоритма трассировки лучей.

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

Рисунок 2: Результат нашей работы с Полом
Рисунок 2: Результат нашей работы с Полом

Алгоритм трассировки лучей Хекберта

Много лет назад исследователь Пол Хекберт написал трассировщик лучей, который мог бы "поместиться на визитной карточке". Идея состояла в том, чтобы написать минимальный трассировщик лучей на 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.

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

Комментарии (2)


  1. noRoman
    04.10.2023 14:12

    Здесь (https://github.com/Mark-Kovalyov/CardRaytracerBenchmark/tree/master) можно рассмотреть реализации raytracers на других языках


    1. IvanNoskov Автор
      04.10.2023 14:12

      Да, спасибо за дополнение