Examples
cartographic hillshade.ts
Hillshade
Shaded relief — procedural terrain lit by a movable sun, tinted hypsometrically. A reusable terrain() DSL function is called 3× for the height + finite-difference normal.
Live
0.0s
One source, compiled every way
The DSL source below, compiled to WebGPU (WGSL), WebGL2 (GLSL ES 3.00 — two stages), and a reflection JSON the host binds from. Nothing but the DSL tab is hand-written.
// ═══ @xgis/shader-dsl example — hillshade (shaded relief) ═══//// A cartographic shader: procedural terrain + Lambert hillshade, the shaded-relief look// of a topographic map. A reusable `terrain()` DSL function (called 3× — once for height,// twice for a finite-difference normal) feeds a sun-lit hypsometric tint. The sun azimuth// and vertical exaggeration are live uniforms; the terrain drifts over time.
import { fn, module, vec2, vec3, vec4, sin, cos, radians, clamp, dot, length, mix, smoothstep, f32, f32T, vec2fT,} from '../src/index.ts'import { VsOut, vs, fullscreenUniforms } from './_fullscreen.ts'import type { Node } from '../src/index.ts'import type { ShaderExample } from './_shared.ts'const U = fullscreenUniforms({ sun_az: f32T, exaggeration: f32T })
// normalize() isn't a DSL builtin — it's just v · (1/|v|). The author spells it inline.const normalize3 = (v: Node<'vec3<f32>'>): Node<'vec3<f32>'> => v.mul(f32(1).div(length(v)))
// Reusable terrain height field, ~[0,1]. Emitted once, called 3× — a real DSL function.const terrain = fn('terrain', { p: vec2fT, t: f32T }, ({ p, t }) => { const h = sin(p.x.mul(3).add(t)) .mul(cos(p.y.mul(3))) .add( sin(p.x.mul(6.1).sub(t.mul(0.7))) .mul(cos(p.y.mul(5.3))) .mul(0.5), ) .add( sin(p.x.mul(12.7)) .mul(cos(p.y.mul(11.1))) .mul(0.25), ) return h.mul(0.28).add(0.5)})
const fs = fn( 'fs', { vo: VsOut }, ({ vo }) => { const uv = vo.uv const t = U.field.time const az = radians(U.field.sun_az) const ex = U.field.exaggeration const p = uv.mul(6) const eps = f32(0.015)
// Height + two neighbours → a finite-difference surface normal. const h = terrain({ p, t }) const hx = terrain({ p: vec2(p.x.add(eps), p.y), t }) const hy = terrain({ p: vec2(p.x, p.y.add(eps)), t }) const n = normalize3(vec3(h.sub(hx).mul(ex), h.sub(hy).mul(ex), eps))
// Sun from the azimuth (fixed elevation) → Lambert term. const sun = normalize3(vec3(cos(az).mul(0.6), sin(az).mul(0.6), 0.55)) const shade = clamp(dot(n, sun), 0, 1)
// Hypsometric tint: lowland green → upland tan → snow. const low = vec3(0.16, 0.32, 0.2) const mid = vec3(0.55, 0.49, 0.3) const high = vec3(0.93, 0.93, 0.96) const base = mix(mix(low, mid, smoothstep(0.3, 0.55, h)), high, smoothstep(0.62, 0.85, h)) const lit = base.mul(shade.mul(0.8).add(0.3)) return vec4(lit, 1) }, { stage: 'fragment', retAttr: '@location(0)' },)
// `terrain` is called via its handle in `fs`, so module() collects it transitively — funcs lists only the entry points.const hillshadeModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs],})
export const hillshade: ShaderExample = { id: 'hillshade', title: 'Hillshade', blurb: 'Shaded relief — procedural terrain lit by a movable sun, tinted hypsometrically. A reusable terrain() DSL function is called 3× for the height + finite-difference normal.', category: 'cartographic', file: 'hillshade.ts', module: hillshadeModule, renderable: true, controls: { time: { kind: 'time' }, resolution: { kind: 'resolution' }, sun_az: { kind: 'slider', label: 'Sun azimuth (°)', min: 0, max: 360, step: 1, value: 135 }, exaggeration: { kind: 'slider', label: 'Exaggeration', min: 0.5, max: 6, step: 0.1, value: 2.5, }, },}struct Uniforms { time: f32, resolution: vec2<f32>, sun_az: f32, exaggeration: f32,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn terrain(p: vec2<f32>, t: f32) -> f32 { return (((((sin(((p.x * 3.0) + t)) * cos((p.y * 3.0))) + ((sin(((p.x * 6.1) - (t * 0.7))) * cos((p.y * 5.3))) * 0.5)) + ((sin((p.x * 12.7)) * cos((p.y * 11.1))) * 0.25)) * 0.28) + 0.5);}
@vertexfn 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)));}
@fragmentfn fs(vo: VsOut) -> @location(0) vec4<f32> { let _cse6 = (vo.uv * 6.0); let _cse2 = terrain(_cse6, U.time); let _cse3 = _cse6.x; let _cse4 = _cse6.y; let _cse5 = radians(U.sun_az); let _cse0 = vec3<f32>(((_cse2 - terrain(vec2<f32>((_cse3 + 0.015), _cse4), U.time)) * U.exaggeration), ((_cse2 - terrain(vec2<f32>(_cse3, (_cse4 + 0.015)), U.time)) * U.exaggeration), 0.015); let _cse1 = vec3<f32>((cos(_cse5) * 0.6), (sin(_cse5) * 0.6), 0.55); return vec4<f32>((mix(mix(vec3<f32>(0.16, 0.32, 0.2), vec3<f32>(0.55, 0.49, 0.3), smoothstep(0.3, 0.55, _cse2)), vec3<f32>(0.93, 0.93, 0.96), smoothstep(0.62, 0.85, _cse2)) * ((clamp(dot((_cse0 * (1.0 / length(_cse0))), (_cse1 * (1.0 / length(_cse1)))), 0.0, 1.0) * 0.8) + 0.3)), 1.0);}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};out vec2 uv;
VsOut vs_impl(uint vi) { float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4(_cse0, _cse1, 0.0, 1.0), vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
void main() { VsOut _out = vs_impl(uint(gl_VertexID)); gl_Position = _out.pos; uv = _out.uv;}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};layout(std140) uniform Uniforms { float time; vec2 resolution; float sun_az; float exaggeration;} U;float terrain(vec2 p, float t);float terrain(vec2 p, float t) { return (((((sin(((p.x * 3.0) + t)) * cos((p.y * 3.0))) + ((sin(((p.x * 6.1) - (t * 0.7))) * cos((p.y * 5.3))) * 0.5)) + ((sin((p.x * 12.7)) * cos((p.y * 11.1))) * 0.25)) * 0.28) + 0.5);}in vec2 uv;layout(location = 0) out vec4 _ret;
vec4 fs_impl(VsOut vo) { vec2 _cse6 = (vo.uv * 6.0); float _cse2 = terrain(_cse6, U.time); float _cse3 = _cse6.x; float _cse4 = _cse6.y; float _cse5 = radians(U.sun_az); vec3 _cse0 = vec3(((_cse2 - terrain(vec2((_cse3 + 0.015), _cse4), U.time)) * U.exaggeration), ((_cse2 - terrain(vec2(_cse3, (_cse4 + 0.015)), U.time)) * U.exaggeration), 0.015); vec3 _cse1 = vec3((cos(_cse5) * 0.6), (sin(_cse5) * 0.6), 0.55); return vec4((mix(mix(vec3(0.16, 0.32, 0.2), vec3(0.55, 0.49, 0.3), smoothstep(0.3, 0.55, _cse2)), vec3(0.93, 0.93, 0.96), smoothstep(0.62, 0.85, _cse2)) * ((clamp(dot((_cse0 * (1.0 / length(_cse0))), (_cse1 * (1.0 / length(_cse1)))), 0.0, 1.0) * 0.8) + 0.3)), 1.0);}
void main() { VsOut vo; vo.pos = gl_FragCoord; vo.uv = uv; _ret = fs_impl(vo);}{ "bindGroups": [ { "group": 0, "entries": [ { "group": 0, "binding": 0, "name": "U", "space": "uniform", "resourceKind": "uniform-buffer", "structName": "Uniforms" } ] } ], "uniforms": [ { "name": "Uniforms", "size": 32, "align": 16, "fields": [ { "name": "time", "type": "f32", "offset": 0, "align": 4, "size": 4 }, { "name": "resolution", "type": "vec2<f32>", "offset": 8, "align": 8, "size": 8 }, { "name": "sun_az", "type": "f32", "offset": 16, "align": 4, "size": 4 }, { "name": "exaggeration", "type": "f32", "offset": 20, "align": 4, "size": 4 } ] } ], "storage": [], "entries": [ { "name": "vs", "stage": "vertex", "inputs": [ "u32" ], "output": "struct:VsOut" }, { "name": "fs", "stage": "fragment", "inputs": [ "struct:VsOut" ], "output": "vec4<f32>" } ], "overrides": []}
Run it locally:
npx tsx examples/print.ts hillshade
— prints the WGSL, GLSL, reflection. Source:
GitHub.
New to the IR? See Concepts; the full authoring + emit surface is in the API reference.