A real-time raymarched rotating tunnel rendered in WebGL, packed into 980 bytes of JavaScript. The entire scene — geometry, lighting, fog, and animation — lives in a single GLSL fragment shader.
| Author | Alexander Timoshenko |
|---|---|
| Competition | webgl |
| Year | 2019 (10th anniversary JS1K) |
| Bytes | 980 / 1024 |
| Technique | Raymarching / SDF |
The JavaScript is almost entirely boilerplate: compile two shaders, link a program, upload a full-screen triangle, then drive a uniform float T (time) via setInterval. All the visual work happens on the GPU.
The demo uses the classic JS1K trick of aliasing WebGL methods by their first and seventh characters — for(B in g) g[B[0]+B[6]] = g[B] — so calls like createShader become cS, saving dozens of bytes.
Int8Array — two triangles worth of positions packed as (0,0), (0,6), (6,0) in BYTE formatfor(B in g) g[B[0]+B[6]] = g[B] — e.g. createShader→cS, shaderSource→sS, compileShader→cS, useProgram→ugbo(p) — a box SDF that defines the tunnel cross-section; the tunnel is an infinite repeating boxs(p) — the full scene SDF: combines fract()-based repetition, smoothstep for the mosaic tile pattern on the walls, and min(t, bo(p)) to union the tile and tunnel geometrye.yxx, e.xxy, e.xyx, e.yyyvec2(sin(3+T), cos(3+T)) — the same vector reused as #define rt for both the box orientation and the light directionp.xy is rotated by .05*T each step, so the tunnel appears to corkscrew around the viewerVertex shader — trivial pass-through. Takes a vec2 p attribute, outputs gl_Position = vec4(p-1, 0, 1) and passes u = p-1 (the NDC UV) to the fragment shader.
Fragment shader — does all the work:
d = normalize(vec3(u, 1)) from the UV.p.xy by the time-varying rotation.h < e), compute the surface normal and a diffuse term l.vec3(.5, l, l) minus fog (t*.05); box-hit cells are tinted cyan-blue.You can run it in the Expanded tab.
for(_='for~uni~mZ3.+T)YfloatXp.WWz)Rs(QabQOcoQNbo(M0,L, K0KJ; H);GG in(.5(A){vec2) return )+e..05xy3 + max()K = sScS(3563void ma3(varyg uHlength(g)O1G}`GceGaS(PKAG * normalize(*Qpe.X ~(B gg[ B[0]+B[6]]g[B];with(g PcP(G3`attribute p;gl_Position=4(u=p-1.,L 2`precision highp XHZ TH\\n#defe rt (sY,NY)\\nMppOp-*rt,1)WxKWyR-.1;}Qpg =fract(WKT+R 0.47- ;gsmoothstep(0.3K1.K25.ggGt.25(5.-2(NWx)sT+R)Wx1.-Wy))GmtKMp)G}duK1)oLL-1c,p,n;t,h;e.002;~ (t i0Hi < 100Hi++ podt;WN*T)Ws*t)(WyK-WxGhQpt += h;if (h<e{ e(-e,en=e.yxxyxxxxxxyyyyyy)Gl dot(1,-rt- p n 0.c =(-Od.x*.2)+,l,l))-(t*Gif (Mp== hc =l-L,1t=0.;} }gl_FragColor4(cKlo(PGug(PGbf34962KcB()GeV(0GvA(J2K512JJJ0GbD,Int8Array.of=LLL6,6,035044GsetInterval(`g.Z1f(g.gf(PK"T"A++*Gg.dr(6KJ3G`K16G}';G=/[^ -FIPSTV[-}]/.exec(_);)with(_.split(G))_=join(shift());eval(_)
var P = g.createProgram()
// Vertex shader: passes screen-space UV to the fragment shader
var vert = g.createShader(g.VERTEX_SHADER)
g.shaderSource(vert, `attribute vec2 p;
varying vec2 u;
void main() {
gl_Position = vec4(u = p - 1., 0, 1);
}`)
g.compileShader(vert)
g.attachShader(P, vert)
// Fragment shader: raymarches the rotating tunnel scene
var frag = g.createShader(g.FRAGMENT_SHADER)
g.shaderSource(frag, `precision highp float;
varying vec2 u;
uniform float T;
#define rt vec2(sin(3. + T), cos(3. + T))
float bo(vec3 p) {
p = abs(p - .5 * vec3(rt, 1));
return max(max(p.x, p.y), p.z) - .1;
}
float s(vec3 p) {
vec3 g = fract(vec3(p.xy, T + p.z) * 0.47) - .5;
g = smoothstep(0.3, 1., 25. * g * g);
float t = .25 * (5. - max(
2.5 * (cos(p.x) * sin(T + p.z)) + length(g) + abs(p.x),
length(g) + abs(1. - p.y)
));
return min(t, bo(p));
}
void main() {
vec3 d = normalize(vec3(u, 1)), o = vec3(0, 0, -1);
vec3 c, p, n;
float t, h;
float e = .002;
for (int i = 0; i < 100; i++) {
p = o + d * t;
p.xy = cos(.05 * T) * p.xy + sin(.05 * t) * vec2(p.y, -p.x);
h = s(p);
t += h;
if (h < e) {
vec2 e = vec2(-e, e);
n = normalize(
e.yxx * s(p + e.yxx) +
e.xxy * s(p + e.xxy) +
e.xyx * s(p + e.xyx) +
e.yyy * s(p + e.yyy)
);
float l = max(dot(normalize(vec3(1, -rt) - p), n), 0.);
c = (-abs(d.x * .2) + vec3(.5, l, l)) - (t * .05);
if (bo(p) == h) c = l - vec3(0, .5, 1), t = 0.;
}
}
gl_FragColor = vec4(c, 1);
}`)
g.compileShader(frag)
g.attachShader(P, frag)
g.linkProgram(P)
g.useProgram(P)
// Full-screen triangle: 3 BYTE vertices covering the entire viewport
var buf = g.createBuffer()
g.bindBuffer(g.ARRAY_BUFFER, buf)
g.bufferData(g.ARRAY_BUFFER, Int8Array.of(0, 0, 0, 6, 6, 0), g.STATIC_DRAW)
var loc = g.getAttribLocation(P, 'p')
g.enableVertexAttribArray(loc)
g.vertexAttribPointer(loc, 2, g.BYTE, false, 0, 0)
// Animate: increment T each frame and redraw
var T = 0
setInterval(function tick() {
g.uniform1f(g.getUniformLocation(P, 'T'), T++ * .05)
g.drawArrays(g.TRIANGLES, 0, 3)
}, 16)
</script>.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box }
body { background: #000 }
canvas { display: none; position: absolute; top: 0; left: 0 }
</style>
</head>
<body>
<canvas id="c" width="512" height="512"></canvas>
<script>
var a = document.getElementById('c')
var b = document.body
var g = a.getContext('webgl') || a.getContext('experimental-webgl')
</script>
<!-- tab source injected here as a <script> tag after 3 seconds -->
</body>
</html>