Skip to content
Founding Lifetime: every Pro skillfirst 100 members · $169 onceUpgrade
Lookbk
Sign In
Effect guide

Raymarching: how a ray marching shader works, with code

A whole 3D scene drawn by one fragment shader, with no meshes and no triangles: just a function that says how far away the nearest surface is. How raymarching and signed distance functions work, a WebGL starter you can paste and run, and the skills that raymarch finished liquid metal and desert dune heroes.

Free skills to start. Pro is $79 a year for every skill, or $9.99 a month for 10. See pricing

What is a raymarching?

Raymarching is a way to draw 3D inside a pixel shader. For every pixel, a ray leaves the camera and walks forward in steps until it reaches a surface. There is no model to load: the scene is described by math, so smooth blends, endless repetition and shapes that morph every frame cost little more than a still sphere.

The scene is usually written as a signed distance function, or SDF: give it a point and it returns how far that point is from the nearest surface, negative inside. A sphere is length(p) − r, and a torus, a box or a capsule each take a line or two. John C. Hart described sphere tracing, the step rule most ray marching shaders use, in 1996; Inigo Quilez’s articles and Shadertoy, the site he co-founded, spread it through shader art.

On a website it gives heroes that look rendered rather than drawn: liquid metal, glass, clouds, terrain, soft shadows and fog, from a few kilobytes of code. The price is graphics work on every pixel of every frame, so it needs care on phones.

How the effect works

Sphere tracing is what makes the march fast. At the ray’s current point, ask the SDF for the distance to the nearest surface. Nothing is closer than that in any direction, so the ray can safely jump that far. In open space the steps are long; near a surface they shrink until the distance drops below a small epsilon, and that is a hit. Rays that pass everything run out of steps or distance and become sky.

Shapes combine with plain math: min() of two distances is their union, max() their intersection, and a smooth minimum melts them together like metaballs. Lighting needs the surface normal, which is the gradient of the SDF: sample it a hair to each side along x, y and z. A second march from the hit point toward the light gives a shadow, and how closely that ray grazes other shapes softens its edge.

Terrain is often marched against a height field instead of an SDF. At each step, compare the ray’s height with the ground below it and step forward by a fraction of the gap. It is the same idea with a rougher distance estimate.

  1. 01

    Cast a ray per pixel

    Turn each pixel’s position into a direction from the camera. A triangle that covers the screen and a fragment shader run this for every pixel at once.

  2. 02

    Write the scene as an SDF

    One function returns the distance to the nearest surface. Combine shapes with min() for a union and a smooth minimum for a melted blend.

  3. 03

    March until you hit

    Step forward by the distance the SDF returns. Stop below about 0.001 for a hit, or at a step or distance limit for a miss.

  4. 04

    Light the hit

    Take the normal from the SDF’s gradient, add diffuse and specular light, march toward the light for a soft shadow, and fade with distance into fog.

HTML

A raymarching shader in plain WebGL

A sphere and a tilting torus melted together with a smooth minimum over a floor: sphere tracing, normals from the gradient, a soft shadow and fog, all in one fragment shader. The pixel ratio is capped at 1.5 because every pixel runs the march. Without WebGL it shows a message; reduced motion gets one frame.

<canvas id="rm" style="position:fixed;inset:0;width:100%;height:100%;background:#0d0f14"></canvas>
<script id="frag" type="x-shader/x-fragment">
// The whole scene lives here: one signed distance function, marched once per pixel
precision highp float;
uniform vec2 uRes; uniform float uTime;
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdTorus(vec3 p, vec2 t) { return length(vec2(length(p.xz) - t.x, p.y)) - t.y; }
float smin(float a, float b, float k) { // smooth union: the two shapes melt together
  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}
float map(vec3 p) {
  float s = sdSphere(p - vec3(0.0, 0.35 * sin(uTime * 0.9), 0.0), 0.55);
  float c = cos(uTime * 0.5), n = sin(uTime * 0.5);
  vec3 q = vec3(p.x, c * p.y - n * p.z, n * p.y + c * p.z); // tilt the ring
  float t = sdTorus(q, vec2(0.9, 0.18));
  return min(smin(s, t, 0.3), p.y + 1.1); // and a floor below
}
vec3 normal(vec3 p) {
  vec2 e = vec2(0.001, 0.0);
  return normalize(vec3(map(p + e.xyy) - map(p - e.xyy),
                        map(p + e.yxy) - map(p - e.yxy),
                        map(p + e.yyx) - map(p - e.yyx)));
}
float shadow(vec3 ro, vec3 rd) { // march toward the light; near misses give a soft edge
  float res = 1.0, t = 0.02;
  for (int i = 0; i < 32; i++) {
    float h = map(ro + rd * t);
    res = min(res, 8.0 * h / t);
    t += clamp(h, 0.02, 0.2);
    if (res < 0.001 || t > 6.0) break;
  }
  return clamp(res, 0.0, 1.0);
}
void main() {
  vec2 uv = (gl_FragCoord.xy * 2.0 - uRes) / uRes.y;
  vec3 ro = vec3(0.0, 0.4, 3.2);               // camera
  vec3 rd = normalize(vec3(uv, -1.6));         // one ray per pixel
  float t = 0.0, d = 1.0;
  for (int i = 0; i < 96; i++) {               // sphere tracing
    d = map(ro + rd * t);
    if (d < 0.001 || t > 12.0) break;
    t += d;                                    // the distance is a safe step
  }
  vec3 sky = mix(vec3(0.05, 0.06, 0.09), vec3(0.16, 0.18, 0.26), uv.y * 0.5 + 0.5);
  vec3 col = sky;
  if (d < 0.001) {
    vec3 p = ro + rd * t, n = normal(p);
    vec3 l = normalize(vec3(0.6, 0.8, 0.4));
    float diff = max(dot(n, l), 0.0) * shadow(p + n * 0.01, l);
    float spec = pow(max(dot(reflect(-l, n), -rd), 0.0), 40.0);
    float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
    vec3 base = p.y < -1.09 ? vec3(0.22, 0.23, 0.28) : vec3(1.0, 0.45, 0.25);
    col = base * (0.12 + diff) + spec * 0.8 + rim * vec3(0.3, 0.5, 1.0) * 0.6;
    col = mix(col, sky, 1.0 - exp(-0.02 * t * t)); // fog
  }
  gl_FragColor = vec4(pow(col, vec3(0.4545)), 1.0);
}
</script>
<script>
const canvas = document.getElementById('rm')
const gl = canvas.getContext('webgl')
const still = matchMedia('(prefers-reduced-motion: reduce)').matches

function size() {
  const dpr = Math.min(devicePixelRatio, 1.5) // every pixel runs the march: keep the count down
  canvas.width = innerWidth * dpr; canvas.height = innerHeight * dpr
}
size()

if (!gl) {
  const g = canvas.getContext('2d')
  g.fillStyle = '#0d0f14'; g.fillRect(0, 0, canvas.width, canvas.height)
  g.fillStyle = '#c9cdd8'; g.font = (16 * Math.min(devicePixelRatio, 1.5)) + 'px system-ui'
  g.textAlign = 'center'
  g.fillText('This browser has no WebGL, so the raymarched scene cannot run.', canvas.width / 2, canvas.height / 2)
} else {
  const shader = (type, src) => {
    const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s)
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s))
    return s
  }
  const prog = gl.createProgram()
  gl.attachShader(prog, shader(gl.VERTEX_SHADER, 'attribute vec2 a; void main() { gl_Position = vec4(a, 0.0, 1.0); }'))
  gl.attachShader(prog, shader(gl.FRAGMENT_SHADER, document.getElementById('frag').textContent))
  gl.bindAttribLocation(prog, 0, 'a')
  gl.linkProgram(prog); gl.useProgram(prog)
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()) // one triangle that covers the screen
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW)
  gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
  const uRes = gl.getUniformLocation(prog, 'uRes'), uTime = gl.getUniformLocation(prog, 'uTime')
  const draw = (seconds) => {
    gl.viewport(0, 0, canvas.width, canvas.height)
    gl.uniform2f(uRes, canvas.width, canvas.height)
    gl.uniform1f(uTime, seconds)
    gl.drawArrays(gl.TRIANGLES, 0, 3)
  }
  addEventListener('resize', () => { size(); if (still) draw(2) })
  if (still) draw(2) // one frame for reduced motion
  else requestAnimationFrame(function frame(now) { draw(now / 1000); requestAnimationFrame(frame) })
}
</script>

A starting point written for this page, free to use in your own projects. The skills below build the finished version with checks, fallbacks and a recorded video.

What to get right on a real page

  1. 01

    Cap the resolution

    Every pixel runs the whole march, so the cost grows with the pixel count. Cap the device pixel ratio, render at a fraction of the screen and scale up, or lower the step limit on slow devices.

  2. 02

    Bound the march

    Give the loop a fixed step limit and a far distance, and march only where the shape can be: a box around the object lets every other pixel skip straight to the background.

  3. 03

    Step short when you bend shapes

    Twisting, rippling or stretching an SDF makes it overestimate the distance, so rays can step through the surface and leave holes. Multiply each step by 0.5 to 0.9 when you distort a shape.

  4. 04

    Plan for no WebGL

    Some old devices, locked-down machines and browsers have no WebGL, or lose the context. Show a still picture of the scene instead of an empty box.

  5. 05

    Hold a still frame for reduced motion

    Respect prefers-reduced-motion: show one finished frame instead of the loop. A visitor who turned motion off still gets the look.

Skills that build a raymarching

Both are raymarched in raw WebGL1 with no libraries, and each builds a finished hero with your colors, fonts, logo and words, and a seamless 1080p loop for launch posts. The first marches a liquid metal blob; the second marches a desert height field.

  1. Liquid Chrome Hero: an example of what it makes
    web
    Liquid Chrome HeroClaude CodeCodexCursor+3 more agents

    Motion Heroes · Pro

    Liquid Chrome Hero

    Animated WebGL hero · page + embed + 1080p video

    A raymarched blob of liquid metal beside your headline: droplets on looping paths melt into one slowly morphing shape with a ripple running over its surface. It reflects a turning photo studio with strip lights and a rim light in your brand color, its edges split into a thin chromatic fringe, and faint rings pulse outward. Chrome, gold, rose gold or any tint, with 14 documented settings and a loop of 8 to 20 seconds.

    Worth knowing: The blob is abstract, not a 3D model of your product. Devices without WebGL get an SVG still.

    Needs: Node.js 18+, Chrome (or Chromium/Edge), and ffmpeg for the video

    See what Liquid Chrome Hero makes
  2. Sand Dune Hero: an example of what it makes
    web
    Sand Dune HeroClaude CodeCodexCursor+3 more agents

    Motion Heroes · Pro

    Sand Dune Hero

    Animated WebGL hero · page + embed + 1080p video

    A sea of dunes at golden hour, ray-marched per pixel through a height field with soft sun shadows, so every crest throws a long shadow over the trough behind it. Wind ripples travel across the slopes, sand streams off the crests in thin plumes, grains glint in the sun and the camera drifts on a closed loop. You set the sand, shade, sky and sun colors among 22 documented settings.

    Worth knowing: The ray march is heavy: lower its quality setting on low-power devices.

    Needs: Node.js 18+, Chrome (or Chromium/Edge), and ffmpeg for the video

    See what Sand Dune Hero makes

Install

Build a raymarching with your agent

Each skill is a SKILL.md folder, so it installs the same way in Claude Code, Codex, Cursor and other agents. The example below uses Liquid Chrome Hero; each skill page has its own prompt.

1. Paste the install prompt

Your agent runs the installer. The first time, it shows a sign-in link so you can approve it in the browser.

Install the "Liquid Chrome Hero" skill from Lookbk (https://lookbk.dev/skills/liquid-chrome-hero).
1. Run: curl -fsSL https://lookbk.dev/cli.js | node - add liquid-chrome-hero
2. If it prints a sign-in link, show it to me. When I say I approved it, run the same command again.
3. When the install finishes, read the SKILL.md it installed and tell me what you need from me to make the first result.

Or run it in a terminal

curl -fsSL https://lookbk.dev/cli.js | node - add liquid-chrome-hero

2. Start a new session

Agents load skills when a session starts. The installer writes the folder where your agent looks:

Claude Code
~/.claude/skills/liquid-chrome-hero
Codex
~/.agents/skills/liquid-chrome-hero
Cursor
~/.agents/skills/liquid-chrome-hero

3. Ask for the result

Describe it in plain words, for example:

Build a Liquid Chrome hero for Ferrous Audio's FA-1 headphone: graphite page, chrome blob with an orange rim light and faint sound rings, headline 'Sound, cast in one piece.', the driver size and weight in the corner, and record a 10-second loop for the launch post.

Another agent? Install a skill in any agent

FAQ

Questions

Still stuck? The guides cover installing, agents and licensing in more detail.

Read the guides
What is the difference between raymarching and ray tracing?

Ray tracing finds exactly where a ray meets a surface, usually by testing triangles or solving an equation. Raymarching walks along the ray in steps and checks the scene at each one, which works for any shape you can write as a distance function, including blends and fractals that have no simple intersection formula.

What is a signed distance function?

A function that takes a point and returns its distance to the nearest surface: positive outside, zero on the surface, negative inside. Since nothing is closer than that distance, a ray can safely step that far, which is what makes sphere tracing fast.

Can I raymarch with three.js?

Yes. Draw a full-screen plane with a ShaderMaterial and put the march in its fragment shader. The library adds little here, since the whole scene lives in the shader; the starter above does the same in plain WebGL.

Is raymarching too slow for a website?

It can be. The work is pixels times steps, every frame. One shape at a capped resolution runs smoothly on most laptops and phones; terrain, reflections and soft shadows cost more. Lower the resolution, bound the march and stop drawing when the hero is off screen.

Can I get a raymarched hero as a video?

Yes. Liquid Chrome Hero and Sand Dune Hero both record a frame-perfect 1080p MP4 that loops seamlessly, and a poster, with portrait sizes too.