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