Home

Introduction to Raymarching


Based on a tutorial

For the original video or blog post, please visit: An introduction to Shader Art Coding | YouTube. You can also find more cool stuff on the author's blog: kishimisu

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.