Hillshade

Hillshade

Shaded relief.

Hillshade shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade 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.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { Node } from '../src/index.js';
import type { ShaderExample } from './_shared.js';
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);
}
@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> {
let _cse6 = (vo.uv * 6.0);
let _cse2 = terrain(_cse6, U.time);
let _cse3 = _cse6.x;
let _cse4 = _cse6.y;
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 _cse5 = radians(U.sun_az);
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 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;
float sun_az;
float exaggeration;
} U;
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;
void main() {
vec2 _cse6 = (uv * 6.0);
float _cse2 = terrain(_cse6, U.time);
float _cse3 = _cse6.x;
float _cse4 = _cse6.y;
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);
float _cse5 = radians(U.sun_az);
vec3 _cse1 = vec3((cos(_cse5) * 0.6), (sin(_cse5) * 0.6), 0.55);
_ret = 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);
}

Hillshade, 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