1. Wstęp

Raymarching jest to technika generowania obrazów 3D, bazująca na zupełnie innym podejściu niż grafika 3D generowana na podstawie obiektów trójwymiarowych. Ta technika używana jest szczególnie w intrach 4kb, gdzie cała grafika jest generowana przy pomocy PixelShadera. Raymarching można go nazwać innym podejściem do Raytracingu, gdzie zamiast wyliczać punkt przecięcia promienia z obiektem na scenie wykonujemy spacer od obserwatora szukając miejsca przecięcia z obiektem sceny.

2. Algorytm Raymarching’u – spacer od obserwatora

Poniżej prezentuje kod shadera realizującego operację ray marchingu, przy jego pomocy otrzymujemy prosty sześcian (można zaszaleć). W przykładowym kodzie najważniejsze są dwie funkcje: rayMatching(vec3 ray, vec3 rayDir) – gdzie: ray – pozycja obserwatora (kamery), rayDir – kierunek patrzenia map(vec3 p) – gdzie p – aktualny point w przestrzeni 3D.
#ifdef GL_ES
precision mediump float;
#endif

uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;

const float PI = 3.14159;
const int MAX_STEPS = 128;
const float STEPS_DELTA=1.0/32.;
const float MAX_DISTANCE = 100.0;
const float EPSILON = 0.01;

//-------------------------------------
// Struktury danych
//-------------------------------------
struct Material
{
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
    int texture;
};

struct Object
{
    float distance;
    float step;
    vec3 pos;
    vec3 normal;
    bool hit;
    Material mat;
};

//-------------------------------------
// Obiekty 
//-------------------------------------
vec2 box(vec3 p, vec3 pos, vec3 size, float material){
    vec3 d = abs(p-pos) - size;
    float distance=min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0));
    return vec2(distance, material);
}

//-------------------------------------
// Mapowanie sceny
//-------------------------------------
//
// Zwraca wektor 2 wymiarowy gdzie:
// x: dystans
// y: id materiału
//
vec2 map(vec3 p)
{
    return box(p, vec3(0.0,0.0,0.0), vec3(1.0,1.0,1.0), 0.0);
}

//-------------------------------------
// Ray Marching
//-------------------------------------
vec3 pointNormal(vec3 pos)
{
    vec3 eps = vec3(EPSILON, 0.0, 0.0 );
    vec3 nor = vec3(
        map(pos+eps.xyy).x - map(pos-eps.xyy).x,
        map(pos+eps.yxy).x - map(pos-eps.yxy).x,
        map(pos+eps.yyx).x - map(pos-eps.yyx).x );
    return normalize(nor);
}

Object rayMarching(vec3 ray, vec3 rayDir){

    float distance = 0.0;
    Object sceneObject;
    vec2 mapOut;

    sceneObject.hit=false;

    for(int i=0; i < MAX_STEPS; ++i) {

        mapOut=map(ray);
        distance += mapOut.x;
        ray += rayDir * mapOut.x;
        sceneObject.step += STEPS_DELTA;;
        if(mapOut.x < EPSILON) {             
            sceneObject.hit=true;
            distance=0.0;
            break;
        }
        if(distance > MAX_DISTANCE) { 
            distance=MAX_DISTANCE; 
            break; 
        }
    }

    sceneObject.distance=distance/MAX_DISTANCE;

    if (sceneObject.hit){
        sceneObject.normal=pointNormal(ray);
        sceneObject.pos=ray;
        sceneObject.mat.ambient=vec3(0.0);
        sceneObject.mat.diffuse=vec3(0.8);
        sceneObject.mat.specular=vec3(1.0);     
    }
    return sceneObject;
}

void main() {

    vec2 pos = (gl_FragCoord.xy*2.0 - resolution.xy) / resolution.y;
    vec2 uv = pos * vec2(resolution.x/resolution.y,1.0);

    //
    // Konfiguracja kamery
    //
    vec3 camPos = vec3(5.0, 2.0, 4.0);
    vec3 camTarget = vec3(0.0, 0.0, 0.0);

    //
    // Obliczenia kierunku kamery
    //
    vec3 camUp  = normalize(vec3(0.0, 1.0, 0.0));
    vec3 camDir = normalize(camTarget - camPos);
    vec3 camSide = cross(camDir, camUp);
    float focus = 1.8;

    vec3 rayDir = normalize(camSide * pos.x + camUp * pos.y + camDir * focus);
    vec3 ray = camPos;

    //
    // RayMarching
    //
    Object sceneObject = rayMarching(ray, rayDir);
    vec3 outColor= vec3(sceneObject.step);
    gl_FragColor=vec4(outColor.r,outColor.g,outColor.b,1.0);
}
Wynikiem pracy tego programu jest, sześcian gdzie im jaśniejszy kolor, tym więcej korków potrzeba było do znalezienia punktu przecięcia.

 

Kroki działania programu:

  1. Na podstawie pozycji kamery i pozycji „na co patrzy” wyliczamy kierunek kamery

       vec3 camUp  = normalize(vec3(0.0, 1.0, 0.0));
    vec3 camDir = normalize(camTarget - camPos);
    vec3 camSide = cross(camDir, camUp);
    float focus = 1.8;
    
    vec3 rayDir = normalize(camSide * pos.x + camUp * pos.y + camDir * focus);
    vec3 ray = camPos;

     

  2. Inicjujemy dane:

        float distance = 0.0;
    Object sceneObject;
    vec2 mapOut;
    sceneObject.hit=false;
     

  3. Przeprowadzamy właściwą operację RayMarchingu. Bieżącą pozycję punktu w przestrzeni 3D, mapujemy na odległość. Po tym mamy 3 warunki wyjścia z pętli wykonującej operację RayMarchingu:

    1. Przekroczyliśmy maksymalną ilość kroków pętli
    2. Dystans jest mniejszy od zakładanej dokładności – znaleźliśmy punkt przecięcia. W tym przypadku wykonujemy obliczenie wektora normalnego, nadania koloru, wyliczenia światła, czy inne operacje na znalezionym punkcie przecięcia.
    3. Przekroczyliśmy maksymalny wyliczany dystans punktu od obserwatora, nie wykonujemy dalszych obliczeń
        
    for(int i=0; i < MAX_STEPS; ++i) {
    
        mapOut=map(ray);
        distance += mapOut.x;
        ray += rayDir * mapOut.x;
        sceneObject.step += STEPS_DELTA;;
        if(mapOut.x < EPSILON) {             
            sceneObject.hit=true;
            distance=0.0;
            break;
        }
        if(distance > MAX_DISTANCE) { 
            distance=MAX_DISTANCE; 
            break; 
        }
    }

    Graficzna wersja algorytmu:

 

Część II – obiekty podstawowe, modyfikatory

Część III – technikalia, kod shadera