Introduction to Raymarching
Based on a tutorial
This experiment demonstrates the basics of raymarching using custom WebGL shaders.
Step by step
Step 1: Basic Ray Marching Setup
Let’s start with the basic setup for ray marching. We create a ray origin (camera position) and direction, then march along this ray to find intersections with objects in our scene.
vec3 generate(vec2 uv, vec2 m) {
// Initialization
vec3 ro = vec3(0,0,-3); // ray origin, towards us, the camera
vec3 rd = normalize(vec3(uv,1)); // ray direction, pointing in z direction
float t = 0.; // total distance travelled
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t; // position along the ray
float d = map(p); // current actual distance to the scene
t+= d; // Marching the ray, the marching sphere will touch closest object in the next iteration
}
// Coloring
finalColor = vec3(t * .2); // color based on t distance
// output color is in [0,1], but t is in [-3,??] so we can multiply it with arbitrary value to see some image
return finalColor;
}
Step 2: Visualizing Iterations
Instead of coloring based on the total distance traveled, we can visualize how many iterations it takes to reach an object. This shows more detail in our scene.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map(p);
t+= d;
finalColor = vec3(i) / 80.; // we divide it to map it to [0,1]
if(d < .001 || t > 100.) break; // early stop if close to the object, or too early stop if too far
}
return finalColor;
}
Step 3: Adjusting Field of View
We can modify the field of view by scaling the UV coordinates before normalizing the ray direction. This changes how much of the scene is visible.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv * 0.5 ,1)); // Multiplying uv by 0.5 creates a wider FOV
float t = 0.;
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Coloring
finalColor = vec3(t * .2);
return finalColor;
}
Step 4: Going Back to Distance-Based Coloring
For a smoother visualization, we return to coloring based on distance. This produces a depth-like effect in our scene.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Coloring
finalColor = vec3(t * .2);
return finalColor;
}
Step 5: Adding Mouse Control
Now let’s add mouse control to our scene. We’ll use the mouse position to rotate both the ray origin and direction, allowing us to look around in the scene.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
// Apply rotations based on mouse position
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Coloring
finalColor = vec3(t * .2);
return finalColor;
}
Step 6: Using Space Repetition
Let’s create a repeating pattern in our scene using space repetition. We’ll use the map2 function which creates a grid of objects that move along the z-axis over time.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching using map2 with space repetition
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map2(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Adjusted multiplier for better visualization
finalColor = vec3(t * .05);
return finalColor;
}
Step 7: Using Color Palette
Instead of grayscale colors, we can use a color palette function to create more vibrant and dynamic visualizations.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map2(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Use the palette function for coloring
finalColor = palette(t * .04);
return finalColor;
}
Step 8: Using Octahedrons
Let’s change our scene to use octahedrons instead of cubes, with a modified spacing pattern along the z-axis.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching using map3 with octahedrons
for (int i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Use palette for coloring
finalColor = palette(t * .04);
return finalColor;
}
Step 9: Using Both Distance and Iteration Count
We can create more detailed and interesting visuals by combining both the distance traveled and the iteration count in our coloring function.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching
int i = 0;
for (i = 0; i <80; i++){
vec3 p = ro +rd * t;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
// Coloring using both distance and iteration count
finalColor = palette(t* .04 + float(i) * .005);
return finalColor;
}
Step 10: Adding Sine Wave Distortion
Let’s add a sine wave distortion to our ray to create a wavy, dream-like effect in our visualization.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
ro.yz *= rot2D(-m.y);
rd.yz *= rot2D(-m.y);
ro.xz *= rot2D(-m.x);
rd.xz *= rot2D(-m.x);
vec3 finalColor = vec3(0);
// Ray marching
int i = 0;
for (i = 0; i <80; i++){
vec3 p = ro +rd * t;
// Add sine wave distortion to y-coordinate
p.y += sin(t)*.35;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
finalColor = palette(t* .04 + float(i) * .005);
return finalColor;
}
Step 11: Mouse-Controlled Wave Frequency
Now we’ll use the mouse position to control the frequency of our sine wave distortion, adding an interactive element to our visualization.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
vec3 finalColor = vec3(0);
// Ray marching
int i = 0;
for (i = 0; i <80; i++){
vec3 p = ro +rd * t;
// Use mouse y-position to control wave frequency
p.y += sin(t*(m.y+1.)*.5)*.35;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
finalColor = palette(t* .04 + float(i) * .005);
return finalColor;
}
Step 12: Adding Ray Rotation
Let’s add a rotation to our ray based on distance and mouse position, creating a spiral-like effect in our scene.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
vec3 finalColor = vec3(0);
// Ray marching
int i = 0;
for (i = 0; i <80; i++){
vec3 p = ro +rd * t;
// Add rotation to the ray based on distance
p.xy *= rot2D(t* .2 *m.x);
p.y += sin(t*(m.y+1.)*.5)*.35;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
finalColor = palette(t* .04 + float(i) * .005);
return finalColor;
}
Step 13: Auto-Animation When Mouse Isn’t Pressed
Finally, let’s add an automatic animation that activates when the mouse isn’t being pressed, creating a dynamic experience even without user interaction.
vec3 generate(vec2 uv, vec2 m) {
vec3 ro = vec3(0,0,-3);
vec3 rd = normalize(vec3(uv,1));
float t = 0.;
vec3 finalColor = vec3(0);
// Auto-animation when mouse isn't pressed (uMouse.z < 0)
if (uMouse.z < 0.) m = vec2(cos(uTime*.2), sin(uTime * .2));
// Ray marching
int i = 0;
for (i = 0; i <80; i++){
vec3 p = ro +rd * t;
p.xy *= rot2D(t* .2 *m.x);
p.y += sin(t*(m.y+1.)*.5)*.35;
float d = map3(p);
t+= d;
if(d < .001 || t > 100.) break;
}
finalColor = palette(t* .04 + float(i) * .005);
return finalColor;
}
Further Learning
This tutorial is based on the work of kishimisu, whose creative shader art and excellent explanations have made WebGL shaders more accessible. To learn more, check out the original An introduction to Shader Art Coding video tutorial on YouTube.