Raymarched sphere

Raymarched sphere

A sphere signed-distance field, sphere-traced from a camera ray and Blinn-Phong shaded with an orbiting light.

Raymarched sphere shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade example — raymarched sphere (signed-distance field) ═══
//
// A sphere distance-field, sphere-traced from a camera ray, then Blinn-Phong shaded
// with an orbiting light. The "hello world" of raymarching. Showcases the new
// `normalize` builtin (ray + surface normal), `length` (the SDF), `dot`, a `Loop`
// march with early `Break`, and a mutable colour `var`. WGSL + GLSL ES 3.00.
import {
fn,
module,
u32,
f32,
vec2,
vec3,
vec4,
sin,
cos,
dot,
max,
pow,
normalize,
length,
Loop,
If,
Break,
Let,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms();
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const uv = vo.uv;
const ndc = vec2(uv.x.mul(2).sub(1), uv.y.mul(2).sub(1));
const ro = vec3(0, 0, 3); // camera
const rd = normalize(vec3(ndc.x, ndc.y, -1.5)); // ray direction
// sphere-trace a unit sphere at the origin: SDF = |p| − 1
const t = f32(0);
const hit = f32(0);
Loop(
u32(0),
(i) => i.lt(u32(72)),
() => {
const p = ro.add(rd.mul(t));
const d = Let(length(p).sub(1)); // materialise once: `t` is a mutated var so CSE can't hoist — without Let the SDF (a `length`) re-emits in both the hit test and `t += d`
If(d.lt(0.001), () => {
hit.assign(1);
Break();
});
t.assign(t.add(d));
If(t.gt(8), () => {
Break();
});
},
);
// background: a soft vertical gradient
const col = vec3(0.04, 0.05, 0.09).add(vec3(0.0, 0.04, 0.1).mul(uv.y));
If(hit.gt(0.5), () => {
const p = ro.add(rd.mul(t));
const n = Let(normalize(p)); // materialise once: the 3 lighting dots below each read `n`, and `t` (a var) blocks CSE — without Let the normal (sqrt + divides) re-emits 3x
const ld = normalize(vec3(cos(U.field.time), 0.75, sin(U.field.time)));
const diff = max(dot(n, ld), 0);
const h = normalize(ld.sub(rd)); // Blinn-Phong halfway (view dir = −rd)
const spec = pow(max(dot(n, h), 0), 48);
const rim = pow(f32(1).sub(max(dot(n, rd.neg()), 0)), 2);
col.assign(
vec3(0.22, 0.48, 0.95)
.mul(diff.mul(0.85).add(0.12)) // diffuse + ambient
.add(vec3(1.0).mul(spec.mul(0.6))) // specular highlight
.add(vec3(0.25, 0.35, 0.55).mul(rim.mul(0.5))), // fresnel rim
);
});
return vec4(col, 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const raymarchModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const raymarchSphere: ShaderExample = {
id: 'raymarch-sphere',
title: 'Raymarched sphere',
blurb:
'A sphere signed-distance field, sphere-traced from a camera ray and Blinn-Phong shaded with an orbiting light. The "hello world" of raymarching — built on normalize / length / dot + a Loop march.',
category: 'generic',
file: 'raymarch-sphere.ts',
module: raymarchModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0);
let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0);
return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));
}
@fragment
fn fs(vo: VsOut) -> @location(0) vec4<f32> {
var _av0: f32 = 0.0;
var _av1: f32 = 0.0;
let _cse0 = vec3<f32>(0.0, 0.0, 3.0);
let _cse3 = vec2<f32>(((vo.uv.x * 2.0) - 1.0), ((vo.uv.y * 2.0) - 1.0));
let _cse1 = normalize(vec3<f32>(_cse3.x, _cse3.y, -1.5));
for (var _v0: u32 = 0u; (_v0 < 72u); _v0 = (_v0 + 1u)) {
let _v1 = (length((_cse0 + (_cse1 * _av1))) - 1.0);
if ((_v1 < 0.001)) {
_av0 = 1.0;
break;
}
_av1 = (_av1 + _v1);
if ((_av1 > 8.0)) {
break;
}
}
var _av2: vec3<f32> = (vec3<f32>(0.04, 0.05, 0.09) + (vec3<f32>(0.0, 0.04, 0.1) * vo.uv.y));
if ((_av0 > 0.5)) {
let _v2 = normalize((_cse0 + (_cse1 * _av1)));
let _cse2 = normalize(vec3<f32>(cos(U.time), 0.75, sin(U.time)));
_av2 = (((vec3<f32>(0.22, 0.48, 0.95) * ((max(dot(_v2, _cse2), 0.0) * 0.85) + 0.12)) + (vec3<f32>(1.0) * (pow(max(dot(_v2, normalize((_cse2 - _cse1))), 0.0), 48.0) * 0.6))) + (vec3<f32>(0.25, 0.35, 0.55) * (pow((1.0 - max(dot(_v2, (-_cse1)), 0.0)), 2.0) * 0.5)));
}
return vec4<f32>(_av2, 1.0);
}
#version 300 es
precision highp float;
precision highp int;
out vec2 uv;
void main() {
uint vi = uint(gl_VertexID);
float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0);
float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0);
gl_Position = vec4(_cse0, _cse1, 0.0, 1.0);
uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));
}
#version 300 es
precision highp float;
precision highp int;
layout(std140) uniform Uniforms {
float time;
vec2 resolution;
} U;
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _av0 = 0.0;
float _av1 = 0.0;
vec3 _cse0 = vec3(0.0, 0.0, 3.0);
vec2 _cse3 = vec2(((uv.x * 2.0) - 1.0), ((uv.y * 2.0) - 1.0));
vec3 _cse1 = normalize(vec3(_cse3.x, _cse3.y, -1.5));
for (uint _v0 = 0u; (_v0 < 72u); _v0 = (_v0 + 1u)) {
float _v1 = (length((_cse0 + (_cse1 * _av1))) - 1.0);
if ((_v1 < 0.001)) {
_av0 = 1.0;
break;
}
_av1 = (_av1 + _v1);
if ((_av1 > 8.0)) {
break;
}
}
vec3 _av2 = (vec3(0.04, 0.05, 0.09) + (vec3(0.0, 0.04, 0.1) * uv.y));
if ((_av0 > 0.5)) {
vec3 _v2 = normalize((_cse0 + (_cse1 * _av1)));
vec3 _cse2 = normalize(vec3(cos(U.time), 0.75, sin(U.time)));
_av2 = (((vec3(0.22, 0.48, 0.95) * ((max(dot(_v2, _cse2), 0.0) * 0.85) + 0.12)) + (vec3(1.0) * (pow(max(dot(_v2, normalize((_cse2 - _cse1))), 0.0), 48.0) * 0.6))) + (vec3(0.25, 0.35, 0.55) * (pow((1.0 - max(dot(_v2, (-_cse1)), 0.0)), 2.0) * 0.5)));
}
_ret = vec4(_av2, 1.0);
}

Raymarched sphere, drawn at build time.

The WGSL and GLSL tabs are the compiler's own output at commit 26de7be8, read from the goldens its emit suite bakes (emit-goldens.test.ts).

This example is written against the fn() builder API, which the editor in the Playground does not take.

File on GitHub

Edit this page Report a problem