The common functions shape numbers: abs(), sign(), floor(), ceil(), round(), fract(), mod(), min(), max() and clamp(), plus three that fragment shaders use everywhere: step(edge, x) (0 below the edge, 1 above), smoothstep(e0, e1, x) (an S-curve from 0 to 1) and mix(a, b, t) (linear blend). The geometric functions treat vectors as points and directions: length(), distance(), dot(), cross(), normalize(), faceforward(), reflect() for mirror directions and refract() for glass and water:
show('result = vec4(step(0.5, 0.4), smoothstep(0.0, 1.0, 0.25), mix(10.0, 20.0, 0.25), 0.0);');
show('result = vec4(mod(-1.0, 3.0), fract(-1.25), sign(-2.0), clamp(1.5, 0.0, 1.0));');
show(`result = vec4(reflect(vec3(1.0, -1.0, 0.0), vec3(0.0, 1.0, 0.0)),
length(vec2(3.0, 4.0)));`);
show(`vec3 ray = normalize(vec3(1.0, -1.0, 0.0)); // 45 degrees down, into water
result = vec4(refract(ray, vec3(0.0, 1.0, 0.0), 1.0 / 1.33), 0.0);`);0, 0.15625, 12.5, 0 2, 0.75, -1, 1 1, 1, 0, 5 0.531659, -0.846958, 0, 0
smoothstep(0.0, 1.0, 0.25) is 0.15625, not 0.25: the curve eases in, which is why it softens edges so well. Unlike C's %, mod(-1.0, 3.0) is 2, because GLSL defines it as x - y * floor(x / y). The refracted ray bends toward the normal, and Directional and Point Lighting builds lighting from dot(), normalize() and reflect().
<!doctype html>
<style>
body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
.names { display: flex; max-width: 600px; text-align: center; font-family: monospace; font-size: 11px; }
.names div { padding: 4px 2px; }
</style>
<canvas id="view" width="600" height="200"></canvas>
<div class="names">
<div style="width:25%">step / smoothstep</div><div style="width:25%">mod(x, 0.5), fract(x * 3)</div>
<div style="width:50%">reflect() and refract() at a water surface (eta = 1 / 1.33)</div>
</div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const vs = `#version 300 es
void main() { gl_Position = vec4(gl_VertexID == 1 ? 3.0 : -1.0, gl_VertexID == 2 ? 3.0 : -1.0, 0.0, 1.0); }`;
const fs = `#version 300 es
precision highp float;
uniform float uTime;
out vec4 outColor;
float curve(float y, float f) { return 1.0 - smoothstep(0.0, 1.5, abs(y - f) / fwidth(y - f)); }
// Distance from p to the segment a-b, built from dot(), clamp() and length().
float segment(vec2 p, vec2 a, vec2 b) {
vec2 ab = b - a;
float t = clamp(dot(p - a, ab) / dot(ab, ab), 0.0, 1.0);
return length(p - (a + ab * t));
}
void main() {
vec2 px = gl_FragCoord.xy;
vec3 color = vec3(0.97, 0.96, 0.93);
if (px.x < 150.0) {
vec2 uv = (px - vec2(10.0)) / vec2(130.0, 180.0);
color = mix(color, vec3(0.71, 0.27, 0.18), curve(uv.y, step(0.5, uv.x)));
color = mix(color, vec3(0.12, 0.37, 0.55), curve(uv.y, smoothstep(0.2, 0.8, uv.x)));
} else if (px.x < 300.0) {
vec2 uv = (px - vec2(160.0, 10.0)) / vec2(130.0, 180.0);
color = mix(color, vec3(0.25, 0.49, 0.23), curve(uv.y, mod(uv.x, 0.5) * 2.0));
color = mix(color, vec3(0.88, 0.6, 0.06), curve(uv.y, fract(uv.x * 3.0) * 0.5));
} else {
vec2 p = px - vec2(450.0, 100.0); // origin: where the ray meets the water
if (p.y < 0.0) color = mix(vec3(0.75, 0.87, 0.93), vec3(0.45, 0.65, 0.8), -p.y / 100.0);
float a = 0.35 + 0.35 * sin(uTime); // swing the incoming ray
vec3 ray = normalize(vec3(sin(a), -cos(a), 0.0));
vec3 n = vec3(0.0, 1.0, 0.0);
vec3 bounce = reflect(ray, n); // mirror direction
vec3 bent = refract(ray, n, 1.0 / 1.33); // bends toward the normal
float w = 1.6;
if (segment(p, -ray.xy * 120.0, vec2(0.0)) < w) color = vec3(0.1);
if (segment(p, vec2(0.0), bounce.xy * 120.0) < w) color = vec3(0.88, 0.6, 0.06);
if (segment(p, vec2(0.0), bent.xy * 120.0) < w) color = vec3(0.71, 0.27, 0.18);
if (abs(p.x) < 0.6 && abs(p.y) < 90.0 && mod(p.y, 8.0) < 4.0) color = vec3(0.5); // dashed normal
}
outColor = vec4(color, 1.0);
}`;
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const uTime = gl.getUniformLocation(program, 'uTime');
function frame(ms) {
gl.uniform1f(uTime, ms * 0.0008);
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>