fragsplainer Gyroid Marcher
A Shader, Explained

Gyroid Marcher

An infinite black-and-white lattice, flown through in twenty lines of code.

A genuine three-dimensional scene — depth, surfaces, a camera gliding forward — conjured from about twenty lines. There are no models and no triangles. Instead a ray is fired through every pixel and marched forward until it bumps into a surface defined purely by arithmetic. The surface here is a gyroid: an endless, smooth lattice that fills all of space.


Marching a ray into the scene → technical explainer

Raymarching walks a point along the ray in steps. Each step asks the map function “how far to the nearest surface?” and advances by (a fraction of) that distance — big leaps through empty space, tiny creeps near a surface. When the steps shrink below a threshold, we’ve arrived.

for (float i=0.; i<90. && dd>.001 && d<2.; i++) {
    d += dd;
    p += rd*d;
    dd = map(p)*.02;
}

The speed 1.00 control sets how fast the camera flies along the lattice; lens 1.00 tightens or widens the lens.

The field: a gyroid, modulated → technical explainer

The whole world is one function. Its core is the gyroiddot(sin(p), cos(p.zxy)) — a single expression that carves a smooth, three-way-symmetric lattice out of empty space. This shader nests one gyroid inside another and multiplies in high-frequency sine waves, so the lattice ripples and shears as you move through it.

#define gyr(p) dot(sin(p.xyz), cos(p.zxy))

It isn’t a true distance field — the values are only roughly proportional to real distance — which is why the march takes cautious 0.02-scaled steps instead of full ones.

Lighting with no lights

Once a ray lands, the surface normal is found by sampling the field a few times around the hit point and taking the difference — the gradient. This shader skips real lighting entirely and just reads two components of that gradient straight into brightness, then fades anything the ray never reached.

vec3 n = norm(p);
float bw = n.x + n.y;
bw *= SS(.9, .15, 1./d);   // fog out the far distance
shaders /image.glsl
↗ Shadertoy
1 #define SS(a,b,c) smoothstep(a-b,a+b,c)
2 #define gyr(p) dot(sin(p.xyz),cos(p.zxy)) // a gyroid: a smooth, infinite 3D lattice
3 #define T iTime
4
5 // The world is one big distance-ish function: nested gyroids modulated by sine
6 // waves and time. It isn't a true distance field — it just has to be small near
7 // surfaces and large away from them, which is enough to march through.
8 float map(in vec3 p) {
9
10 return (1. + .2*sin(p.y*600.)) *
11 gyr(( p*(10.) + .8*gyr(( p*8. )) )) *
12 (1.+sin(T+length(p.xy)*10.)) +
13 .3 * sin(T*.15 + p.z * 5. + p.y) *
14 (2.+gyr(( p*(sin(T*.2+p.z*3.)*350.+250.) )));
15
16 }
17
18 // Surface normal by finite differences: sample the field a few times around p
19 // and take the gradient. (Left un-normalized on purpose — it doubles as shading.)
20 vec3 norm(in vec3 p) {
21 float m = map(p);
22 vec2 d = vec2(.06+.06*sin(p.z),0.);
23 return map(p)-vec3(
24 map(p-d.xyy),map(p-d.yxy),map(p-d.yyx)
25 );
26 }
27
28 void mainImage( out vec4 color, in vec2 coord ) {
29 vec2 uv = coord/R.xy;
30 vec2 uvc = (coord-R.xy/2.)/R.y;
31 float d = 0.;
32 float dd = 1.;
33 vec3 p = vec3(0.,0.,T*0.25*1.00); // camera flies along +z
34 vec3 rd = normalize(vec3(uvc.xy,1.00)); // ray for this pixel
35
36
37 for (float i=0.;i<90. && dd>.001 && d < 2.;i++) {
38 d += dd; // advance along the ray
39 p += rd*d;
40 dd = map(p)*.02; // next step = (a fraction of) the field value
41 }
42
43
44
45 vec3 n = norm(p);
46 float bw = n.x+n.y; // fake lighting straight from the gradient
47 bw *= SS(.9,.15,1./d); // fade out whatever the ray never reached
48 color = vec4(vec3(bw),1.0);
49
50 }
51