Fruxis by Inigo Quilez is a really, really cool scene. And as I am learning Raymarching (again), this time I will try to break this whole thing down, so to get a little bit advanced-than-before view of raymarching. So, let’s go make fruxis again!
There are quite a few objects inside this scene. And as the original author (iq) was using Spanish at that time, let’s learn a few Spanish words first:
And we are going to draw it one by one. K, let’s begin!
Well first, there’s gonna be an empty scene. You can create one at here. Then we need to implement all sorts of basic raymarching functions.
I don’t know if you could remember this, but here’s the gist of raymarching: you have a camera, and a direction, and a ray shoots out from the camera, towards the direction. And for every iteration, calculate the nearest object’s distance to the ray’s head, and the ray advances said distance. If this makes the ray’s head ends up in an object (negative distance), the raymarching is done. Otherwise if the ray is going too far or there has been too many iterations already, the raymarching is done.
That’s why we need to create the camera first:
float an = 2.0 * sin(0.7 + 0.5 * iTime);
vec3 ro = vec3(0.8 + 0.2 * sin(2.0 + an), 0.4, 1.1 + 0.25 * sin(an));
We are naming it ro
because it’s a raymarching tradition. ro
stands for Ray Origin and rd
stands for Ray Direction. Setting it as the output of the whole graph, and we will see that it sways gently:
void main() {
float an = 2.0 * sin(0.7 + 0.5 * time);
vec3 ro = vec3(0.8 + 0.2 * sin(2.0 + an), 0.4, 1.1 + 0.25 * sin(an));
color = vec4(ro, 1.0);
}
And now, onto the next step! Getting a direction:
vec3 center = vec3(-0.2, 0.0, 0.0);
// LookAt matrix
vec3 front = normalize(center - ro);
vec3 right = normalize(cross(front, vec3(0.0, 1.0, 0.0)));
vec3 up = normalize(cross(right, front));
float aspect = iResolution.x / iResolution.y;
vec2 uv = (fragCoord / iResolution.xy) * 2.0 - 1.0;
uv.x *= aspect; // multiples aspect so it doesn't lose
vec3 rd = normalize(uv.x * right + uv.y * up + 2.0 * front);
(iq himself really optimzed the variable names and stuffs. If you want to see how he works, check out here, around line 650.)
And after getting both ro
and rd
, it’s time to write the raymarching’s core function: raymarching! (Or intersect
as iq calls)
float intersect(vec3 ro, vec3 rd, out vec4 info) {
float depth = 0.0;
for (int i = 0; i < 90; i++) {
float dist = map(ro + rd * depth, info);
if (dist < 0.001) {
break;
}
depth += dist;
}
return depth;
}
The code above says a lot. First, there will be 90 marches; Second, the raymarching here does not concern about a way too long ray, so I retract my statement above. The info
vector itself was used to convey object info. What object does the ray bumped into? It will be used for coloring that particular object later. But there’s a new function in the intersect
function: the map
! It is the SDF function. And now, we are going to start out simple:
float map(vec3 p, out vec4 info) {
float closest = 1000.0;
vec4 result = vec4(-1.0);
vec3 objPos = vec3(0.0);
float dist = ball(p, objPos);
if (dist < closest) { closest = dist; result = vec4(1.0, objPos); }
info = result;
return closest;
}
Here, we keep track of the closest distance after all objects. The result
is a vector4 whose x
component is kinda like the material ID, and yzw
component is the object’s position, so to say.
float ball(vec3 p, out vec3 objPos) {
objPos = p;
return length(p) - 0.5;
}
And now the raymarching part is done! Heading back to the mainImage
, we are going to create a crude test to prove it works:
// ...
vec3 rd = normalize(uv.x * right + uv.y * up + 2.0 * front);
vec4 info;
float dist = intersect(ro, rd, info);
fragColor = vec4(info.yzw, 1.0);
Yep, just displaying the info! You should be able to see a ball that looks kinda like it’s breathing:
float ball(vec3 p, out vec3 objPos) {
objPos = p;
return length(p) - 0.5;
}
float map(vec3 p, out vec4 info) {
float closest = 1000.0;
vec4 result = vec4(-1.0);
vec3 objPos = vec3(0.0);
float dist = ball(p, objPos);
if (dist < closest) { closest = dist; result = vec4(1.0, objPos); }
info = result;
return closest;
}
float intersect(vec3 ro, vec3 rd, out vec4 info) {
float depth = 0.0;
for (int i = 0; i < 90; i++) {
float dist = map(ro + rd * depth, info);
if (dist < 0.001) {
break;
}
depth += dist;
}
return depth;
}
void main() {
float an = 2.0 * sin(0.7 + 0.5 * time);
vec3 ro = vec3(0.8 + 0.2 * sin(2.0 + an), 0.4, 1.1 + 0.25 * sin(an));
vec3 center = vec3(-0.2, 0.0, 0.0);
// LookAt matrix
vec3 front = normalize(center - ro);
vec3 right = normalize(cross(front, vec3(0.0, 1.0, 0.0)));
vec3 up = normalize(cross(right, front));
// This thing is a square, man! There's no need for aspect.
vec3 rd = normalize(uv.x * right + uv.y * up + 2.0 * front);
vec4 info;
float dist = intersect(ro, rd, info);
color = vec4(info.yzw, 1.0);
}
It’s not breathing though. The ball looks the same no matter how the camera rotates. But that also means we are ready to go for the next step!
The important stuff in Fruxis is the floor. Without floor, all the good imaginations will be gone:
And it is of utmost importance that we make the floor first.
float suelo(vec3 p, out vec3 objPos) {
objPos = p;
return p.y;
}
After adding it, the scene should look kinda like this:
Ugh. Ugly. Don’t sweat it though, because we are going to add
In fruxis, the camera points to the corner of the wall. Ever wondered what’s in the opposite direction of those fruits? Well, nothing!
And I guess that’s the importance of the walls, right? It blocks your view.
float pared(vec3 p, out vec3 objPos) {
objPos = 4.0 * p;
float d1 = 0.6 + pos.z;
float d2 = 0.6 + pos.x;
d1 = min(d1, d2);
// d1 = min(d1, sdBox(p - vec3(0.0,2.0,0.0), vec3(1.5, 0.05, 1.5)));
return d1;
}
And now, the scene code should probably look like this:
float map(vec3 p, out vec4 info) {
float closest = 1000.0;
vec4 result = vec4(-1.0);
vec3 objPos = vec3(0.0);
float dist = suelo(p, objPos);
if (dist < closest) { closest = dist; result = vec4(1.0, objPos); }
dist = pared(p, objPos);
if (dist < closest) { closest = dist; result = vec4(2.0, objPos); }
dist = ball(p, objPos);
if (dist < closest) { closest = dist; result = vec4(3.0, objPos); }
info = result;
return closest;
}
Well, it’s getting there! After adding some crude walls & floors, it’s time for the crucial part of the scene: lighting!
A scene is nothing but pure ugliness if the lighting is dumb (just like above). So now, let’s dive into magnificent iq’s code, and check out how his lighting works! But for every lighting, we will be needing their normals. That’s why we need this estimate normal function:
vec3 calcNormal(vec3 p) {
vec4 trash; // as map() takes 2 arguments
const float epsilon = 0.001;
return normalize(vec3(
map(p, trash) - map(vec3(p.x - epsilon, p.yz), trash),
map(p, trash) - map(vec3(p.x, p.y - epsilon, p.z), trash),
map(p, trash) - map(vec3(p.xy, p.z - epsilon), trash)
));
}
Then we can get the normal by doing:
vec3 n = calcNormal(ro + dist * rd);
After getting this, we are going to calculate the basic lighting (phong) first. And we are going to do this separately. First, we define a variable for final lighting calculation:
vec3 lighting = vec3(0.0);
The scene itself contains a large amount of lighting calculations. And here they are:
In all of those variables, bfl
, bce
, bak
, and fre
seems to be GI hacks simulating lights reflecting from all kinds of directions. amb
, dif
and spe
are standard phong lighting variables, occ
and sha
are real stuffs. Let’s begin from Phong, anyway:
Following iq, we are going to set our ambient lighting contributor to 1.0. Don’t sweat it, because the ambient color itself is pretty dark.
float ambient = 1.0;
lighting += ambient * vec3(0.18, 0.10, 0.12) * occulusion * attenuation;
There’s another two variables here: occlusion and attenuation. For now, let’s just assume it to be all vec3(1.0)
s. We will get back later.
Once there are diffuse lighting, the light position will be expected. And here it is:
const vec3 lightPos = vec3(3.62, 2.99, 0.71 );
vec3 lightDir = normalize(rlight);
It’s pretty clear that this is a directional light. And here’s the diffuse lighting:
float diffuse = max(dot(n, lightDir), 0.0);
lighting += diffuse * vec3(2.5, 1.8, 1.3);
And finally, the last member of the Phong family, Specular lighting:
vec3 headToLight =