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.
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.
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.
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.
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
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.
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.
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.
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.
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.
- 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 - 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-hero2. 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 guidesWhat 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.

