Add ios-application-dev and shader-dev skills

Power by MiniMax
This commit is contained in:
akai
2026-03-17 17:25:23 +08:00
parent cfddb22ea3
commit 1706804893
91 changed files with 41607 additions and 4 deletions

View File

@@ -0,0 +1,364 @@
## WebGL2 Adaptation Requirements
**IMPORTANT: GLSL Type Strictness**: float and vec types cannot be implicitly converted. `vec3 v = 1.0;` is illegal; you must use the vector form (e.g., `vec3(1.0)`, `vec3(1.0) * x`, `value * vec3(1.0)`).
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
- Use `canvas.getContext("webgl2")`
- Shader first line: `#version 300 es`, add `precision highp float;` in fragment shader
- Vertex shader: `attribute` -> `in`, `varying` -> `out`
- Fragment shader: `varying` -> `in`, `gl_FragColor` -> custom `out vec4 fragColor`, `texture2D()` -> `texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to the standard `void main()` entry point
# SDF Ambient Occlusion
## Use Cases
- Simulating indirect light occlusion in raymarching / SDF scenes
- Adding spatial depth and contact shadows (darkening in concavities and crevices)
- From 5 samples (performance priority) to 32 hemisphere samples (quality priority)
## Core Principles
Sample the SDF along the surface normal direction at multiple distances, comparing the "expected distance" with the "actual distance" to estimate occlusion.
For surface point P, normal N, and sampling distance h:
- Expected distance = h (SDF should equal h when surroundings are open)
- Actual distance = map(P + N * h)
- Occlusion contribution = h - map(P + N * h) (larger difference = stronger occlusion)
```
AO = 1 - k * sum(weight_i * max(0, h_i - map(P + N * h_i)))
```
Result: 1.0 = no occlusion, 0.0 = fully occluded. Weights decay exponentially (closer samples have higher weight).
## Implementation Steps
### Step 1: SDF Scene
```glsl
float map(vec3 p) {
float d = p.y; // ground
d = min(d, length(p - vec3(0.0, 1.0, 0.0)) - 1.0); // sphere
d = min(d, length(vec2(length(p.xz) - 1.5, p.y - 0.5)) - 0.4); // torus
return d;
}
```
### Step 2: Normal Calculation
```glsl
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
```
### Step 3: Classic Normal-Direction AO (5 Samples)
```glsl
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0; // sampling distance 0.01~0.13
float d = map(pos + h * nor);
occ += (h - d) * sca; // (expected - actual) * weight
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
```
### Step 4: Applying AO to Lighting
```glsl
float ao = calcAO(pos, nor);
// affect ambient light only (physically correct)
vec3 ambient = vec3(0.2, 0.3, 0.5) * ao;
vec3 color = diffuse * shadow + ambient;
// affect all lighting (visually stronger)
vec3 color = (diffuse * shadow + ambient) * ao;
// combined with sky visibility
float skyVis = 0.5 + 0.5 * nor.y;
vec3 color = diffuse * shadow + ambient * ao * skyVis;
```
### Step 5: Raymarching Integration
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// ... camera setup, ray generation ...
float t = 0.0;
for (int i = 0; i < 128; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if (d < 0.001) break;
t += d;
if (t > 100.0) break;
}
vec3 col = vec3(0.0);
if (t < 100.0) {
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
float ao = calcAO(pos, nor);
vec3 lig = normalize(vec3(1.0, 0.8, -0.6));
float dif = clamp(dot(nor, lig), 0.0, 1.0);
float sky = 0.5 + 0.5 * nor.y;
col = vec3(1.0) * dif + vec3(0.2, 0.3, 0.5) * sky * ao;
}
fragColor = vec4(col, 1.0);
}
```
## Complete Code Template
Runs directly in ShaderToy:
```glsl
// SDF Ambient Occlusion — ShaderToy Template
// Synthesized from classic raymarching implementations
#define AO_STEPS 5
#define AO_MAX_DIST 0.12
#define AO_MIN_DIST 0.01
#define AO_DECAY 0.95
#define AO_STRENGTH 3.0
#define MARCH_STEPS 128
#define MAX_DIST 100.0
#define SURF_DIST 0.001
float map(vec3 p) {
float ground = p.y;
float sphere = length(p - vec3(0.0, 1.0, 0.0)) - 1.0;
float torus = length(vec2(length(p.xz) - 1.5, p.y - 0.5)) - 0.4;
float box = length(max(abs(p - vec3(-2.5, 0.75, 0.0)) - vec3(0.75), 0.0)) - 0.05;
float d = min(ground, sphere);
d = min(d, torus);
d = min(d, box);
return d;
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < AO_STEPS; i++) {
float h = AO_MIN_DIST + AO_MAX_DIST * float(i) / float(AO_STEPS - 1);
float d = map(pos + h * nor);
occ += (h - d) * sca;
sca *= AO_DECAY;
}
return clamp(1.0 - AO_STRENGTH * occ, 0.0, 1.0);
}
float calcShadow(vec3 ro, vec3 rd, float mint, float maxt, float k) {
float res = 1.0;
float t = mint;
for (int i = 0; i < 64; i++) {
float h = map(ro + rd * t);
res = min(res, k * h / t);
t += clamp(h, 0.01, 0.2);
if (res < 0.001 || t > maxt) break;
}
return clamp(res, 0.0, 1.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float an = 0.3 * iTime;
vec3 ro = vec3(4.0 * cos(an), 2.5, 4.0 * sin(an));
vec3 ta = vec3(0.0, 0.5, 0.0);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = cross(uu, ww);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.8 * ww);
float t = 0.0;
for (int i = 0; i < MARCH_STEPS; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if (d < SURF_DIST) break;
t += d;
if (t > MAX_DIST) break;
}
vec3 col = vec3(0.4, 0.5, 0.7) - 0.3 * rd.y;
if (t < MAX_DIST) {
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
float ao = calcAO(pos, nor);
vec3 lig = normalize(vec3(0.8, 0.6, -0.5));
float dif = clamp(dot(nor, lig), 0.0, 1.0);
float sha = calcShadow(pos + nor * 0.01, lig, 0.02, 20.0, 8.0);
float sky = 0.5 + 0.5 * nor.y;
vec3 mate = vec3(0.18);
if (pos.y < 0.01) {
float f = mod(floor(pos.x) + floor(pos.z), 2.0);
mate = 0.1 + 0.08 * f * vec3(1.0);
}
col = vec3(0.0);
col += mate * vec3(1.0, 0.9, 0.7) * dif * sha;
col += mate * vec3(0.2, 0.3, 0.5) * sky * ao;
col += mate * vec3(0.3, 0.2, 0.1) * clamp(-nor.y, 0.0, 1.0) * ao;
}
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Multiplicative AO (Spout / P_Malin)
```glsl
float calcAO_multiplicative(vec3 pos, vec3 nor) {
float ao = 1.0;
float dist = 0.0;
for (int i = 0; i <= 5; i++) {
dist += 0.1;
float d = map(pos + nor * dist);
ao *= 1.0 - max(0.0, (dist - d) * 0.2 / dist);
}
return ao;
}
```
### Multi-Scale Separated AO (Protophore / Eric Heitz)
Exponentially increasing sampling distances, separating short-range contact shadows from long-range ambient occlusion, fully unrolled without loops.
```glsl
float calcAO_multiscale(vec3 pos, vec3 nor) {
float aoS = 1.0;
aoS *= clamp(map(pos + nor * 0.1) * 10.0, 0.0, 1.0);
aoS *= clamp(map(pos + nor * 0.2) * 5.0, 0.0, 1.0);
aoS *= clamp(map(pos + nor * 0.4) * 2.5, 0.0, 1.0);
aoS *= clamp(map(pos + nor * 0.8) * 1.25, 0.0, 1.0);
float ao = aoS;
ao *= clamp(map(pos + nor * 1.6) * 0.625, 0.0, 1.0);
ao *= clamp(map(pos + nor * 3.2) * 0.3125, 0.0, 1.0);
ao *= clamp(map(pos + nor * 6.4) * 0.15625,0.0, 1.0);
return max(0.035, pow(ao, 0.3));
}
```
### Jittered Sampling AO
Hash jittering breaks banding artifacts, `1/(1+l)` distance falloff.
```glsl
float hash(float n) { return fract(sin(n) * 43758.5453); }
float calcAO_jittered(vec3 pos, vec3 nor, float maxDist) {
float ao = 0.0;
const float nbIte = 6.0;
for (float i = 1.0; i < nbIte + 0.5; i++) {
float l = (i + hash(i)) * 0.5 / nbIte * maxDist;
ao += (l - map(pos + nor * l)) / (1.0 + l);
}
return clamp(1.0 - ao / nbIte, 0.0, 1.0);
}
// call: calcAO_jittered(pos, nor, 4.0)
```
### Hemisphere Random Direction AO
Random direction sampling within the normal hemisphere, closer to physically accurate, requires 32 samples.
```glsl
vec2 hash2(float n) {
return fract(sin(vec2(n, n + 1.0)) * vec2(43758.5453, 22578.1459));
}
float calcAO_hemisphere(vec3 pos, vec3 nor, float seed) {
float occ = 0.0;
for (int i = 0; i < 32; i++) {
float h = 0.01 + 4.0 * pow(float(i) / 31.0, 2.0);
vec2 an = hash2(seed + float(i) * 13.1) * vec2(3.14159, 6.2831);
vec3 dir = vec3(sin(an.x) * sin(an.y), sin(an.x) * cos(an.y), cos(an.x));
dir *= sign(dot(dir, nor));
occ += clamp(5.0 * map(pos + h * dir) / h, -1.0, 1.0);
}
return clamp(occ / 32.0, 0.0, 1.0);
}
```
### Fibonacci Sphere Uniform Hemisphere AO
Fibonacci sphere points for quasi-uniform hemisphere sampling, avoiding random clustering.
```glsl
vec3 forwardSF(float i, float n) {
const float PI = 3.141592653589793;
const float PHI = 1.618033988749895;
float phi = 2.0 * PI * fract(i / PHI);
float zi = 1.0 - (2.0 * i + 1.0) / n;
float sinTheta = sqrt(1.0 - zi * zi);
return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, zi);
}
float hash1(float n) { return fract(sin(n) * 43758.5453); }
float calcAO_fibonacci(vec3 pos, vec3 nor) {
float ao = 0.0;
for (int i = 0; i < 32; i++) {
vec3 ap = forwardSF(float(i), 32.0);
float h = hash1(float(i));
ap *= sign(dot(ap, nor)) * h * 0.1;
ao += clamp(map(pos + nor * 0.01 + ap) * 3.0, 0.0, 1.0);
}
ao /= 32.0;
return clamp(ao * 6.0, 0.0, 1.0);
}
```
## Performance & Composition
### Performance Tips
- **Bottleneck**: Number of `map()` calls. Each AO sample = one full SDF evaluation
- **Sample count selection**: Classic normal-direction 3~5 samples is sufficient; hemisphere sampling needs 16~32
- **Early exit**: `if (occ > 0.35) break;` skips over heavily occluded regions
- **Unroll loops**: Fixed iteration count (4~7) manually unrolled is more GPU-friendly
- **Distance degradation**: `float aoSteps = mix(5.0, 2.0, clamp(t / 50.0, 0.0, 1.0));`
- **Preprocessor toggle**: `#ifdef ENABLE_AMBIENT_OCCLUSION` for on/off control
- **SDF simplification**: AO sampling can use a simplified `map()`, ignoring fine details
### Composition Tips
- **AO + Soft Shadow**: `col = diffuse * sha + ambient * ao;`
- **AO + Sky Visibility**: `col += skyColor * ao * (0.5 + 0.5 * nor.y);`
- **AO + Bounce Light/SSS**: `col += bounceColor * bou * ao;`
- **AO + Convexity Detection**: Sample along both +N/-N to get both AO and convexity
- **AO + Fresnel Reflection**: `col += envColor * fre * ao;` reduces environment reflection in occluded areas
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/ambient-occlusion.md)

View File

@@ -0,0 +1,542 @@
# Analytic Ray Tracing
## Use Cases
- Rendering scenes composed of geometric primitives (spheres, planes, boxes, cylinders, ellipsoids, etc.)
- Requiring precise surface intersection points, normals, and distance calculations (no iterative approximation)
- Building the underlying geometry engine for ray tracers / path tracers
- Scenes requiring accurate shadows, reflections, and refractions
## Core Principles
Substitute the ray equation `P(t) = O + tD` into the geometric body's implicit equation to obtain an algebraic equation in `t`, then solve it in closed form.
**Unified intersection workflow**: Build equation -> Simplify to standard form -> Discriminant test -> Take smallest positive root -> Compute gradient at intersection for normal
**Key formulas**:
- **Sphere** `|P-C|^2 = r^2` -> Quadratic equation
- **Plane** `N·P + d = 0` -> Linear equation
- **Box** Intersection of three pairs of parallel planes -> Slab Method
- **Ellipsoid** `|P/R|^2 = 1` -> Sphere intersection in scaled space
- **Torus** `(|P_xy| - R)^2 + P_z^2 = r^2` -> Quartic equation
## Implementation Steps
### Step 1: Ray Generation
```glsl
vec3 generateRay(vec2 fragCoord, vec2 resolution, vec3 ro, vec3 ta) {
vec2 p = (2.0 * fragCoord - resolution) / resolution.y;
vec3 cw = normalize(ta - ro);
vec3 cu = normalize(cross(cw, vec3(0, 1, 0)));
vec3 cv = cross(cu, cw);
float fov = 1.5;
return normalize(p.x * cu + p.y * cv + fov * cw);
}
```
### Step 2: Ray-Sphere Intersection
```glsl
// Optimized version with sphere center at origin
float iSphere(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, float r) {
float b = dot(ro, rd);
float c = dot(ro, ro) - r * r;
float h = b * b - c;
if (h < 0.0) return MAX_DIST;
h = sqrt(h);
float d1 = -b - h;
float d2 = -b + h;
if (d1 >= distBound.x && d1 <= distBound.y) {
normal = normalize(ro + rd * d1);
return d1;
} else if (d2 >= distBound.x && d2 <= distBound.y) {
normal = normalize(ro + rd * d2);
return d2;
}
return MAX_DIST;
}
```
```glsl
// General version, supports arbitrary sphere center (sph = vec4(center.xyz, radius))
float sphIntersect(vec3 ro, vec3 rd, vec4 sph) {
vec3 oc = ro - sph.xyz;
float b = dot(oc, rd);
float c = dot(oc, oc) - sph.w * sph.w;
float h = b * b - c;
if (h < 0.0) return -1.0;
return -b - sqrt(h);
}
```
### Step 3: Ray-Plane Intersection
```glsl
float iPlane(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal,
vec3 planeNormal, float planeDist) {
float denom = dot(rd, planeNormal);
if (denom > 0.0) return MAX_DIST;
float d = -(dot(ro, planeNormal) + planeDist) / denom;
if (d < distBound.x || d > distBound.y) return MAX_DIST;
normal = planeNormal;
return d;
}
// fast horizontal ground plane
float iGroundPlane(vec3 ro, vec3 rd, float height) {
return -(ro.y - height) / rd.y;
}
```
### Step 4: Ray-Box Intersection (Slab Method)
```glsl
float iBox(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, vec3 boxSize) {
vec3 m = sign(rd) / max(abs(rd), 1e-8);
vec3 n = m * ro;
vec3 k = abs(m) * boxSize;
vec3 t1 = -n - k;
vec3 t2 = -n + k;
float tN = max(max(t1.x, t1.y), t1.z);
float tF = min(min(t2.x, t2.y), t2.z);
if (tN > tF || tF <= 0.0) return MAX_DIST;
if (tN >= distBound.x && tN <= distBound.y) {
normal = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);
return tN;
} else if (tF >= distBound.x && tF <= distBound.y) {
normal = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);
return tF;
}
return MAX_DIST;
}
```
### Step 5: Ray-Ellipsoid Intersection
```glsl
// Transform to unit sphere space for intersection, transform normal back to original space
float iEllipsoid(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, vec3 rad) {
vec3 ocn = ro / rad;
vec3 rdn = rd / rad;
float a = dot(rdn, rdn);
float b = dot(ocn, rdn);
float c = dot(ocn, ocn);
float h = b * b - a * (c - 1.0);
if (h < 0.0) return MAX_DIST;
float d = (-b - sqrt(h)) / a;
if (d < distBound.x || d > distBound.y) return MAX_DIST;
normal = normalize((ro + d * rd) / rad);
return d;
}
```
### Step 6: Ray-Cylinder Intersection (With End Caps)
```glsl
// pa, pb: cylinder axis endpoints, ra: radius
float iCylinder(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal,
vec3 pa, vec3 pb, float ra) {
vec3 ca = pb - pa;
vec3 oc = ro - pa;
float caca = dot(ca, ca);
float card = dot(ca, rd);
float caoc = dot(ca, oc);
float a = caca - card * card;
float b = caca * dot(oc, rd) - caoc * card;
float c = caca * dot(oc, oc) - caoc * caoc - ra * ra * caca;
float h = b * b - a * c;
if (h < 0.0) return MAX_DIST;
h = sqrt(h);
float d = (-b - h) / a;
float y = caoc + d * card;
if (y > 0.0 && y < caca && d >= distBound.x && d <= distBound.y) {
normal = (oc + d * rd - ca * y / caca) / ra;
return d;
}
d = ((y < 0.0 ? 0.0 : caca) - caoc) / card;
if (abs(b + a * d) < h && d >= distBound.x && d <= distBound.y) {
normal = normalize(ca * sign(y) / caca);
return d;
}
return MAX_DIST;
}
```
### Step 7: Scene Intersection and Shading
```glsl
#define MAX_DIST 1e10
vec3 worldHit(vec3 ro, vec3 rd, vec2 dist, out vec3 normal) {
vec3 d = vec3(dist, 0.0);
vec3 tmpNormal;
float t;
t = iPlane(ro, rd, d.xy, normal, vec3(0, 1, 0), 0.0);
if (t < d.y) { d.y = t; d.z = 1.0; }
t = iSphere(ro - vec3(0, 0.5, 0), rd, d.xy, tmpNormal, 0.5);
if (t < d.y) { d.y = t; d.z = 2.0; normal = tmpNormal; }
t = iBox(ro - vec3(2, 0.5, 0), rd, d.xy, tmpNormal, vec3(0.5));
if (t < d.y) { d.y = t; d.z = 3.0; normal = tmpNormal; }
return d;
}
vec3 shade(vec3 pos, vec3 normal, vec3 rd, vec3 albedo) {
vec3 lightDir = normalize(vec3(-1.0, 0.75, 1.0));
float diff = max(dot(normal, lightDir), 0.0);
float amb = 0.5 + 0.5 * normal.y;
return albedo * (amb * 0.2 + diff * 0.8);
}
```
> **IMPORTANT: Critical pitfall**: `d.xy` must be passed as distBound, and `d.y` must be updated each time a closer intersection is found! If the deployed code passes the original `dist` directly without updating, the intersection logic will fail (all object distance tests become invalid), resulting in a completely black screen.
```glsl
#define MAX_BOUNCES 4
#define EPSILON 0.001
float schlickFresnel(float cosTheta, float F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
vec3 radiance(vec3 ro, vec3 rd) {
vec3 color = vec3(0.0);
vec3 mask = vec3(1.0);
vec3 normal;
for (int i = 0; i < MAX_BOUNCES; i++) {
vec3 res = worldHit(ro, rd, vec2(EPSILON, MAX_DIST), normal);
if (res.z < 0.5) {
color += mask * vec3(0.6, 0.8, 1.0);
break;
}
vec3 hitPos = ro + rd * res.y;
vec3 albedo = getAlbedo(res.z);
float F = schlickFresnel(max(0.0, dot(normal, -rd)), 0.04);
color += mask * (1.0 - F) * shade(hitPos, normal, rd, albedo);
mask *= F * albedo;
rd = reflect(rd, normal);
ro = hitPos + EPSILON * rd;
}
return color;
}
```
## Complete Code Template
Runs directly on ShaderToy, includes sphere, plane, and box primitives with reflection and Blinn-Phong shading.
> **IMPORTANT: Must follow**: All intersection function calls must use `d.xy` as the `distBound` parameter, and update `d.y` after each closer intersection is found. Incorrect usage: `iSphere(ro, rd, dist, ...)` (always using the original dist). Correct usage: `iSphere(ro, rd, d.xy, ...)` followed by `if (t < d.y) { d.y = t; ... }` to update.
```glsl
// Analytic Ray Tracing - Complete ShaderToy Template
#define MAX_DIST 1e10
#define EPSILON 0.001
#define MAX_BOUNCES 3
#define FOV 1.5
#define GAMMA 2.2
#define SHADOW_ENABLED true
float iSphere(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, float r) {
float b = dot(ro, rd);
float c = dot(ro, ro) - r * r;
float h = b * b - c;
if (h < 0.0) return MAX_DIST;
h = sqrt(h);
float d1 = -b - h, d2 = -b + h;
if (d1 >= distBound.x && d1 <= distBound.y) { normal = normalize(ro + rd * d1); return d1; }
if (d2 >= distBound.x && d2 <= distBound.y) { normal = normalize(ro + rd * d2); return d2; }
return MAX_DIST;
}
float iPlane(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal,
vec3 planeNormal, float planeDist) {
float denom = dot(rd, planeNormal);
if (denom > 0.0) return MAX_DIST;
float d = -(dot(ro, planeNormal) + planeDist) / denom;
if (d < distBound.x || d > distBound.y) return MAX_DIST;
normal = planeNormal;
return d;
}
float iBox(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, vec3 boxSize) {
vec3 m = sign(rd) / max(abs(rd), 1e-8);
vec3 n = m * ro;
vec3 k = abs(m) * boxSize;
vec3 t1 = -n - k, t2 = -n + k;
float tN = max(max(t1.x, t1.y), t1.z);
float tF = min(min(t2.x, t2.y), t2.z);
if (tN > tF || tF <= 0.0) return MAX_DIST;
if (tN >= distBound.x && tN <= distBound.y) {
normal = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz); return tN;
}
if (tF >= distBound.x && tF <= distBound.y) {
normal = -sign(rd) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz); return tF;
}
return MAX_DIST;
}
struct Material { vec3 albedo; float specular; float roughness; };
Material getMaterial(float matId, vec3 pos) {
if (matId < 1.5) {
float checker = mod(floor(pos.x) + floor(pos.z), 2.0);
return Material(vec3(0.4 + 0.4 * checker), 0.02, 0.8);
} else if (matId < 2.5) { return Material(vec3(1.0, 0.2, 0.2), 0.5, 0.3); }
else if (matId < 3.5) { return Material(vec3(0.2, 0.4, 1.0), 0.1, 0.6); }
else if (matId < 4.5) { return Material(vec3(1.0, 1.0, 1.0), 0.8, 0.05); }
else { return Material(vec3(0.8, 0.6, 0.2), 0.3, 0.4); }
}
vec3 worldHit(vec3 ro, vec3 rd, vec2 dist, out vec3 normal) {
vec3 d = vec3(dist, 0.0); vec3 tmp; float t;
t = iPlane(ro, rd, d.xy, tmp, vec3(0, 1, 0), 0.0);
if (t < d.y) { d.y = t; d.z = 1.0; normal = tmp; }
t = iSphere(ro - vec3(-2.0, 1.0, 0.0), rd, d.xy, tmp, 1.0);
if (t < d.y) { d.y = t; d.z = 2.0; normal = tmp; }
t = iSphere(ro - vec3(0.0, 0.6, 2.0), rd, d.xy, tmp, 0.6);
if (t < d.y) { d.y = t; d.z = 3.0; normal = tmp; }
t = iSphere(ro - vec3(2.0, 0.8, -1.0), rd, d.xy, tmp, 0.8);
if (t < d.y) { d.y = t; d.z = 4.0; normal = tmp; }
t = iBox(ro - vec3(0.0, 0.5, -2.0), rd, d.xy, tmp, vec3(0.5));
if (t < d.y) { d.y = t; d.z = 5.0; normal = tmp; }
return d;
}
float shadow(vec3 ro, vec3 rd, float maxDist) {
vec3 normal;
vec3 res = worldHit(ro, rd, vec2(EPSILON, maxDist), normal);
return res.z > 0.5 ? 0.3 : 1.0;
}
float schlick(float cosTheta, float F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
vec3 skyColor(vec3 rd) {
vec3 col = mix(vec3(1.0), vec3(0.5, 0.7, 1.0), 0.5 + 0.5 * rd.y);
vec3 sunDir = normalize(vec3(-0.4, 0.7, -0.6));
float sun = clamp(dot(sunDir, rd), 0.0, 1.0);
col += vec3(1.0, 0.6, 0.1) * (pow(sun, 4.0) + 10.0 * pow(sun, 32.0));
return col;
}
vec3 render(vec3 ro, vec3 rd) {
vec3 color = vec3(0.0), mask = vec3(1.0), normal;
for (int bounce = 0; bounce < MAX_BOUNCES; bounce++) {
vec3 res = worldHit(ro, rd, vec2(EPSILON, 100.0), normal);
if (res.z < 0.5) { color += mask * skyColor(rd); break; }
vec3 hitPos = ro + rd * res.y;
Material mat = getMaterial(res.z, hitPos);
vec3 lightDir = normalize(vec3(-0.4, 0.7, -0.6));
float diff = max(dot(normal, lightDir), 0.0);
float amb = 0.5 + 0.5 * normal.y;
float sha = SHADOW_ENABLED ? shadow(hitPos + normal * EPSILON, lightDir, 50.0) : 1.0;
vec3 halfVec = normalize(lightDir - rd);
float spec = pow(max(dot(normal, halfVec), 0.0), 1.0 / max(mat.roughness, 0.001));
float F = schlick(max(0.0, dot(normal, -rd)), 0.04 + 0.96 * mat.specular);
vec3 diffCol = mat.albedo * (amb * 0.15 + diff * sha * 0.85);
vec3 specCol = vec3(spec * sha);
color += mask * mix(diffCol, specCol, F * mat.specular);
mask *= F * mat.albedo;
if (length(mask) < 0.01) break;
rd = reflect(rd, normal);
ro = hitPos + normal * EPSILON;
}
return color;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float angle = 0.3 * iTime;
vec3 ro = vec3(4.0 * cos(angle), 2.5, 4.0 * sin(angle));
vec3 ta = vec3(0.0, 0.5, 0.0);
vec3 cw = normalize(ta - ro);
vec3 cu = normalize(cross(cw, vec3(0, 1, 0)));
vec3 cv = cross(cu, cw);
vec3 rd = normalize(p.x * cu + p.y * cv + FOV * cw);
vec3 col = render(ro, rd);
col = col / (1.0 + col);
col = pow(col, vec3(1.0 / GAMMA));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Path Tracing
```glsl
vec3 cosWeightedRandomHemisphereDirection(vec3 n, inout uint seed) {
uint ri = seed * 1103515245u + 12345u;
seed = ri;
float r1 = float(ri) / float(0xFFFFFFFFu);
ri = seed * 1103515245u + 12345u;
seed = ri;
float r2 = float(ri) / float(0xFFFFFFFFu);
vec3 uu = normalize(cross(n, abs(n.y) > 0.5 ? vec3(1,0,0) : vec3(0,1,0)));
vec3 vv = cross(uu, n);
float ra = sqrt(r1);
float rx = ra * cos(6.2831 * r2);
float ry = ra * sin(6.2831 * r2);
float rz = sqrt(1.0 - r1);
return normalize(rx * uu + ry * vv + rz * n);
}
// In the bounce loop, replace reflect with:
// rd = cosWeightedRandomHemisphereDirection(normal, seed);
// ro = hitPos + EPSILON * rd;
// mask *= mat.albedo;
```
### Variant 2: Analytic Soft Shadow
```glsl
float sphSoftShadow(vec3 ro, vec3 rd, vec4 sph) {
vec3 oc = ro - sph.xyz;
float b = dot(oc, rd);
float c = dot(oc, oc) - sph.w * sph.w;
float h = b * b - c;
float d = sqrt(max(0.0, sph.w * sph.w - h)) - sph.w;
float t = -b - sqrt(max(h, 0.0));
return (t > 0.0) ? max(d, 0.0) / t : 1.0;
}
```
### Variant 3: Analytic Anti-Aliasing
```glsl
vec2 sphDistances(vec3 ro, vec3 rd, vec4 sph) {
vec3 oc = ro - sph.xyz;
float b = dot(oc, rd);
float c = dot(oc, oc) - sph.w * sph.w;
float h = b * b - c;
float d = sqrt(max(0.0, sph.w * sph.w - h)) - sph.w;
return vec2(d, -b - sqrt(max(h, 0.0)));
}
// float px = 2.0 / iResolution.y;
// vec2 dt = sphDistances(ro, rd, sph);
// float coverage = 1.0 - clamp(dt.x / (dt.y * px), 0.0, 1.0);
// col = mix(bgColor, sphereColor, coverage);
```
### Variant 4: Refraction (Snell's Law)
```glsl
// Requires a random number function defined first
float hash1(float p) {
return fract(sin(p) * 43758.5453);
}
// Add refraction branch in the render loop:
float refrIndex = 1.5; // glass ~ 1.5, water ~ 1.33
bool inside = dot(rd, normal) > 0.0;
vec3 n = inside ? -normal : normal;
float eta = inside ? refrIndex : 1.0 / refrIndex;
vec3 refracted = refract(rd, n, eta);
float cosI = abs(dot(rd, n));
float F = schlick(cosI, pow((1.0 - eta) / (1.0 + eta), 2.0));
// Use bounce count as random seed
float randSeed = float(bounce) + 1.0;
if (refracted != vec3(0.0) && hash1(randSeed * 12.9898) > F) {
rd = refracted;
} else {
rd = reflect(rd, n);
}
ro = hitPos + rd * EPSILON;
```
### Variant 5: Higher-Order Algebraic Surface (Sphere4)
```glsl
float iSphere4(vec3 ro, vec3 rd, vec2 distBound, inout vec3 normal, float ra) {
float r2 = ra * ra;
vec3 d2 = rd*rd, d3 = d2*rd;
vec3 o2 = ro*ro, o3 = o2*ro;
float ka = 1.0 / dot(d2, d2);
float k0 = ka * dot(ro, d3);
float k1 = ka * dot(o2, d2);
float k2 = ka * dot(o3, rd);
float k3 = ka * (dot(o2, o2) - r2 * r2);
float c0 = k1 - k0 * k0;
float c1 = k2 + 2.0 * k0 * (k0 * k0 - 1.5 * k1);
float c2 = k3 - 3.0 * k0 * (k0 * (k0 * k0 - 2.0 * k1) + 4.0/3.0 * k2);
float p = c0 * c0 * 3.0 + c2;
float q = c0 * c0 * c0 - c0 * c2 + c1 * c1;
float h = q * q - p * p * p * (1.0/27.0);
if (h < 0.0) return MAX_DIST;
h = sqrt(h);
float s = sign(q+h) * pow(abs(q+h), 1.0/3.0);
float t = sign(q-h) * pow(abs(q-h), 1.0/3.0);
vec2 v = vec2((s+t) + c0*4.0, (s-t) * sqrt(3.0)) * 0.5;
float r = length(v);
float d = -abs(v.y) / sqrt(r + v.x) - c1/r - k0;
if (d >= distBound.x && d <= distBound.y) {
vec3 pos = ro + rd * d;
normal = normalize(pos * pos * pos);
return d;
}
return MAX_DIST;
}
```
## Common Errors and Safeguards
### Error 1: Distance Bound Not Updated
**Symptom**: Screen is completely black or shows only background
**Cause**: `distBound.y` not updated after each intersection
**Fix**:
```glsl
// WRONG:
t = iSphere(ro, rd, dist, tmpNormal, 1.0);
// CORRECT:
t = iSphere(ro, rd, d.xy, tmpNormal, 1.0);
if (t < d.y) { d.y = t; d.z = matId; normal = tmpNormal; }
```
### Error 2: EPSILON Too Small Causing Self-Intersection Artifacts
**Symptom**: Black spots or artifacts on object surfaces
**Cause**: `EPSILON` value too small, ray still intersects with itself
**Fix**: Adjust EPSILON based on scene scale; typical values 1e-3 ~ 1e-2
### Error 3: Variable Used as Loop Upper Bound
**Symptom**: WebGL2 compilation failure or shader crash
**Cause**: In GLSL ES 3.0, `for` loop upper bounds must be constants
**Fix**: Use `#define` for loop upper bounds, and keep bounds to 4-5 iterations max
### Error 4: Division by Zero Causing NaN
**Symptom**: Stripe patterns from NaN propagation across the screen
**Cause**: Division not protected when ray direction components are zero
**Fix**: Always use `max(abs(x), 1e-8)` or similar protection
### Error 5: Missing Hash Function in Refraction Variant
**Symptom**: Compilation error "undefined function 'hash1'"
**Fix**: Add the function definition when using the refraction variant:
```glsl
float hash1(float p) {
return fract(sin(p) * 43758.5453);
}
```
## Performance & Composition
**Performance tips**:
- **Distance bound clipping**: Shorten `distBound.y` after each closer intersection; subsequent objects are automatically skipped
- **Bounding sphere pre-test**: Pre-screen with bounding sphere for complex geometry (torus, etc.)
- **Shadow ray simplification**: Only need to determine occlusion, no normal calculation needed
- **Avoid unnecessary sqrt**: Return early when discriminant is negative; `c > 0.0 && b > 0.0` for fast rejection
- **Grid acceleration**: Use 3D DDA grid traversal for large numbers of similar primitives
**Composition approaches**:
- **+ Raymarching SDF**: Analytic primitives define major structures, SDF handles complex details
- **+ Volume effects**: Analytic intersection provides precise entry/exit distances for volume sampling within the range
- **+ PBR materials**: Precise normals plug directly into Cook-Torrance and other BRDFs
- **+ Spatial transforms**: Rotate/translate rays to reuse the same intersection functions
- **+ Analytic AA/AO/soft shadows**: Fully analytic pipeline, zero noise
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/analytic-ray-tracing.md)

View File

@@ -0,0 +1,124 @@
# Anti-Aliasing Techniques
## Use Cases
- Eliminating jagged edges (staircase artifacts) in ray-marched or SDF-rendered scenes
- Smooth 2D SDF shape rendering
- Post-process edge smoothing for any shader output
- Temporal smoothing for noise reduction
## Core Principles
Anti-aliasing in shaders differs from rasterization pipelines. Without hardware MSAA on procedural geometry, we rely on analytical or post-process approaches.
## Techniques
### 1. Supersampling (SSAA) for Ray Marching
Render multiple sub-pixel samples and average:
```glsl
#define AA 2 // 1=off, 2=4x, 3=9x
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec3 totalColor = vec3(0.0);
for (int m = 0; m < AA; m++)
for (int n = 0; n < AA; n++) {
vec2 offset = vec2(float(m), float(n)) / float(AA) - 0.5;
vec2 uv = (2.0 * (fragCoord + offset) - iResolution.xy) / iResolution.y;
vec3 col = render(uv);
totalColor += col;
}
fragColor = vec4(totalColor / float(AA * AA), 1.0);
}
```
Cost: AA^2 × full render. Use AA=2 for quality, AA=1 for development.
### 2. SDF Analytical Anti-Aliasing
For 2D SDF shapes, use pixel width to compute smooth edges:
```glsl
float d = sdShape(uv);
float fw = fwidth(d); // screen-space derivative of SDF
float alpha = smoothstep(fw, -fw, d); // smooth edge over exactly 1 pixel
// Alternative: manual pixel width for more control
float pixelWidth = 2.0 / iResolution.y; // approximate pixel size in UV space
float alpha2 = smoothstep(pixelWidth, -pixelWidth, d);
```
For 3D SDF scenes, apply anti-aliasing at the edge of geometry:
```glsl
// After ray marching, at the surface:
float edgeFade = 1.0 - smoothstep(0.0, 0.01 * t, lastSdfValue);
// t = ray distance — scales threshold with distance for consistent edge width
```
### 3. Temporal Anti-Aliasing (TAA) Basics
Blend current frame with previous frame using a multipass buffer:
```glsl
// Buffer A: render with sub-pixel jitter
vec2 jitter = (hash22(vec2(iFrame)) - 0.5) / iResolution.xy;
vec2 uv = (fragCoord + jitter) / iResolution.xy;
vec3 currentColor = render(uv);
// Buffer A output: store current render
fragColor = vec4(currentColor, 1.0);
// Image shader: blend with history
vec3 current = texture(iChannel0, fragCoord / iResolution.xy).rgb; // this frame
vec3 history = texture(iChannel1, fragCoord / iResolution.xy).rgb; // previous frame
float blend = 0.9; // higher = smoother but more ghosting
fragColor = vec4(mix(current, history, blend), 1.0);
```
Note: Full TAA also needs motion vectors and neighborhood clamping to avoid ghosting.
### 4. FXAA (Fast Approximate Anti-Aliasing)
Simplified post-process edge detection and smoothing:
```glsl
vec3 fxaa(sampler2D tex, vec2 uv, vec2 texelSize) {
// Sample center and 4 neighbors
vec3 rgbM = texture(tex, uv).rgb;
vec3 rgbN = texture(tex, uv + vec2(0.0, texelSize.y)).rgb;
vec3 rgbS = texture(tex, uv - vec2(0.0, texelSize.y)).rgb;
vec3 rgbE = texture(tex, uv + vec2(texelSize.x, 0.0)).rgb;
vec3 rgbW = texture(tex, uv - vec2(texelSize.x, 0.0)).rgb;
// Luma for edge detection
vec3 lumaCoeff = vec3(0.299, 0.587, 0.114);
float lumaN = dot(rgbN, lumaCoeff);
float lumaS = dot(rgbS, lumaCoeff);
float lumaE = dot(rgbE, lumaCoeff);
float lumaW = dot(rgbW, lumaCoeff);
float lumaM = dot(rgbM, lumaCoeff);
float lumaMin = min(lumaM, min(min(lumaN, lumaS), min(lumaE, lumaW)));
float lumaMax = max(lumaM, max(max(lumaN, lumaS), max(lumaE, lumaW)));
float lumaRange = lumaMax - lumaMin;
// Skip if edge contrast is low
if (lumaRange < max(0.0312, lumaMax * 0.125)) return rgbM;
// Blend along edge direction
vec2 dir;
dir.x = -((lumaN + lumaS) - 2.0 * lumaM);
dir.y = ((lumaE + lumaW) - 2.0 * lumaM);
float dirReduce = max(lumaRange * 0.25, 1.0 / 128.0);
float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
dir = clamp(dir * rcpDirMin, -8.0, 8.0) * texelSize;
vec3 rgbA = 0.5 * (texture(tex, uv + dir * (1.0/3.0 - 0.5)).rgb +
texture(tex, uv + dir * (2.0/3.0 - 0.5)).rgb);
return rgbA;
}
```
## Choosing the Right Approach
| Method | Cost | Quality | Best For |
|--------|------|---------|----------|
| SSAA 2x2 | 4× render | Excellent | Final quality renders |
| SDF analytical | Minimal | Great for SDF | 2D shapes, UI elements |
| TAA | 1× + blend | Good + temporal | Animated scenes with multipass |
| FXAA | 1 pass post | Good | Any scene, post-process only |
→ For deeper details, see [reference/anti-aliasing.md](../reference/anti-aliasing.md)

View File

@@ -0,0 +1,522 @@
# Atmospheric & Subsurface Scattering
## Use Cases
- Sky rendering (sunrise/sunset/noon/night)
- Aerial perspective
- Sun halo (Mie scattering haze)
- Planetary atmosphere rim glow
- Translucent material SSS (candles, skin, jelly)
- Volumetric light (God rays)
## Core Principles
Three physical mechanisms:
**Rayleigh scattering** — molecular-scale particles, β_R(λ) ∝ 1/λ⁴, shorter wavelengths scatter more strongly (blue sky / red sunset).
Sea-level values: `vec3(5.5e-6, 13.0e-6, 22.4e-6)` m⁻¹.
Phase function: `P_R(θ) = 3/(16π) × (1 + cos²θ)`, symmetric forward-backward.
**Mie scattering** — aerosol particles, wavelength-independent, strong forward scattering (sun halo).
Sea-level value: `vec3(21e-6)` m⁻¹.
Phase function: Henyey-Greenstein, `g ≈ 0.76~0.88`.
**Beer-Lambert attenuation**`T(A→B) = exp(-∫ σ_e(s) ds)`, exponential decay of light through a medium.
**Algorithm flow**: ray march along the view ray; at each sample point: compute density → compute optical depth toward the sun → Beer-Lambert attenuation → phase function weighting → accumulate.
## Implementation Steps
### Step 1: Ray-Sphere Intersection
```glsl
// Returns (t_near, t_far); no intersection when t_near > t_far
vec2 raySphereIntersect(vec3 p, vec3 dir, float r) {
float b = dot(p, dir);
float c = dot(p, p) - r * r;
float d = b * b - c;
if (d < 0.0) return vec2(1e5, -1e5);
d = sqrt(d);
return vec2(-b - d, -b + d);
}
```
### Step 2: Atmospheric Physical Constants
```glsl
#define PLANET_RADIUS 6371e3
#define ATMOS_RADIUS 6471e3
#define PLANET_CENTER vec3(0.0)
#define BETA_RAY vec3(5.5e-6, 13.0e-6, 22.4e-6) // Rayleigh scattering coefficients
#define BETA_MIE vec3(21e-6) // Mie scattering coefficients
#define BETA_OZONE vec3(2.04e-5, 4.97e-5, 1.95e-6) // Ozone absorption
#define MIE_G 0.76 // Anisotropy parameter 0.76~0.88
#define MIE_EXTINCTION 1.1 // Extinction/scattering ratio
#define H_RAY 8000.0 // Rayleigh scale height
#define H_MIE 1200.0 // Mie scale height
#define H_OZONE 30e3 // Ozone peak altitude
#define OZONE_FALLOFF 4e3 // Ozone decay width
#define PRIMARY_STEPS 32 // Primary ray steps 8(mobile)~64(high quality)
#define LIGHT_STEPS 8 // Light direction steps 4~16
```
### Step 3: Phase Functions
```glsl
float phaseRayleigh(float cosTheta) {
return 3.0 / (16.0 * 3.14159265) * (1.0 + cosTheta * cosTheta);
}
// Henyey-Greenstein phase function
float phaseMie(float cosTheta, float g) {
float gg = g * g;
float num = (1.0 - gg) * (1.0 + cosTheta * cosTheta);
float denom = (2.0 + gg) * pow(1.0 + gg - 2.0 * g * cosTheta, 1.5);
return 3.0 / (8.0 * 3.14159265) * num / denom;
}
```
### Step 4: Atmospheric Density Sampling
```glsl
// Returns vec3(rayleigh, mie, ozone) density
vec3 atmosphereDensity(vec3 pos, float planetRadius) {
float height = length(pos) - planetRadius;
float densityRay = exp(-height / H_RAY);
float densityMie = exp(-height / H_MIE);
float denom = (H_OZONE - height) / OZONE_FALLOFF;
float densityOzone = (1.0 / (denom * denom + 1.0)) * densityRay;
return vec3(densityRay, densityMie, densityOzone);
}
```
### Step 5: Light Direction Optical Depth
```glsl
vec3 lightOpticalDepth(vec3 pos, vec3 sunDir) {
float atmoDist = raySphereIntersect(pos - PLANET_CENTER, sunDir, ATMOS_RADIUS).y;
float stepSize = atmoDist / float(LIGHT_STEPS);
float rayPos = stepSize * 0.5;
vec3 optDepth = vec3(0.0);
for (int i = 0; i < LIGHT_STEPS; i++) {
vec3 samplePos = pos + sunDir * rayPos;
float height = length(samplePos - PLANET_CENTER) - PLANET_RADIUS;
if (height < 0.0) return vec3(1e10); // Occluded by planet
optDepth += atmosphereDensity(samplePos, PLANET_RADIUS) * stepSize;
rayPos += stepSize;
}
return optDepth;
}
```
### Step 6: Primary Scattering Integration
```glsl
vec3 calculateScattering(
vec3 rayOrigin, vec3 rayDir, float maxDist,
vec3 sunDir, vec3 sunIntensity
) {
vec2 atmoHit = raySphereIntersect(rayOrigin - PLANET_CENTER, rayDir, ATMOS_RADIUS);
if (atmoHit.x > atmoHit.y) return vec3(0.0);
vec2 planetHit = raySphereIntersect(rayOrigin - PLANET_CENTER, rayDir, PLANET_RADIUS);
float tStart = max(atmoHit.x, 0.0);
float tEnd = atmoHit.y;
if (planetHit.x > 0.0) tEnd = min(tEnd, planetHit.x);
tEnd = min(tEnd, maxDist);
float stepSize = (tEnd - tStart) / float(PRIMARY_STEPS);
float cosTheta = dot(rayDir, sunDir);
float phaseR = phaseRayleigh(cosTheta);
float phaseM = phaseMie(cosTheta, MIE_G);
vec3 totalRay = vec3(0.0), totalMie = vec3(0.0), optDepthI = vec3(0.0);
float rayPos = tStart + stepSize * 0.5;
for (int i = 0; i < PRIMARY_STEPS; i++) {
vec3 samplePos = rayOrigin + rayDir * rayPos;
vec3 density = atmosphereDensity(samplePos, PLANET_RADIUS) * stepSize;
optDepthI += density;
vec3 optDepthL = lightOpticalDepth(samplePos, sunDir);
vec3 tau = BETA_RAY * (optDepthI.x + optDepthL.x)
+ BETA_MIE * 1.1 * (optDepthI.y + optDepthL.y)
+ BETA_OZONE * (optDepthI.z + optDepthL.z);
vec3 attenuation = exp(-tau);
totalRay += density.x * attenuation;
totalMie += density.y * attenuation;
rayPos += stepSize;
}
return sunIntensity * (totalRay * BETA_RAY * phaseR + totalMie * BETA_MIE * phaseM);
}
```
### Step 7: Tone Mapping
```glsl
vec3 tonemapExposure(vec3 color) { return 1.0 - exp(-color); }
vec3 tonemapReinhard(vec3 color) {
float l = dot(color, vec3(0.2126, 0.7152, 0.0722));
vec3 tc = color / (color + 1.0);
return mix(color / (l + 1.0), tc, tc);
}
vec3 gammaCorrect(vec3 color) { return pow(color, vec3(1.0 / 2.2)); }
```
## Complete Code Template
Fully runnable Rayleigh + Mie atmospheric scattering for ShaderToy:
```glsl
#define PI 3.14159265359
#define PLANET_RADIUS 6371e3
#define ATMOS_RADIUS 6471e3
#define PLANET_CENTER vec3(0.0)
#define BETA_RAY vec3(5.5e-6, 13.0e-6, 22.4e-6)
#define BETA_MIE vec3(21e-6)
#define BETA_OZONE vec3(2.04e-5, 4.97e-5, 1.95e-6)
#define MIE_G 0.76
#define MIE_EXTINCTION 1.1
#define H_RAY 8e3
#define H_MIE 1.2e3
#define H_OZONE 30e3
#define OZONE_FALLOFF 4e3
#define PRIMARY_STEPS 32
#define LIGHT_STEPS 8
#define SUN_INTENSITY vec3(40.0)
vec2 raySphereIntersect(vec3 p, vec3 dir, float r) {
float b = dot(p, dir);
float c = dot(p, p) - r * r;
float d = b * b - c;
if (d < 0.0) return vec2(1e5, -1e5);
d = sqrt(d);
return vec2(-b - d, -b + d);
}
float phaseRayleigh(float cosTheta) {
return 3.0 / (16.0 * PI) * (1.0 + cosTheta * cosTheta);
}
float phaseMie(float cosTheta, float g) {
float gg = g * g;
float num = (1.0 - gg) * (1.0 + cosTheta * cosTheta);
float denom = (2.0 + gg) * pow(1.0 + gg - 2.0 * g * cosTheta, 1.5);
return 3.0 / (8.0 * PI) * num / denom;
}
vec3 atmosphereDensity(vec3 pos) {
float height = length(pos - PLANET_CENTER) - PLANET_RADIUS;
float dRay = exp(-height / H_RAY);
float dMie = exp(-height / H_MIE);
float dOzone = (1.0 / (pow((H_OZONE - height) / OZONE_FALLOFF, 2.0) + 1.0)) * dRay;
return vec3(dRay, dMie, dOzone);
}
vec3 calculateScattering(
vec3 start, vec3 dir, float maxDist,
vec3 sceneColor, vec3 sunDir, vec3 sunIntensity
) {
start -= PLANET_CENTER;
float a = dot(dir, dir);
float b = 2.0 * dot(dir, start);
float c = dot(start, start) - ATMOS_RADIUS * ATMOS_RADIUS;
float d = b * b - 4.0 * a * c;
if (d < 0.0) return sceneColor;
vec2 rayLen = vec2(
max((-b - sqrt(d)) / (2.0 * a), 0.0),
min((-b + sqrt(d)) / (2.0 * a), maxDist)
);
if (rayLen.x > rayLen.y) return sceneColor;
bool allowMie = maxDist > rayLen.y;
rayLen.y = min(rayLen.y, maxDist);
rayLen.x = max(rayLen.x, 0.0);
float stepSize = (rayLen.y - rayLen.x) / float(PRIMARY_STEPS);
float rayPos = rayLen.x + stepSize * 0.5;
vec3 totalRay = vec3(0.0);
vec3 totalMie = vec3(0.0);
vec3 optI = vec3(0.0);
float mu = dot(dir, sunDir);
float phaseR = phaseRayleigh(mu);
float phaseM = allowMie ? phaseMie(mu, MIE_G) : 0.0;
for (int i = 0; i < PRIMARY_STEPS; i++) {
vec3 pos = start + dir * rayPos;
float height = length(pos) - PLANET_RADIUS;
vec3 density = vec3(exp(-height / H_RAY), exp(-height / H_MIE), 0.0);
float dOzone = (H_OZONE - height) / OZONE_FALLOFF;
density.z = (1.0 / (dOzone * dOzone + 1.0)) * density.x;
density *= stepSize;
optI += density;
float la = dot(sunDir, sunDir);
float lb = 2.0 * dot(sunDir, pos);
float lc = dot(pos, pos) - ATMOS_RADIUS * ATMOS_RADIUS;
float ld = lb * lb - 4.0 * la * lc;
float lightStepSize = (-lb + sqrt(ld)) / (2.0 * la * float(LIGHT_STEPS));
float lightPos = lightStepSize * 0.5;
vec3 optL = vec3(0.0);
for (int j = 0; j < LIGHT_STEPS; j++) {
vec3 posL = pos + sunDir * lightPos;
float heightL = length(posL) - PLANET_RADIUS;
vec3 densityL = vec3(exp(-heightL / H_RAY), exp(-heightL / H_MIE), 0.0);
float dOzoneL = (H_OZONE - heightL) / OZONE_FALLOFF;
densityL.z = (1.0 / (dOzoneL * dOzoneL + 1.0)) * densityL.x;
densityL *= lightStepSize;
optL += densityL;
lightPos += lightStepSize;
}
vec3 attn = exp(
-BETA_RAY * (optI.x + optL.x)
- BETA_MIE * MIE_EXTINCTION * (optI.y + optL.y)
- BETA_OZONE * (optI.z + optL.z)
);
totalRay += density.x * attn;
totalMie += density.y * attn;
rayPos += stepSize;
}
vec3 opacity = exp(-(BETA_MIE * optI.y + BETA_RAY * optI.x + BETA_OZONE * optI.z));
return (
phaseR * BETA_RAY * totalRay +
phaseM * BETA_MIE * totalMie
) * sunIntensity + sceneColor * opacity;
}
vec3 getCameraVector(vec3 resolution, vec2 coord) {
vec2 uv = coord.xy / resolution.xy - vec2(0.5);
uv.x *= resolution.x / resolution.y;
return normalize(vec3(uv.x, uv.y, -1.0));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec3 rayDir = getCameraVector(iResolution, fragCoord);
vec3 cameraPos = vec3(0.0, PLANET_RADIUS + 100.0, 0.0);
vec3 sunDir = normalize(vec3(0.0, cos(-iTime / 8.0), sin(-iTime / 8.0)));
vec4 scene = vec4(0.0, 0.0, 0.0, 1e12);
vec3 sunDisk = vec3(dot(rayDir, sunDir) > 0.9998 ? 3.0 : 0.0);
scene.xyz = sunDisk;
vec2 groundHit = raySphereIntersect(cameraPos - PLANET_CENTER, rayDir, PLANET_RADIUS);
if (groundHit.x > 0.0) {
scene.w = groundHit.x;
vec3 hitPos = cameraPos + rayDir * groundHit.x - PLANET_CENTER;
vec3 normal = normalize(hitPos);
float shadow = max(0.0, dot(normal, sunDir));
scene.xyz = vec3(0.1, 0.15, 0.08) * shadow;
}
vec3 col = calculateScattering(
cameraPos, rayDir, scene.w,
scene.xyz, sunDir, SUN_INTENSITY
);
col = 1.0 - exp(-col);
col = pow(col, vec3(1.0 / 2.2));
fragColor = vec4(col, 1.0);
}
```
## Advanced Fog Models
Three progressive fog techniques, from simple to physically motivated. These can be used standalone or combined with the full atmospheric scattering above.
### Level 1: Basic Exponential Fog
```glsl
vec3 applyFog(vec3 col, float t) {
float fogAmount = 1.0 - exp(-t * density);
vec3 fogColor = vec3(0.5, 0.6, 0.7);
return mix(col, fogColor, fogAmount);
}
```
### Level 2: Sun-Aware Fog (Scattering Tint)
Fog color shifts warm when looking toward the sun — creates a very natural light dispersion effect:
```glsl
vec3 applyFogSun(vec3 col, float t, vec3 rd, vec3 sunDir) {
float fogAmount = 1.0 - exp(-t * density);
float sunAmount = max(dot(rd, sunDir), 0.0);
vec3 fogColor = mix(
vec3(0.5, 0.6, 0.7), // base fog (blue-grey)
vec3(1.0, 0.9, 0.7), // sun-facing fog (warm gold)
pow(sunAmount, 8.0)
);
return mix(col, fogColor, fogAmount);
}
```
### Level 3: Height-Based Fog (Analytical Integration)
Density decreases exponentially with altitude: `d(y) = a * exp(-b * y)`. The formula is an exact analytical integral along the ray, not an approximation — fog pools in valleys and clears at altitude:
```glsl
vec3 applyFogHeight(vec3 col, float t, vec3 ro, vec3 rd) {
float a = 0.5; // density multiplier
float b = 0.3; // density falloff with height
float fogAmount = (a / b) * exp(-ro.y * b) * (1.0 - exp(-t * rd.y * b)) / rd.y;
fogAmount = clamp(fogAmount, 0.0, 1.0);
vec3 fogColor = vec3(0.5, 0.6, 0.7);
return mix(col, fogColor, fogAmount);
}
```
### Level 4: Extinction + Inscattering Separation
Independent RGB coefficients for absorption and scattering — allows chromatic fog effects where different wavelengths scatter differently:
```glsl
vec3 applyFogPhysical(vec3 col, float t, vec3 fogCol) {
vec3 be = vec3(0.02, 0.025, 0.03); // extinction coefficients (RGB)
vec3 bi = vec3(0.015, 0.02, 0.025); // inscattering coefficients (RGB)
vec3 extinction = exp(-t * be);
vec3 inscatter = (1.0 - exp(-t * bi));
return col * extinction + fogCol * inscatter;
}
```
## Common Variants
### Variant 1: Non-Physical Analytic Approximation (No Ray March)
Extremely low-cost analytic sky, suitable for mobile / backgrounds.
```glsl
#define zenithDensity(x) 0.7 / pow(max(x - 0.1, 0.0035), 0.75)
vec3 getSkyAbsorption(vec3 skyColor, float zenith) {
return exp2(skyColor * -zenith) * 2.0;
}
float getMie(vec2 p, vec2 lp) {
float disk = clamp(1.0 - pow(distance(p, lp), 0.1), 0.0, 1.0);
return disk * disk * (3.0 - 2.0 * disk) * 2.0 * 3.14159;
}
vec3 getAtmosphericScattering(vec2 screenPos, vec2 lightPos) {
vec3 skyColor = vec3(0.39, 0.57, 1.0);
float zenith = zenithDensity(screenPos.y);
float rayleighMult = 1.0 + pow(1.0 - clamp(distance(screenPos, lightPos), 0.0, 1.0), 2.0) * 1.57;
vec3 absorption = getSkyAbsorption(skyColor, zenith);
vec3 sunAbsorption = getSkyAbsorption(skyColor, zenithDensity(lightPos.y + 0.1));
vec3 sky = skyColor * zenith * rayleighMult;
vec3 mie = getMie(screenPos, lightPos) * sunAbsorption;
float sunDist = clamp(length(max(lightPos.y + 0.1, 0.0)), 0.0, 1.0);
vec3 totalSky = mix(sky * absorption, sky / (sky + 0.5), sunDist);
totalSky += mie;
totalSky *= sunAbsorption * 0.5 + 0.5 * length(sunAbsorption);
return totalSky;
}
```
### Variant 2: Ozone Absorption Layer
Already integrated in the complete template. Set `BETA_OZONE` to a non-zero value to enable, producing a deeper blue zenith and purple tones at sunset.
### Variant 3: Subsurface Scattering (SSS)
For translucent materials (candles/skin/jelly), using SDF-estimated thickness to control light transmission.
```glsl
float subsurface(vec3 p, vec3 viewDir, vec3 normal) {
vec3 scatterDir = refract(viewDir, normal, 1.0 / 1.5); // IOR 1.3~2.0
vec3 samplePos = p;
float accumThickness = 0.0;
float MAX_SCATTER = 2.5;
for (float i = 0.1; i < MAX_SCATTER; i += 0.2) {
samplePos += scatterDir * i;
accumThickness += map(samplePos); // SDF function
}
float thickness = max(0.0, -accumThickness);
float SCATTER_STRENGTH = 16.0;
return SCATTER_STRENGTH * pow(MAX_SCATTER * 0.5, 3.0) / thickness;
}
// Usage: float ss = max(0.0, subsurface(hitPos, viewDir, normal));
// vec3 sssColor = albedo * smoothstep(0.0, 2.0, pow(ss, 0.6));
// finalColor = mix(lambertian, sssColor, 0.7) + specular;
```
### Variant 4: LUT Precomputation Pipeline (Production Grade)
Precompute Transmittance/Multiple Scattering/Sky-View into LUTs, only table lookups at runtime.
```glsl
// Transmittance LUT query (Hillaire 2020)
vec3 getValFromTLUT(sampler2D tex, vec2 bufferRes, vec3 pos, vec3 sunDir) {
float height = length(pos);
vec3 up = pos / height;
float sunCosZenithAngle = dot(sunDir, up);
vec2 uv = vec2(
256.0 * clamp(0.5 + 0.5 * sunCosZenithAngle, 0.0, 1.0),
64.0 * max(0.0, min(1.0, (height - groundRadiusMM) / (atmosphereRadiusMM - groundRadiusMM)))
);
uv /= bufferRes;
return texture(tex, uv).rgb;
}
```
### Variant 5: Analytic Fast Atmosphere (with Aerial Perspective)
Analytic exponential approximation replacing ray march, with distance attenuation support.
```glsl
void getRayleighMie(float opticalDepth, float densityR, float densityM, out vec3 R, out vec3 M) {
vec3 C_RAYLEIGH = vec3(5.802, 13.558, 33.100) * 1e-6;
vec3 C_MIE = vec3(3.996e-6);
R = (1.0 - exp(-opticalDepth * densityR * C_RAYLEIGH / 2.5)) * 2.5;
M = (1.0 - exp(-opticalDepth * densityM * C_MIE / 0.5)) * 0.5;
}
vec3 getLightTransmittance(vec3 lightDir) {
vec3 C_RAYLEIGH = vec3(5.802, 13.558, 33.100) * 1e-6;
vec3 C_MIE = vec3(3.996e-6);
vec3 C_OZONE = vec3(0.650, 1.881, 0.085) * 1e-6;
float extinction = exp(-clamp(lightDir.y + 0.05, 0.0, 1.0) * 40.0)
+ exp(-clamp(lightDir.y + 0.5, 0.0, 1.0) * 5.0) * 0.4
+ pow(clamp(1.0 - lightDir.y, 0.0, 1.0), 2.0) * 0.02
+ 0.002;
return exp(-(C_RAYLEIGH + C_MIE + C_OZONE) * extinction * 1e6);
}
```
## Performance & Composition
### Performance Tips
- **Nested ray march (O(N*M))**: reduce step counts (mobile: PRIMARY=12, LIGHT=4), use analytic approximation instead of light march, precompute Transmittance LUT
- **Dense exp()/pow()**: Schlick approximation replacing HG phase function — `k = 1.55*g - 0.55*g³; phase = (1-k²) / (4π*(1+k*cosθ)²)`
- **Full-screen per-pixel**: Sky-View LUT (200x200) table lookup, half-resolution rendering + bilinear upsampling
- **Banding dithering**: non-uniform step offset of 0.3, temporal blue noise dithering
### Composition Tips
- **+ Volumetric clouds**: atmospheric transmittance determines sun color reaching the cloud layer, set `maxDist` to cloud distance
- **+ SDF scene**: SDF hit distance → `maxDist`, scene color → `sceneColor`, automatic aerial perspective
- **+ God Rays**: add occlusion to scattering integration (shadow map or additional ray march)
- **+ Terrain**: `finalColor = terrainColor * transmittance + inscattering`
- **+ PBR/SSS**: `diffuse = mix(lambert, sss, 0.7); final = ambient + albedo*diffuse + specular + fresnel*env`
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/atmospheric-scattering.md)

View File

@@ -0,0 +1,115 @@
# Camera & Lens Effects
## Use Cases
- Adding cinematic depth of field (bokeh blur)
- Motion blur for dynamic scenes
- Lens distortion and chromatic aberration
- Film grain and photographic realism
## Techniques
### 1. Depth of Field (Thin Lens Model)
Simulate camera aperture by jittering ray origins on a virtual lens disk:
```glsl
// For each sample:
vec2 lens = randomDisk(seed) * apertureSize; // random point on aperture
vec3 focalPoint = ro + rd * focalDistance; // point on focal plane
vec3 newRo = ro + cameraRight * lens.x + cameraUp * lens.y; // offset origin
vec3 newRd = normalize(focalPoint - newRo); // new ray toward focal point
// Accumulate multiple samples (16-64) for smooth bokeh
// Use with AA loop or temporal accumulation
// Disk sampling helper:
vec2 randomDisk(float seed) {
float angle = hash11(seed) * 6.2831853;
float radius = sqrt(hash11(seed + 1.0));
return vec2(cos(angle), sin(angle)) * radius;
}
```
Parameters:
- `apertureSize`: 0.0 = pinhole (sharp), 0.1-0.5 = visible bokeh
- `focalDistance`: distance to the in-focus plane
### 2. Post-Process Depth of Field (Single Pass)
Cheaper approximation using depth buffer blur:
```glsl
vec3 dofPostProcess(sampler2D colorTex, sampler2D depthTex, vec2 uv) {
float depth = texture(depthTex, uv).r;
float coc = abs(depth - focalDepth) * apertureSize; // circle of confusion
coc = clamp(coc, 0.0, maxBlur);
vec3 color = vec3(0.0);
float total = 0.0;
// 16-tap Poisson disk sampling
for (int i = 0; i < 16; i++) {
vec2 offset = poissonDisk[i] * coc / iResolution.xy;
color += texture(colorTex, uv + offset).rgb;
total += 1.0;
}
return color / total;
}
```
### 3. Motion Blur (Velocity-Based)
```glsl
// Simple radial motion blur (camera rotation)
vec3 motionBlur(vec2 uv, float amount) {
vec3 color = vec3(0.0);
vec2 center = vec2(0.5);
int samples = 8;
for (int i = 0; i < samples; i++) {
float t = float(i) / float(samples - 1) - 0.5;
vec2 sampleUV = mix(uv, center, t * amount);
color += texture(iChannel0, sampleUV).rgb;
}
return color / float(samples);
}
// Time-based motion blur for ray marching
// Sample multiple time offsets within the frame:
// float t_shutter = iTime + (hash11(seed) - 0.5) * shutterSpeed;
// Use t_shutter instead of iTime for scene animation
```
### 4. Lens Distortion
```glsl
// Barrel/pincushion distortion
vec2 lensDistortion(vec2 uv, float k1, float k2) {
vec2 centered = uv - 0.5;
float r2 = dot(centered, centered);
float distortion = 1.0 + k1 * r2 + k2 * r2 * r2;
return centered * distortion + 0.5;
// k1 > 0: pincushion, k1 < 0: barrel
}
```
### 5. Film Grain
```glsl
vec3 filmGrain(vec3 color, vec2 uv, float time, float intensity) {
float grain = hash12(uv * iResolution.xy + fract(time) * 1000.0) - 0.5;
// Apply more grain in darker areas (realistic film response)
float luminance = dot(color, vec3(0.299, 0.587, 0.114));
float grainAmount = intensity * (1.0 - luminance * 0.5);
return color + grain * grainAmount;
}
```
### 6. Vignette
```glsl
vec3 vignette(vec3 color, vec2 uv, float intensity, float smoothness) {
vec2 centered = uv - 0.5;
float dist = length(centered);
float vig = smoothstep(0.5, 0.5 - smoothness, dist);
return color * mix(1.0 - intensity, 1.0, vig);
}
```
→ For deeper details, see [reference/camera-effects.md](../reference/camera-effects.md)

View File

@@ -0,0 +1,531 @@
# Cellular Automata & Reaction-Diffusion
## Use Cases
- GPU grid evolution simulation (cellular automata, reaction-diffusion)
- Organic texture generation: spots, stripes, mazes, coral, vein patterns
- Conway's Game of Life and variants (custom B/S rule sets)
- Gray-Scott reaction-diffusion real-time visualization
- Using simulation results to drive 3D surface displacement, lighting, or coloring
## Core Principles
### Cellular Automata (CA)
Each cell on a discrete grid updates based on **its own state** and **neighbor states** according to fixed rules. Conway B3/S23 rules:
- Dead cell with exactly 3 live neighbors → birth
- Live cell with 2 or 3 live neighbors → survival
- Otherwise → death
Neighbor computation (Moore neighborhood, 8 neighbors): `k = Σ cell(px + offset)`
### Reaction-Diffusion (RD)
Gray-Scott model — two substances u (activator) and v (inhibitor) diffuse and react:
```
∂u/∂t = Du·∇²u - u·v² + F·(1-u)
∂v/∂t = Dv·∇²v + u·v² - (F+k)·v
```
- `Du, Dv`: diffusion coefficients (Du > Dv produces patterns)
- `F`: feed rate, `k`: kill rate
- `∇²`: Laplacian, discretized using a nine-point stencil
Key parameters `(F, k)` determine the pattern:
| F | k | Pattern |
|---|---|---------|
| 0.035 | 0.065 | spots |
| 0.040 | 0.060 | stripes |
| 0.025 | 0.055 | labyrinthine |
| 0.050 | 0.065 | solitons |
## Implementation Steps
### Step 1: Grid State Storage & Self-Feedback
```glsl
// Buffer A: iChannel0 bound to Buffer A itself (self-feedback)
vec4 prevState = texelFetch(iChannel0, ivec2(fragCoord), 0);
// UV sampling (supports texture filtering)
vec2 uv = fragCoord / iResolution.xy;
vec4 prevSmooth = texture(iChannel0, uv);
```
### Step 2: Initialization (Noise Seeding)
```glsl
float hash1(float n) {
return fract(sin(n) * 138.5453123);
}
vec3 hash33(in vec2 p) {
float n = sin(dot(p, vec2(41, 289)));
return fract(vec3(2097152, 262144, 32768) * n);
}
if (iFrame < 2) {
// CA: random binary
float f = step(0.9, hash1(fragCoord.x * 13.0 + hash1(fragCoord.y * 71.1)));
fragColor = vec4(f, 0.0, 0.0, 0.0);
} else if (iFrame < 10) {
// RD: random continuous values
vec3 noise = hash33(fragCoord / iResolution.xy + vec2(53, 43) * float(iFrame));
fragColor = vec4(noise, 1.0);
}
```
### Step 3: Neighbor Sampling & Laplacian
```glsl
// --- Method A: Discrete CA neighbor counting ---
int cell(in ivec2 p) {
ivec2 r = ivec2(textureSize(iChannel0, 0));
p = (p + r) % r; // wrap-around boundary
return (texelFetch(iChannel0, p, 0).x > 0.5) ? 1 : 0;
}
ivec2 px = ivec2(fragCoord);
int k = cell(px+ivec2(-1,-1)) + cell(px+ivec2(0,-1)) + cell(px+ivec2(1,-1))
+ cell(px+ivec2(-1, 0)) + cell(px+ivec2(1, 0))
+ cell(px+ivec2(-1, 1)) + cell(px+ivec2(0, 1)) + cell(px+ivec2(1, 1));
// --- Method B: Nine-point Laplacian (for RD) ---
// Weights: diagonal 0.5, cross 1.0, center -6.0
vec2 laplacian(vec2 uv) {
vec2 px = 1.0 / iResolution.xy;
vec4 P = vec4(px, 0.0, -px.x);
return
0.5 * texture(iChannel0, uv - P.xy).xy
+ texture(iChannel0, uv - P.zy).xy
+ 0.5 * texture(iChannel0, uv - P.wy).xy
+ texture(iChannel0, uv - P.xz).xy
- 6.0 * texture(iChannel0, uv).xy
+ texture(iChannel0, uv + P.xz).xy
+ 0.5 * texture(iChannel0, uv + P.wy).xy
+ texture(iChannel0, uv + P.zy).xy
+ 0.5 * texture(iChannel0, uv + P.xy).xy;
}
// --- Method C: 3x3 weighted blur (Gaussian approximation) ---
// Weights: corner 1, edge 2, center 4, total 16
float blur3x3(vec2 uv) {
vec3 e = vec3(1, 0, -1);
vec2 px = 1.0 / iResolution.xy;
float res = 0.0;
res += texture(iChannel0, uv + e.xx*px).x + texture(iChannel0, uv + e.xz*px).x
+ texture(iChannel0, uv + e.zx*px).x + texture(iChannel0, uv + e.zz*px).x;
res += (texture(iChannel0, uv + e.xy*px).x + texture(iChannel0, uv + e.yx*px).x
+ texture(iChannel0, uv + e.yz*px).x + texture(iChannel0, uv + e.zy*px).x) * 2.;
res += texture(iChannel0, uv + e.yy*px).x * 4.;
return res / 16.0;
}
```
### Step 4: State Update Rules
```glsl
// --- CA: Conway B3/S23 ---
int e = cell(px);
float f = (((k == 2) && (e == 1)) || (k == 3)) ? 1.0 : 0.0;
// --- CA: Generic Birth/Survival bitmask ---
float ff = 0.0;
if (currentAlive) {
ff = ((stayset & (1 << (k - 1))) > 0) ? float(k) : 0.0;
} else {
ff = ((bornset & (1 << (k - 1))) > 0) ? 1.0 : 0.0;
}
// --- RD: Gray-Scott update ---
float u = prevState.x;
float v = prevState.y;
vec2 Duv = laplacian(uv) * DIFFUSION;
float du = Duv.x - u * v * v + F * (1.0 - u);
float dv = Duv.y + u * v * v - (F + k) * v;
fragColor.xy = clamp(vec2(u + du * DT, v + dv * DT), 0.0, 1.0);
// --- RD: Simplified version (gradient + random decay) ---
float avgRD = blur3x3(uv);
vec2 pwr = (1.0 / iResolution.xy) * 1.5;
vec2 lap = vec2(
texture(iChannel0, uv + vec2(pwr.x, 0)).y - texture(iChannel0, uv - vec2(pwr.x, 0)).y,
texture(iChannel0, uv + vec2(0, pwr.y)).y - texture(iChannel0, uv - vec2(0, pwr.y)).y
);
uv = uv + lap * (1.0 / iResolution.xy) * 3.0;
float newRD = texture(iChannel0, uv).x + (noise.z - 0.5) * 0.0025 - 0.002;
newRD += dot(texture(iChannel0, uv + (noise.xy - 0.5) / iResolution.xy).xy, vec2(1, -1)) * 0.145;
```
### Step 5: Visualization & Coloring
```glsl
// Color mapping
float c = 1.0 - texture(iChannel0, uv).y;
vec3 col = pow(vec3(1.5, 1, 1) * c, vec3(1, 4, 12));
// Gradient normals + bump lighting
vec3 normal(vec2 uv) {
vec3 delta = vec3(1.0 / iResolution.xy, 0.0);
float du = texture(iChannel0, uv + delta.xz).x - texture(iChannel0, uv - delta.xz).x;
float dv = texture(iChannel0, uv + delta.zy).x - texture(iChannel0, uv - delta.zy).x;
return normalize(vec3(du, dv, 1.0));
}
// Specular highlight
float c2 = 1.0 - texture(iChannel0, uv + 0.5 / iResolution.xy).y;
col += vec3(0.36, 0.73, 1.0) * max(c2 * c2 - c * c, 0.0) * 12.0;
// Vignette + gamma
col *= pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.125) * 1.15;
col *= smoothstep(0.0, 1.0, iTime / 2.0);
fragColor = vec4(sqrt(min(col, 1.0)), 1.0);
```
## Complete Code Template
ShaderToy setup: Buffer A's iChannel0 = Buffer A (self-feedback, linear filtering). Image's iChannel0 = Buffer A.
### Standalone HTML JS Skeleton (Ping-Pong Render Pipeline)
CA/RD requires framebuffer self-feedback. The following JS skeleton demonstrates the correct WebGL2 multi-pass ping-pong structure:
```javascript
<script>
let frameCount = 0;
let mouse = [0, 0, 0, 0];
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2');
const ext = gl.getExtension('EXT_color_buffer_float');
function createShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error(gl.getShaderInfoLog(s));
return s;
}
function createProgram(vsSrc, fsSrc) {
const p = gl.createProgram();
gl.attachShader(p, createShader(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, createShader(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(p);
return p;
}
const vsSource = `#version 300 es
in vec2 pos;
void main(){ gl_Position=vec4(pos,0,1); }`;
// fsBuffer / fsImage: adapt from the Buffer A / Image templates below (uniform declarations + void main entry point)
const progBuf = createProgram(vsSource, fsBuffer);
const progImg = createProgram(vsSource, fsImage);
function createFBO(w, h) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
const fmt = ext ? gl.RGBA16F : gl.RGBA;
const typ = ext ? gl.FLOAT : gl.UNSIGNED_BYTE;
gl.texImage2D(gl.TEXTURE_2D, 0, fmt, w, h, 0, gl.RGBA, typ, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { fbo, tex };
}
let W, H, bufA, bufB;
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function resize() {
canvas.width = W = innerWidth;
canvas.height = H = innerHeight;
bufA = createFBO(W, H);
bufB = createFBO(W, H);
frameCount = 0;
}
addEventListener('resize', resize);
resize();
canvas.addEventListener('mousedown', e => { mouse[2] = e.clientX; mouse[3] = H - e.clientY; });
canvas.addEventListener('mouseup', () => { mouse[2] = 0; mouse[3] = 0; });
canvas.addEventListener('mousemove', e => { mouse[0] = e.clientX; mouse[1] = H - e.clientY; });
function render(t) {
t *= 0.001;
// Buffer pass: read bufA → write bufB
gl.useProgram(progBuf);
gl.bindFramebuffer(gl.FRAMEBUFFER, bufB.fbo);
gl.viewport(0, 0, W, H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, bufA.tex);
gl.uniform1i(gl.getUniformLocation(progBuf, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(progBuf, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progBuf, 'iTime'), t);
gl.uniform1i(gl.getUniformLocation(progBuf, 'iFrame'), frameCount);
gl.uniform4f(gl.getUniformLocation(progBuf, 'iMouse'), ...mouse);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
[bufA, bufB] = [bufB, bufA];
// Image pass: read bufA → screen
gl.useProgram(progImg);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, W, H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, bufA.tex);
gl.uniform1i(gl.getUniformLocation(progImg, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(progImg, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progImg, 'iTime'), t);
gl.uniform1i(gl.getUniformLocation(progImg, 'iFrame'), frameCount);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
frameCount++;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
```
### Buffer A (Simulation Computation)
```glsl
// Gray-Scott Reaction-Diffusion — Buffer A (Simulation)
// iChannel0 = Buffer A (self-feedback, linear filtering)
#define DU 0.210 // u diffusion coefficient (0.1~0.3)
#define DV 0.105 // v diffusion coefficient (0.05~0.15)
#define F 0.040 // feed rate (0.01~0.08)
#define K 0.060 // kill rate (0.04~0.07)
#define DT 1.0 // time step (0.5~2.0)
#define INIT_FRAMES 10
float hash1(float n) {
return fract(sin(n) * 138.5453123);
}
vec3 hash33(vec2 p) {
float n = sin(dot(p, vec2(41.0, 289.0)));
return fract(vec3(2097152.0, 262144.0, 32768.0) * n);
}
// Nine-point Laplacian: diagonal 0.05, cross 0.2, center -1.0
vec2 laplacian9(vec2 uv) {
vec2 px = 1.0 / iResolution.xy;
vec2 c = texture(iChannel0, uv).xy;
vec2 n = texture(iChannel0, uv + vec2( 0, px.y)).xy;
vec2 s = texture(iChannel0, uv + vec2( 0,-px.y)).xy;
vec2 e = texture(iChannel0, uv + vec2( px.x, 0)).xy;
vec2 w = texture(iChannel0, uv + vec2(-px.x, 0)).xy;
vec2 ne = texture(iChannel0, uv + vec2( px.x, px.y)).xy;
vec2 nw = texture(iChannel0, uv + vec2(-px.x, px.y)).xy;
vec2 se = texture(iChannel0, uv + vec2( px.x,-px.y)).xy;
vec2 sw = texture(iChannel0, uv + vec2(-px.x,-px.y)).xy;
return (n + s + e + w) * 0.2 + (ne + nw + se + sw) * 0.05 - c;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
// Initialization
if (iFrame < INIT_FRAMES) {
float rnd = hash1(fragCoord.x * 13.0 + hash1(fragCoord.y * 71.1 + float(iFrame)));
float u = 1.0;
float v = (rnd > 0.9) ? 1.0 : 0.0;
vec2 center = iResolution.xy * 0.5;
if (abs(fragCoord.x - center.x) < 20.0 && abs(fragCoord.y - center.y) < 20.0) {
v = hash1(fragCoord.x * 7.0 + fragCoord.y * 13.0) > 0.5 ? 1.0 : 0.0;
}
fragColor = vec4(u, v, 0.0, 1.0);
return;
}
// Read current state
vec2 state = texture(iChannel0, uv).xy;
float u = state.x;
float v = state.y;
// Gray-Scott equations
vec2 lap = laplacian9(uv);
float uvv = u * v * v;
float du = DU * lap.x - uvv + F * (1.0 - u);
float dv = DV * lap.y + uvv - (F + K) * v;
u += du * DT;
v += dv * DT;
// Mouse interaction: click to add v
if (iMouse.z > 0.0) {
if (length(fragCoord - iMouse.xy) < 10.0) v = 1.0;
}
fragColor = vec4(clamp(u, 0.0, 1.0), clamp(v, 0.0, 1.0), 0.0, 1.0);
}
```
### Image (Visualization Output)
```glsl
// Gray-Scott Reaction-Diffusion — Image (Visualization)
// iChannel0 = Buffer A (linear filtering)
#define LIGHT_STRENGTH 12.0 // specular intensity (5~20)
#define COLOR_MODE 0 // 0=blue-gold, 1=flame, 2=monochrome
#define VIGNETTE 1 // 0=off, 1=vignette on
vec3 getNormal(vec2 uv) {
vec2 d = 1.0 / iResolution.xy;
float du = texture(iChannel0, uv + vec2(d.x, 0)).y - texture(iChannel0, uv - vec2(d.x, 0)).y;
float dv = texture(iChannel0, uv + vec2(0, d.y)).y - texture(iChannel0, uv - vec2(0, d.y)).y;
return normalize(vec3(du, dv, 0.05));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float val = texture(iChannel0, uv).y;
float c = 1.0 - val;
vec3 col;
#if COLOR_MODE == 0
float pattern = -cos(uv.x*0.75*3.14159-0.9)*cos(uv.y*1.5*3.14159-0.75)*0.5+0.5;
col = pow(vec3(1.5, 1.0, 1.0) * c, vec3(1.0, 4.0, 12.0));
col = mix(col, col.zyx, clamp(pattern - 0.2, 0.0, 1.0));
#elif COLOR_MODE == 1
col = vec3(c * 1.2, pow(c, 3.0), pow(c, 9.0));
#else
col = vec3(c);
#endif
float c2 = 1.0 - texture(iChannel0, uv + 0.5 / iResolution.xy).y;
col += vec3(0.36, 0.73, 1.0) * max(c2*c2 - c*c, 0.0) * LIGHT_STRENGTH;
#if VIGNETTE == 1
col *= pow(16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y), 0.125) * 1.15;
#endif
col *= smoothstep(0.0, 1.0, iTime / 2.0);
fragColor = vec4(sqrt(clamp(col, 0.0, 1.0)), 1.0);
}
```
## Common Variants
### Variant 1: Conway's Game of Life (Discrete CA)
```glsl
int cell(in ivec2 p) {
ivec2 r = ivec2(textureSize(iChannel0, 0));
p = (p + r) % r;
return (texelFetch(iChannel0, p, 0).x > 0.5) ? 1 : 0;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
ivec2 px = ivec2(fragCoord);
int k = cell(px+ivec2(-1,-1)) + cell(px+ivec2(0,-1)) + cell(px+ivec2(1,-1))
+ cell(px+ivec2(-1, 0)) + cell(px+ivec2(1, 0))
+ cell(px+ivec2(-1, 1)) + cell(px+ivec2(0, 1)) + cell(px+ivec2(1, 1));
int e = cell(px);
float f = (((k == 2) && (e == 1)) || (k == 3)) ? 1.0 : 0.0;
if (iFrame < 2)
f = step(0.9, fract(sin(fragCoord.x*13.0 + sin(fragCoord.y*71.1)) * 138.5));
fragColor = vec4(f, 0.0, 0.0, 1.0);
}
```
### Variant 2: Configurable Rule Set CA (B/S Bitmask)
```glsl
#define BORN_SET 8 // birth bitmask, 8 = B3
#define STAY_SET 12 // survival bitmask, 12 = S23
#define LIVEVAL 2.0
#define DECIMATE 1.0 // decay value
float ff = 0.0;
float ev = texelFetch(iChannel0, px, 0).w;
if (ev > 0.5) {
if (DECIMATE > 0.0) ff = ev - DECIMATE;
if ((STAY_SET & (1 << (k - 1))) > 0) ff = LIVEVAL;
} else {
ff = ((BORN_SET & (1 << (k - 1))) > 0) ? LIVEVAL : 0.0;
}
```
### Variant 3: Separable Gaussian Blur RD (Multi-Buffer)
```glsl
// Buffer B: horizontal blur (reads Buffer A)
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float h = 1.0 / iResolution.x;
vec4 sum = vec4(0.0);
sum += texture(iChannel0, fract(vec2(uv.x - 4.0*h, uv.y))) * 0.05;
sum += texture(iChannel0, fract(vec2(uv.x - 3.0*h, uv.y))) * 0.09;
sum += texture(iChannel0, fract(vec2(uv.x - 2.0*h, uv.y))) * 0.12;
sum += texture(iChannel0, fract(vec2(uv.x - 1.0*h, uv.y))) * 0.15;
sum += texture(iChannel0, fract(vec2(uv.x, uv.y))) * 0.16;
sum += texture(iChannel0, fract(vec2(uv.x + 1.0*h, uv.y))) * 0.15;
sum += texture(iChannel0, fract(vec2(uv.x + 2.0*h, uv.y))) * 0.12;
sum += texture(iChannel0, fract(vec2(uv.x + 3.0*h, uv.y))) * 0.09;
sum += texture(iChannel0, fract(vec2(uv.x + 4.0*h, uv.y))) * 0.05;
fragColor = vec4(sum.xyz / 0.98, 1.0);
}
// Buffer C: vertical blur (reads Buffer B), same structure but along y-axis
// Buffer A: reaction step reads Buffer C as the diffusion term
```
### Variant 4: Continuous Differential Operator CA (Vein/Fluid Style)
```glsl
#define STEPS 40 // advection step count (10~60)
#define ts 0.2 // advection rotation strength
#define cs -2.0 // curl scale
#define ls 0.05 // Laplacian scale
#define amp 1.0 // self-amplification coefficient
#define upd 0.4 // update smoothing coefficient
// 3x3 discrete curl and divergence
curl = uv_n.x - uv_s.x - uv_e.y + uv_w.y
+ _D * (uv_nw.x + uv_nw.y + uv_ne.x - uv_ne.y
+ uv_sw.y - uv_sw.x - uv_se.y - uv_se.x);
div = uv_s.y - uv_n.y - uv_e.x + uv_w.x
+ _D * (uv_nw.x - uv_nw.y - uv_ne.x - uv_ne.y
+ uv_sw.x + uv_sw.y + uv_se.y - uv_se.x);
// Multi-step advection loop
for (int i = 0; i < STEPS; i++) {
advect(off, vUv, texel, curl, div, lapl, blur);
offd = rot(offd, ts * curl);
off += offd;
ab += blur / float(STEPS);
}
```
### Variant 5: RD-Driven 3D Surface (Raymarched RD)
```glsl
// Image pass: use RD texture for displacement in SDF
vec2 map(in vec3 pos) {
vec3 p = normalize(pos);
vec2 uv;
uv.x = 0.5 + atan(p.z, p.x) / (2.0 * 3.14159);
uv.y = 0.5 - asin(p.y) / 3.14159;
float y = texture(iChannel0, uv).y;
float displacement = 0.1 * y;
float sd = length(pos) - (2.0 + displacement);
return vec2(sd, y);
}
```
## Performance & Composition
### Performance Tips
- **texelFetch vs texture**: Use `texelFetch` for discrete CA (exact pixel reads), `texture` for continuous RD (bilinear interpolation)
- **Separable blur replaces large kernels**: For large diffusion radii, use two-pass separable Gaussian (O(2N)) instead of NxN Laplacian (O(N²))
- **Sub-iterations**: Multiple small DT steps within a single frame improves stability
- **Reduced resolution**: Low-resolution buffer simulation + Image pass upsampling
- **Avoid branching**: Use `step()/mix()/clamp()` instead of `if/else`
### Composition Directions
- **RD + Raymarching**: RD as heightmap mapped onto 3D surface for displacement modeling
- **CA/RD + Particle Systems**: Field used as velocity field or spawn probability field to drive particles
- **RD + Bump Lighting**: Compute normals from RD values, combine with environment maps for metallic etching/ripple effects
- **CA + Color Decay Trails**: After death, fade per-frame with different RGB decay rates producing colored trails
- **RD + Domain Transforms**: Apply vortex/spiral transforms before sampling, producing spiral swirl patterns
## Further Reading
Full step-by-step tutorial, mathematical derivations, and advanced usage in [reference](../reference/cellular-automata.md)

View File

@@ -0,0 +1,380 @@
# Color Palette & Color Space
## Use Cases
- Mapping scalar values (distance, temperature, time, iteration count) to continuous color ramps
- Perceptually uniform color interpolation/gradients
- HDR rendering with linear-space workflow (sRGB decode -> shading -> tone mapping -> sRGB encode)
- Physically realistic glow/flame/blackbody radiation colors
## Core Principles
Core: **map a scalar t in [0,1] to an RGB vec3**.
### Cosine Palette
```
color(t) = a + b * cos(2pi * (c * t + d))
```
- **a** = brightness offset (~0.5), **b** = amplitude (~0.5), **c** = frequency, **d** = phase (the key parameter controlling color style)
### HSV/HSL Branchless Conversion
```
rgb = clamp(abs(mod(H*6 + vec3(0,4,2), 6) - 3) - 1, 0, 1)
```
Uses piecewise linear functions to approximate RGB variation with hue. C1 continuity can be achieved via `rgb*rgb*(3-2*rgb)`.
### CIE Lab/Lch Perceptually Uniform Interpolation
RGB -> XYZ -> Lab -> Lch pipeline; interpolate in perceptually uniform space to avoid brightness discontinuities in RGB/HSV.
### Blackbody Radiation Palette
Temperature T -> Planckian locus approximation -> CIE chromaticity -> XYZ -> RGB, with Stefan-Boltzmann (T^4) controlling brightness.
## Implementation
### Cosine Palette
```glsl
// a: offset, b: amplitude, c: frequency, d: phase, t: input scalar
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(6.28318 * (c * t + d));
}
```
### Classic Preset Parameters
```glsl
// Rainbow: d=(0.0, 0.33, 0.67)
// Warm: d=(0.0, 0.10, 0.20)
// Blue-purple to orange: c=(1,0.7,0.4) d=(0.0, 0.15, 0.20)
// Warm-cool mix: a=(.8,.5,.4) b=(.2,.4,.2) c=(2,1,1) d=(0.0, 0.25, 0.25)
// Simplified version: fixed a/b/c, only adjust d
vec3 palette(float t) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.263, 0.416, 0.557);
return a + b * cos(6.28318 * (c * t + d));
}
```
### HSV -> RGB (Standard + Smooth)
```glsl
// Standard HSV -> RGB (branchless)
vec3 hsv2rgb(vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return c.z * mix(vec3(1.0), rgb, c.y);
}
// Smooth version (C1 continuous)
vec3 hsv2rgb_smooth(vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb); // Hermite smoothing
return c.z * mix(vec3(1.0), rgb, c.y);
}
```
### HSL -> RGB
```glsl
vec3 hue2rgb(float h) {
return clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
}
vec3 hsl2rgb(float h, float s, float l) {
vec3 rgb = hue2rgb(h);
return l + s * (rgb - 0.5) * (1.0 - abs(2.0 * l - 1.0));
}
```
### RGB -> HSV
```glsl
// Sam Hocevar branchless method
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
```
### CIE Lab/Lch Conversion Pipeline
```glsl
float xyzF(float t) { return mix(pow(t, 1.0/3.0), 7.787037 * t + 0.139731, step(t, 0.00885645)); }
float xyzR(float t) { return mix(t * t * t, 0.1284185 * (t - 0.139731), step(t, 0.20689655)); }
vec3 rgb2lch(vec3 c) {
c *= mat3(0.4124, 0.3576, 0.1805,
0.2126, 0.7152, 0.0722,
0.0193, 0.1192, 0.9505);
c = vec3(xyzF(c.x), xyzF(c.y), xyzF(c.z));
vec3 lab = vec3(max(0.0, 116.0 * c.y - 16.0),
500.0 * (c.x - c.y),
200.0 * (c.y - c.z));
return vec3(lab.x, length(lab.yz), atan(lab.z, lab.y));
}
vec3 lch2rgb(vec3 c) {
c = vec3(c.x, cos(c.z) * c.y, sin(c.z) * c.y);
float lg = (1.0 / 116.0) * (c.x + 16.0);
vec3 xyz = vec3(xyzR(lg + 0.002 * c.y),
xyzR(lg),
xyzR(lg - 0.005 * c.z));
return xyz * mat3( 3.2406, -1.5372, -0.4986,
-0.9689, 1.8758, 0.0415,
0.0557, -0.2040, 1.0570);
}
// Circular hue interpolation
float lerpAngle(float a, float b, float x) {
float ang = mod(mod((a - b), 6.28318) + 9.42477, 6.28318) - 3.14159;
return ang * x + b;
}
vec3 lerpLch(vec3 a, vec3 b, float x) {
return vec3(mix(b.xy, a.xy, x), lerpAngle(a.z, b.z, x));
}
```
### sRGB Gamma & Tone Mapping
```glsl
// Precise sRGB encoding
float sRGB_encode(float t) {
return mix(1.055 * pow(t, 1.0/2.4) - 0.055, 12.92 * t, step(t, 0.0031308));
}
vec3 sRGB_encode(vec3 c) {
return vec3(sRGB_encode(c.x), sRGB_encode(c.y), sRGB_encode(c.z));
}
// Fast approximation: pow(color, vec3(2.2)) / pow(color, vec3(1.0/2.2))
// Reinhard tone mapping
vec3 tonemap_reinhard(vec3 col) {
return col / (1.0 + col);
}
```
### Blackbody Radiation Palette
```glsl
#define TEMP_MAX 4000.0 // Tunable: maximum temperature (K)
vec3 blackbodyPalette(float t) {
t *= TEMP_MAX;
float cx = (0.860117757 + 1.54118254e-4 * t + 1.28641212e-7 * t * t)
/ (1.0 + 8.42420235e-4 * t + 7.08145163e-7 * t * t);
float cy = (0.317398726 + 4.22806245e-5 * t + 4.20481691e-8 * t * t)
/ (1.0 - 2.89741816e-5 * t + 1.61456053e-7 * t * t);
float d = 2.0 * cx - 8.0 * cy + 4.0;
vec3 XYZ = vec3(3.0 * cx / d, 2.0 * cy / d, 1.0 - (3.0 * cx + 2.0 * cy) / d);
vec3 RGB = mat3(3.240479, -0.969256, 0.055648,
-1.537150, 1.875992, -0.204043,
-0.498535, 0.041556, 1.057311) * vec3(XYZ.x / XYZ.y, 1.0, XYZ.z / XYZ.y);
return max(RGB, 0.0) * pow(t * 0.0004, 4.0);
}
```
## Complete Code Template
A ShaderToy shader demonstrating all core techniques:
```glsl
// === Procedural Color Palette Showcase ===
#define PI 3.14159265
#define TAU 6.28318530
// ============ Tunable Parameters ============
#define PALETTE_A vec3(0.5, 0.5, 0.5) // Offset: increase = brighter overall
#define PALETTE_B vec3(0.5, 0.5, 0.5) // Amplitude: increase = more contrast
#define PALETTE_C vec3(1.0, 1.0, 1.0) // Frequency: increase = denser color variation
#define PALETTE_D vec3(0.0, 0.33, 0.67) // Phase: change = completely different hues
#define TEMP_MAX 4000.0 // Blackbody max temperature (K)
#define NUM_ITER 4 // Fractal iteration count
// ============ Color Functions ============
vec3 cosinePalette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b * cos(TAU * (c * t + d));
}
vec3 palette(float t) {
return cosinePalette(t, PALETTE_A, PALETTE_B, PALETTE_C, PALETTE_D);
}
vec3 hsv2rgb(vec3 c) {
vec3 rgb = clamp(abs(mod(c.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
return c.z * mix(vec3(1.0), rgb, c.y);
}
vec3 hsl2rgb(float h, float s, float l) {
vec3 rgb = clamp(abs(mod(h * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
return l + s * (rgb - 0.5) * (1.0 - abs(2.0 * l - 1.0));
}
vec3 blackbodyPalette(float t) {
t *= TEMP_MAX;
float cx = (0.860117757 + 1.54118254e-4*t + 1.28641212e-7*t*t)
/ (1.0 + 8.42420235e-4*t + 7.08145163e-7*t*t);
float cy = (0.317398726 + 4.22806245e-5*t + 4.20481691e-8*t*t)
/ (1.0 - 2.89741816e-5*t + 1.61456053e-7*t*t);
float d = 2.0*cx - 8.0*cy + 4.0;
vec3 XYZ = vec3(3.0*cx/d, 2.0*cy/d, 1.0 - (3.0*cx + 2.0*cy)/d);
vec3 RGB = mat3(3.240479, -0.969256, 0.055648,
-1.537150, 1.875992, -0.204043,
-0.498535, 0.041556, 1.057311) * vec3(XYZ.x/XYZ.y, 1.0, XYZ.z/XYZ.y);
return max(RGB, 0.0) * pow(t * 0.0004, 4.0);
}
vec3 sRGB(vec3 c) { return pow(clamp(c, 0.0, 1.0), vec3(1.0/2.2)); }
vec3 tonemap(vec3 c) { return c / (1.0 + c); }
// ============ Main ============
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
vec2 uv0 = uv;
float band = fragCoord.y / iResolution.y;
vec3 col = vec3(0.0);
if (band < 0.2) {
// Cosine Palette
float t = fragCoord.x / iResolution.x + iTime * 0.1;
col = palette(t);
} else if (band < 0.4) {
// HSV color wheel
float h = fragCoord.x / iResolution.x;
float v = (band - 0.2) / 0.2;
col = hsv2rgb(vec3(h + iTime * 0.05, 1.0, v));
} else if (band < 0.6) {
// HSL color wheel
float h = fragCoord.x / iResolution.x;
float l = (band - 0.4) / 0.2;
col = hsl2rgb(h + iTime * 0.05, 1.0, l);
} else if (band < 0.8) {
// Blackbody radiation
float t = fragCoord.x / iResolution.x;
col = tonemap(blackbodyPalette(t));
} else {
// Cosine Palette fractal glow
vec2 p = uv;
vec3 finalColor = vec3(0.0);
for (int i = 0; i < NUM_ITER; i++) {
p = fract(p * 1.5) - 0.5;
float d = length(p) * exp(-length(uv0));
vec3 c = palette(length(uv0) + float(i) * 0.4 + iTime * 0.4);
d = sin(d * 8.0 + iTime) / 8.0;
d = abs(d);
d = pow(0.01 / d, 1.2);
finalColor += c * d;
}
col = tonemap(finalColor);
}
// Band separator lines
float bandLine = smoothstep(0.003, 0.0, abs(fract(band * 5.0) - 0.5) - 0.49);
col *= 1.0 - bandLine * 0.8;
col = sRGB(col);
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Multi-Harmonic Cosine Palette (Anti-Aliased)
```glsl
vec3 fcos(vec3 x) {
vec3 w = fwidth(x);
return cos(x) * smoothstep(TAU, 0.0, w);
}
vec3 getColor(float t) {
vec3 col = vec3(0.4);
col += 0.12 * fcos(TAU * t * 1.0 + vec3(0.0, 0.8, 1.1));
col += 0.11 * fcos(TAU * t * 3.1 + vec3(0.3, 0.4, 0.1));
col += 0.10 * fcos(TAU * t * 5.1 + vec3(0.1, 0.7, 1.1));
col += 0.09 * fcos(TAU * t * 9.1 + vec3(0.2, 0.8, 1.4));
col += 0.08 * fcos(TAU * t * 17.1 + vec3(0.2, 0.6, 0.7));
col += 0.07 * fcos(TAU * t * 31.1 + vec3(0.1, 0.6, 0.7));
col += 0.06 * fcos(TAU * t * 65.1 + vec3(0.0, 0.5, 0.8));
col += 0.06 * fcos(TAU * t * 115.1 + vec3(0.1, 0.4, 0.7));
col += 0.09 * fcos(TAU * t * 265.1 + vec3(1.1, 1.4, 2.7));
return col;
}
```
### Hash-Driven Per-Tile Color
```glsl
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 tileId = floor(uv);
vec3 tileColor = palette(hash12(tileId));
```
### Saturation-Preserving Improved RGB Interpolation
```glsl
float getsat(vec3 c) {
float mi = min(min(c.x, c.y), c.z);
float ma = max(max(c.x, c.y), c.z);
return (ma - mi) / (ma + 1e-7);
}
vec3 iLerp(vec3 a, vec3 b, float x) {
vec3 ic = mix(a, b, x) + vec3(1e-6, 0.0, 0.0);
float sd = abs(getsat(ic) - mix(getsat(a), getsat(b), x));
vec3 dir = normalize(vec3(2.0*ic.x - ic.y - ic.z,
2.0*ic.y - ic.x - ic.z,
2.0*ic.z - ic.y - ic.x));
float lgt = dot(vec3(1.0), ic);
float ff = dot(dir, normalize(ic));
ic += 1.5 * dir * sd * ff * lgt;
return clamp(ic, 0.0, 1.0);
}
```
### Circular Hue Interpolation
```glsl
// HSV space (hue [0,1])
vec3 lerpHSV(vec3 a, vec3 b, float x) {
float hue = (mod(mod((b.x - a.x), 1.0) + 1.5, 1.0) - 0.5) * x + a.x;
return vec3(hue, mix(a.yz, b.yz, x));
}
// Lch space (hue [0, 2pi])
float lerpAngle(float a, float b, float x) {
float ang = mod(mod((a - b), TAU) + PI * 3.0, TAU) - PI;
return ang * x + b;
}
```
### Additive Color Stacking (Glow/HDR)
```glsl
vec3 finalColor = vec3(0.0);
for (int i = 0; i < 4; i++) {
vec3 col = palette(length(uv) + float(i) * 0.4 + iTime * 0.4);
float glow = pow(0.01 / abs(sdfValue), 1.2);
finalColor += col * glow;
}
finalColor = finalColor / (1.0 + finalColor); // Reinhard tonemap
```
## Performance & Composition
**Performance tips:**
- Cosine Palette: ~3-4 clock cycles (1 MAD + 1 COS + 1 MAD)
- HSV/HSL conversion: fully branchless using `mod`/`abs`/`clamp` vectorization
- Multi-harmonic band-limited filtering: `fwidth()` + `smoothstep` adds ~2 extra instructions to eliminate aliasing
- Lch pipeline ~57 instructions; if you only need "slightly better than RGB", use `iLerp` (~15 instructions) instead
- sRGB approximation `pow(c, 2.2)` has <0.4% error and optimizes better in the compiler
**Common combinations:**
- **Cosine Palette + SDF Raymarching**: normals/distance/attributes as t input
- **HSL/HSV + Data Visualization**: iteration count -> hue, saturation/brightness encode other dimensions
- **Cosine Palette + Fractals/Noise**: `length(uv)` or `fbm(p)` + `iTime` driving dynamic colors
- **Blackbody + Volume Rendering/Fire**: temperature field -> `blackbodyPalette()` -> physically plausible colors
- **Linear space workflow**: sRGB decode -> linear shading -> tonemap -> sRGB encode
- **Hash + Palette + Tiling**: `hash(tileID)` as palette input for unified color harmony
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/color-palette.md)

View File

@@ -0,0 +1,491 @@
## WebGL2 Adaptation Requirements
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
- Use `canvas.getContext("webgl2")`
- First line of shaders: `#version 300 es`, add `precision highp float;` in fragment shaders
- Vertex shader: `attribute` -> `in`, `varying` -> `out`
- Fragment shader: `varying` -> `in`, `gl_FragColor` -> custom output variable (must be declared before `void main()`, e.g. `out vec4 outColor;`), `texture2D()` -> `texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to the standard `void main()` entry point
# CSG Boolean Operations
## Core Principles
CSG boolean operations are per-point value operations on two distance fields:
| Operation | Expression | Meaning |
|-----------|-----------|---------|
| Union | `min(d1, d2)` | Take nearest surface, keeping both shapes |
| Intersection | `max(d1, d2)` | Take farthest surface, keeping only the overlap |
| Subtraction | `max(d1, -d2)` | Cut d1 using the interior of d2 |
**Smooth booleans** (smooth min/max) introduce a blending band in the transition region. The parameter `k` controls the blend band width (larger = rounder, `k=0` degenerates to hard boolean). Multiple variants exist with different mathematical properties.
## Implementation Steps
### Step 1: Hard Boolean Operations
```glsl
float opUnion(float d1, float d2) { return min(d1, d2); }
float opIntersection(float d1, float d2) { return max(d1, d2); }
float opSubtraction(float d1, float d2) { return max(d1, -d2); }
```
### Step 2: Smooth Union (Polynomial Version)
```glsl
// k: blend radius, typical values 0.05~0.5
float opSmoothUnion(float d1, float d2, float k) {
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) - k * h * (1.0 - h);
}
```
### Step 3: Smooth Subtraction and Intersection (Polynomial Version)
```glsl
float opSmoothSubtraction(float d1, float d2, float k) {
float h = clamp(0.5 - 0.5 * (d2 + d1) / k, 0.0, 1.0);
return mix(d2, -d1, h) + k * h * (1.0 - h);
}
float opSmoothIntersection(float d1, float d2, float k) {
float h = clamp(0.5 - 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) + k * h * (1.0 - h);
}
```
### Step 4: Quadratic Optimized Version (Recommended as Default)
```glsl
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
float smax(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return max(a, b) + h * h * 0.25 / k;
}
// Subtraction via smax
float sSub(float d1, float d2, float k) {
return smax(d1, -d2, k);
}
```
### Step 4b: Smooth Minimum Variant Library
Different smin implementations have different mathematical properties. Choose based on your needs:
| Variant | Rigid | Associative | Best For |
|---------|-------|-------------|----------|
| Quadratic (default above) | Yes | No | General use, fastest |
| Cubic | Yes | No | Smoother C2 transitions |
| Quartic | Yes | No | Highest quality blending |
| Exponential | No | Yes | Multi-body blending (order-independent) |
| Circular Geometric | Yes | Yes | Strict local blending |
**Rigid**: preserves original SDF shape outside the blend region (no under-estimation).
**Associative**: `smin(a, smin(b, c))` == `smin(smin(a, b), c)` — important when blending many objects where evaluation order varies.
```glsl
// --- Cubic Polynomial smin (C2 continuous, smoother transitions) ---
float sminCubic(float a, float b, float k) {
k *= 6.0;
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * k * (1.0 / 6.0);
}
// --- Quartic Polynomial smin (C3 continuous, highest quality) ---
float sminQuartic(float a, float b, float k) {
k *= 16.0 / 3.0;
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * h * (4.0 - h) * k * (1.0 / 16.0);
}
// --- Exponential smin (associative — order independent for multi-body blending) ---
float sminExp(float a, float b, float k) {
float r = exp2(-a / k) + exp2(-b / k);
return -k * log2(r);
}
// --- Circular Geometric smin (rigid + local + associative) ---
float sminCircle(float a, float b, float k) {
k *= 1.0 / (1.0 - sqrt(0.5));
return max(k, min(a, b)) - length(max(k - vec2(a, b), 0.0));
}
// --- Gradient-aware smin (carries material/color through blending) ---
// x = distance, yzw = material properties or color components
vec4 sminColor(vec4 a, vec4 b, float k) {
k *= 4.0;
float h = max(k - abs(a.x - b.x), 0.0) / (2.0 * k);
return vec4(
min(a.x, b.x) - h * h * k,
mix(a.yzw, b.yzw, (a.x < b.x) ? h : 1.0 - h)
);
}
// --- Smooth maximum from any smin variant ---
// smax(a, b, k) = -smin(-a, -b, k)
// Smooth subtraction: smax(d1, -d2, k)
// Smooth intersection: smax(d1, d2, k)
```
### Step 5: Basic SDF Primitives
```glsl
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
}
float sdCylinder(vec3 p, float h, float r) {
vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
```
### Step 6: CSG Composition for Scene Building
```glsl
float mapScene(vec3 p) {
float cube = sdBox(p, vec3(1.0));
float sphere = sdSphere(p, 1.2);
float cylX = sdCylinder(p.yzx, 2.0, 0.4);
float cylY = sdCylinder(p.xyz, 2.0, 0.4);
float cylZ = sdCylinder(p.zxy, 2.0, 0.4);
// (cube intersect sphere) - three cylinders = nut
float shape = opIntersection(cube, sphere);
float holes = opUnion(cylX, opUnion(cylY, cylZ));
return opSubtraction(shape, holes);
}
```
### Step 7: Smooth CSG Modeling for Organic Forms
```glsl
// Use different k values for different body parts: large k for major joints, small k for fine details
float mapCreature(vec3 p) {
float body = sdSphere(p, 0.5);
float head = sdSphere(p - vec3(0.0, 0.6, 0.3), 0.25);
float d = smin(body, head, 0.15); // large blend
float leg = sdCylinder(p - vec3(0.2, -0.5, 0.0), 0.3, 0.08);
d = smin(d, leg, 0.08); // medium blend
float eye = sdSphere(p - vec3(0.05, 0.75, 0.4), 0.05);
d = smax(d, -eye, 0.02); // small blend for subtraction
return d;
}
```
### Step 8: Ray Marching Main Loop
```glsl
float rayMarch(vec3 ro, vec3 rd, float maxDist) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * t;
float d = mapScene(p);
if (d < SURF_DIST) return t;
t += d;
if (t > maxDist) break;
}
return -1.0;
}
```
### Step 9: Normal Calculation (Tetrahedral Sampling, 4 Samples More Efficient Than 6 with Central Differences)
```glsl
vec3 calcNormal(vec3 pos) {
vec2 e = vec2(0.001, -0.001);
return normalize(
e.xyy * mapScene(pos + e.xyy) +
e.yyx * mapScene(pos + e.yyx) +
e.yxy * mapScene(pos + e.yxy) +
e.xxx * mapScene(pos + e.xxx)
);
}
```
## Full Code Template
```glsl
// === CSG Boolean Operations - WebGL2 Full Template ===
// Note: When generating HTML with this template, pass iTime, iResolution, etc. via uniforms
#define MAX_STEPS 128
#define MAX_DIST 50.0
#define SURF_DIST 0.001
#define SMOOTH_K 0.1
// === Hard Boolean Operations ===
float opUnion(float d1, float d2) { return min(d1, d2); }
float opIntersection(float d1, float d2) { return max(d1, d2); }
float opSubtraction(float d1, float d2) { return max(d1, -d2); }
// === Smooth Boolean Operations (Quadratic Optimized) ===
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
float smax(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return max(a, b) + h * h * 0.25 / k;
}
// === SDF Primitives ===
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
}
float sdCylinder(vec3 p, float h, float r) {
vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(r, h);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float sdEllipsoid(vec3 p, vec3 r) {
float k0 = length(p / r);
float k1 = length(p / (r * r));
return k0 * (k0 - 1.0) / k1;
}
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
vec3 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h) - r;
}
// === Scene Definition ===
float mapScene(vec3 p) {
// Rotation animation
float angle = iTime * 0.3;
float c = cos(angle), s = sin(angle);
p.xz = mat2(c, -s, s, c) * p.xz;
// Primitives
float cube = sdBox(p, vec3(1.0));
float sphere = sdSphere(p, 1.25);
float cylR = 0.45;
float cylX = sdCylinder(p.yzx, 2.0, cylR);
float cylY = sdCylinder(p.xyz, 2.0, cylR);
float cylZ = sdCylinder(p.zxy, 2.0, cylR);
// Hard boolean combination: nut = (cube intersect sphere) - three cylinders
float nut = opSubtraction(
opIntersection(cube, sphere),
opUnion(cylX, opUnion(cylY, cylZ))
);
// Organic spheres -- smooth union blending
float blob1 = sdSphere(p - vec3(1.8, 0.0, 0.0), 0.4);
float blob2 = sdSphere(p - vec3(-1.8, 0.0, 0.0), 0.4);
float blob3 = sdSphere(p - vec3(0.0, 1.8, 0.0), 0.4);
float blobs = smin(blob1, smin(blob2, blob3, 0.3), 0.3);
return smin(nut, blobs, 0.15);
}
// === Normal Calculation (Tetrahedral Sampling) ===
vec3 calcNormal(vec3 pos) {
vec2 e = vec2(0.001, -0.001);
return normalize(
e.xyy * mapScene(pos + e.xyy) +
e.yyx * mapScene(pos + e.yyx) +
e.yxy * mapScene(pos + e.yxy) +
e.xxx * mapScene(pos + e.xxx)
);
}
// === Ray Marching ===
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * t;
float d = mapScene(p);
if (d < SURF_DIST) return t;
t += d;
if (t > MAX_DIST) break;
}
return -1.0;
}
// === Soft Shadows ===
float calcSoftShadow(vec3 ro, vec3 rd, float k) {
float res = 1.0;
float t = 0.02;
for (int i = 0; i < 64; i++) {
float h = mapScene(ro + rd * t);
res = min(res, k * h / t);
t += clamp(h, 0.01, 0.2);
if (res < 0.001 || t > 20.0) break;
}
return clamp(res, 0.0, 1.0);
}
// === AO (Ambient Occlusion) ===
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.01 + 0.12 * float(i);
float d = mapScene(pos + h * nor);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
// === Main Function (WebGL2 Adapted) ===
out vec4 outColor;
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
// Camera
float camDist = 4.0;
float camAngle = 0.3;
vec3 ro = vec3(
camDist * cos(iTime * 0.2),
camDist * sin(camAngle),
camDist * sin(iTime * 0.2)
);
vec3 ta = vec3(0.0, 0.0, 0.0);
// Camera matrix
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = cross(uu, ww);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 2.0 * ww);
// Background color
vec3 col = vec3(0.4, 0.5, 0.6) - 0.3 * rd.y;
// Ray marching
float t = rayMarch(ro, rd);
if (t > 0.0) {
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
vec3 lightDir = normalize(vec3(0.8, 0.6, -0.3));
float dif = clamp(dot(nor, lightDir), 0.0, 1.0);
float sha = calcSoftShadow(pos + nor * 0.01, lightDir, 16.0);
float ao = calcAO(pos, nor);
float amb = 0.5 + 0.5 * nor.y;
vec3 mate = vec3(0.2, 0.3, 0.4);
col = vec3(0.0);
col += mate * 2.0 * dif * sha;
col += mate * 0.3 * amb * ao;
}
col = pow(col, vec3(0.4545));
outColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Exponential Smooth Union
```glsl
float sminExp(float a, float b, float k) {
float res = exp(-k * a) + exp(-k * b);
return -log(res) / k;
}
```
### Variant 2: Smooth Operations with Color Blending
```glsl
// Returns blend factor for the caller to blend colors
float sminWithFactor(float a, float b, float k, out float blend) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
blend = h;
return mix(b, a, h) - k * h * (1.0 - h);
}
// float blend;
// float d = sminWithFactor(d1, d2, 0.1, blend);
// vec3 color = mix(color2, color1, blend);
// vec3 overload of smax
vec3 smax(vec3 a, vec3 b, float k) {
vec3 h = max(k - abs(a - b), 0.0);
return max(a, b) + h * h * 0.25 / k;
}
```
### Variant 3: Stepwise CSG Modeling (Architectural/Industrial)
```glsl
float sdBuilding(vec3 p) {
float walls = sdBox(p, vec3(1.0, 0.8, 1.0));
vec3 roofP = p;
roofP.y -= 0.8;
float roof = sdBox(roofP, vec3(1.2, 0.3, 1.2));
float d = opUnion(walls, roof);
// Cut windows (exploiting symmetry)
vec3 winP = abs(p);
winP -= vec3(1.01, 0.3, 0.4);
float window = sdBox(winP, vec3(0.1, 0.15, 0.12));
d = opSubtraction(d, window);
// Hollow out interior
float hollow = sdBox(p, vec3(0.95, 0.75, 0.95));
d = opSubtraction(d, hollow);
return d;
}
```
### Variant 4: Large-Scale Organic Character Modeling
```glsl
float mapCharacter(vec3 p) {
float body = sdEllipsoid(p, vec3(0.5, 0.4, 0.6));
float head = sdEllipsoid(p - vec3(0.0, 0.5, 0.5), vec3(0.25));
float d = smin(body, head, 0.2); // large k: wide blend
float ear = sdEllipsoid(p - vec3(0.3, 0.6, 0.3), vec3(0.15, 0.2, 0.05));
d = smin(d, ear, 0.08); // medium blend
float nostril = sdSphere(p - vec3(0.0, 0.4, 0.7), 0.03);
d = smax(d, -nostril, 0.02); // small k: fine sculpting
return d;
}
```
## Performance & Composition Tips
**Performance:**
- Bounding volume acceleration: use AABB/bounding spheres to skip distant sub-scenes, reducing `mapScene()` calls
- Tetrahedral sampling normals (4 samples) outperform central differences (6 samples)
- Step scaling `t += d * 0.9` can reduce overshoot penetration
- Prefer quadratic optimized smin/smax (fastest); use exponential version when extreme smoothness is needed
- `k` must not be zero (division by zero error); fall back to hard boolean when near zero
- For symmetric shapes, use `abs()` to fold coordinates and define only one side
**Composition techniques:**
- **+ Domain Repetition**: `mod()`/`fract()` for infinite repetition of CSG shapes (mechanical arrays, railings)
- **+ Procedural Displacement**: overlay noise displacement on SDF for surface detail
- **+ Procedural Texturing**: use smin blend factor to simultaneously blend material ID / color
- **+ 2D SDF**: equally applicable to 2D scenes (clouds, UI shape compositing)
- **+ Animation**: bind k values, positions, and radii to `iTime` for dynamic deformation
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/csg-boolean-operations.md)

View File

@@ -0,0 +1,333 @@
# Domain Repetition & Space Folding
## Use Cases
- **Infinite repeating scenes**: render infinitely extending geometry from a single SDF primitive (corridors, cities, star fields)
- **Kaleidoscope/symmetry effects**: N-fold rotational symmetry, mirror symmetry, polyhedral symmetry
- **Fractal geometry**: generate self-similar structures through iterative space folding (Apollonian, Kali-set)
- **Architectural/mechanical structures**: build complex yet regular scenes using repetition + variation
- **Spiral/toroidal topology**: repeat geometry along polar or spiral paths
Core value: **define geometry in a single cell, render infinite space**.
## Core Principles
The essence of domain repetition is **coordinate transformation**: before computing the SDF, fold/map point `p` into a finite "fundamental domain".
**Three fundamental operations:**
| Operation | Formula | Effect |
|-----------|---------|--------|
| **mod repetition** | `p = mod(p + c/2, c) - c/2` | Infinite translational repetition along an axis |
| **abs mirroring** | `p = abs(p)` | Mirror symmetry across an axis plane |
| **Rotational folding** | `angle = mod(atan(p.y,p.x), TAU/N)` | N-fold rotational symmetry |
Key math: `mod(x,c)` -> periodic mapping to `[0,c)`; `abs(x)` -> reflection symmetry; `fract(x)` = `mod(x,1.0)` -> normalized period.
## Implementation Steps
### Step 1: Cartesian Domain Repetition (mod repetition)
```glsl
// Infinite translational repetition along one or more axes
vec3 domainRepeat(vec3 p, vec3 period) {
return mod(p + period * 0.5, period) - period * 0.5;
}
float map(vec3 p) {
vec3 q = domainRepeat(p, vec3(4.0)); // repeat every 4 units
return sdBox(q, vec3(0.5));
}
```
### Step 2: Symmetric Folding (abs-mod triangle wave)
```glsl
// Boundary-continuous symmetric folding, coordinates oscillate 0->tile->0
vec3 symmetricFold(vec3 p, float tile) {
return abs(vec3(tile) - mod(p, vec3(tile * 2.0)));
}
// Star Nest classic usage
p = abs(vec3(tile) - mod(p, vec3(tile * 2.0)));
```
### Step 3: Angular Domain Repetition (Polar Coordinate Folding)
```glsl
// N-way rotational symmetry (kaleidoscope)
vec2 pmod(vec2 p, float count) {
float angle = atan(p.x, p.y) + PI / count;
float sector = TAU / count;
angle = floor(angle / sector) * sector;
return p * rot(-angle);
}
p1.xy = pmod(p1.xy, 5.0); // 5-fold symmetry
```
### Step 4: fract Domain Folding (Fractal Iteration)
```glsl
// Apollonian fractal core loop
float map(vec3 p, float s) {
float scale = 1.0;
vec4 orb = vec4(1000.0);
for (int i = 0; i < 8; i++) {
p = -1.0 + 2.0 * fract(0.5 * p + 0.5); // centered fract folding
float r2 = dot(p, p);
orb = min(orb, vec4(abs(p), r2));
float k = s / r2; // spherical inversion scaling
p *= k;
scale *= k;
}
return 0.25 * abs(p.y) / scale;
}
```
### Step 5: Iterative abs Folding (IFS / Kali-set)
```glsl
// IFS abs folding fractal
float ifsBox(vec3 p) {
for (int i = 0; i < 5; i++) {
p = abs(p) - 1.0;
p.xy *= rot(iTime * 0.3);
p.xz *= rot(iTime * 0.1);
}
return sdBox(p, vec3(0.4, 0.8, 0.3));
}
// Kali-set variant: mod repetition + IFS + dot(p,p) scaling
vec2 de(vec3 pos) {
vec3 tpos = pos;
tpos.xz = abs(0.5 - mod(tpos.xz, 1.0));
vec4 p = vec4(tpos, 1.0); // w tracks scaling
for (int i = 0; i < 7; i++) {
p.xyz = abs(p.xyz) - vec3(-0.02, 1.98, -0.02);
p = p * (2.0) / clamp(dot(p.xyz, p.xyz), 0.4, 1.0)
- vec4(0.5, 1.0, 0.4, 0.0);
p.xz *= rot(0.416);
}
return vec2(length(max(abs(p.xyz)-vec3(0.1,5.0,0.1), 0.0)) / p.w, 0.0);
}
```
### Step 6: Reflection Folding (Polyhedral Symmetry)
```glsl
// Plane reflection
float pReflect(inout vec3 p, vec3 planeNormal, float offset) {
float t = dot(p, planeNormal) + offset;
if (t < 0.0) p = p - (2.0 * t) * planeNormal;
return sign(t);
}
// Icosahedral folding
void pModIcosahedron(inout vec3 p) {
vec3 nc = vec3(-0.5, -cos(PI/5.0), sqrt(0.75 - cos(PI/5.0)*cos(PI/5.0)));
p = abs(p);
pReflect(p, nc, 0.0);
p.xy = abs(p.xy);
pReflect(p, nc, 0.0);
p.xy = abs(p.xy);
pReflect(p, nc, 0.0);
}
```
### Step 7: Toroidal/Cylindrical Domain Warping
```glsl
// Bend the xz plane into a toroidal topology
vec2 displaceLoop(vec2 p, float radius) {
return vec2(length(p) - radius, atan(p.y, p.x));
}
pDonut.xz = displaceLoop(pDonut.xz, donutRadius);
pDonut.z *= donutRadius; // unfold angle to linear length
```
### Step 8: 1D Centered Domain Repetition (with Cell ID)
```glsl
// Returns cell index, usable for random variations
float pMod1(inout float p, float size) {
float halfsize = size * 0.5;
float c = floor((p + halfsize) / size);
p = mod(p + halfsize, size) - halfsize;
return c;
}
float cellID = pMod1(p.x, 2.0);
float salt = fract(sin(cellID * 127.1) * 43758.5453);
```
## Full Code Template
Combined demo: Cartesian repetition + angular repetition + IFS folding. Runs directly in ShaderToy.
```glsl
#define PI 3.14159265359
#define TAU 6.28318530718
#define MAX_STEPS 100
#define MAX_DIST 50.0
#define SURF_DIST 0.001
#define PERIOD 4.0
#define ANGULAR_COUNT 6.0
#define IFS_ITERS 5
#define IFS_OFFSET 1.2
mat2 rot(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c);
}
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return length(max(d, 0.0)) + min(max(d.x, max(d.y, d.z)), 0.0);
}
vec3 domainRepeat(vec3 p, vec3 period) {
return mod(p + period * 0.5, period) - period * 0.5;
}
vec2 pmod(vec2 p, float count) {
float a = atan(p.x, p.y) + PI / count;
float n = TAU / count;
a = floor(a / n) * n;
return p * rot(-a);
}
float map(vec3 p) {
vec3 q = domainRepeat(p, vec3(PERIOD));
q.xz = pmod(q.xz, ANGULAR_COUNT);
for (int i = 0; i < IFS_ITERS; i++) {
q = abs(q) - IFS_OFFSET;
q.xy *= rot(0.785);
q.yz *= rot(0.471);
}
return sdBox(q, vec3(0.15, 0.4, 0.15));
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
float raymarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
float d = map(ro + rd * t);
if (d < SURF_DIST || t > MAX_DIST) break;
t += d;
}
return t;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
float time = iTime * 0.5;
vec3 ro = vec3(sin(time) * 6.0, 3.0 + sin(time * 0.7) * 2.0, cos(time) * 6.0);
vec3 ta = vec3(0.0);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = cross(uu, ww);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.8 * ww);
float t = raymarch(ro, rd);
vec3 col = vec3(0.0);
if (t < MAX_DIST) {
vec3 p = ro + rd * t;
vec3 n = calcNormal(p);
vec3 lightDir = normalize(vec3(0.5, 0.8, -0.6));
float diff = clamp(dot(n, lightDir), 0.0, 1.0);
float amb = 0.5 + 0.5 * n.y;
vec3 baseColor = 0.5 + 0.5 * cos(p * 0.5 + vec3(0.0, 2.0, 4.0));
col = baseColor * (0.2 * amb + 0.8 * diff);
col *= exp(-0.03 * t * t);
}
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### 1. Volumetric Light/Glow Rendering
```glsl
float acc = 0.0, t = 0.0;
for (int i = 0; i < 99; i++) {
float dist = map(ro + rd * t);
dist = max(abs(dist), 0.02);
acc += exp(-dist * 3.0); // decay factor controls glow sharpness
t += dist * 0.5; // step scale <1 for denser sampling
}
vec3 col = vec3(acc * 0.01, acc * 0.011, acc * 0.012);
```
### 2. Single-Axis/Dual-Axis Selective Repetition
```glsl
q.xz = mod(q.xz + 2.0, 4.0) - 2.0; // repeat only xz, y stays unchanged
```
### 3. Fractal fract Domain Folding (Apollonian Type)
```glsl
float scale = 1.0;
for (int i = 0; i < 8; i++) {
p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
float k = 1.2 / dot(p, p);
p *= k;
scale *= k;
}
return 0.25 * abs(p.y) / scale;
```
### 4. Multi-Layer Nested Repetition
```glsl
float indexX = amod(p.xz, segments); // outer layer: angular repetition
p.x -= radius;
p.y = repeat(p.y, cellSize); // inner layer: linear repetition
float salt = rng(vec2(indexX, floor(p.y / cellSize)));
```
### 5. Finite Domain Repetition (Clamp Limited)
```glsl
vec3 domainRepeatLimited(vec3 p, float size, vec3 limit) {
return p - size * clamp(floor(p / size + 0.5), -limit, limit);
}
// Repeat 5 times along x, 3 times along y/z
vec3 q = domainRepeatLimited(p, 2.0, vec3(2.0, 1.0, 1.0));
```
## Performance & Composition Tips
**Performance:**
- 5-8 fractal iterations are typically sufficient; use `vec4.w` to track scaling and avoid extra variables
- Ensure geometry radius < period/2 to prevent inaccurate SDF at cell boundaries
- Volumetric light step size should increase with distance: `t += dist * (0.3 + t * 0.02)`
- Use `clamp(dot(p,p), min, max)` to prevent numerical explosion
- Avoid `normalize()` inside loops; manually divide by length instead
**Composition:**
- **Domain Repetition + Ray Marching**: the most fundamental combination, used by all reference shaders
- **Domain Repetition + Orbit Trap Coloring**: record `min(orb, abs(p))` during fractal iteration for coloring
- **Domain Repetition + Toroidal Warping**: `displaceLoop` to bend space before applying linear/angular repetition
- **Domain Repetition + Noise Variation**: cell ID -> pseudo-random number -> modulate geometry parameters
- **Domain Repetition + Polar Spiral**: `cartToPolar` combined with `pMod1` for spiral path repetition
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/domain-repetition.md)

View File

@@ -0,0 +1,414 @@
# Domain Warping
## Use Cases
- **Marble/jade textures**: multi-layer warping produces streaked stone textures
- **Fabric/silk appearance**: warping field creases simulate textile surfaces
- **Geological formations**: rock strata, lava flows, surface erosion
- **Gas giant atmospheres**: Jupiter-style banded circulation
- **Smoke/fire/explosions**: fluid effects combined with volumetric rendering
- **Abstract art backgrounds**: procedural organic patterns, suitable for UI backgrounds, music visualization
- **Electric current/plasma effects**: ridged FBM variant produces sharp arc patterns
Core advantage: relies only on math functions (no texture assets needed), outputs seamless tiling, animatable, GPU-friendly.
## Core Principles
Warp input coordinates with noise, then query the main function:
```
f(p) -> f(p + fbm(p))
```
Classic multi-layer recursive nesting:
```
result = fbm(p + fbm(p + fbm(p)))
```
Each FBM layer's output serves as a coordinate offset for the next layer; deeper nesting produces more organic deformation.
**Key mathematical structure**:
1. **Noise** `noise(p)`: pseudo-random values at integer lattice points + Hermite interpolation `f*f*(3.0-2.0*f)`
2. **FBM**: `fbm(p) = sum of (0.5^i) * noise(p * 2^i * R^i)`, where `R` is a rotation matrix for decorrelation
3. **Domain warping chain**: `fbm(p + fbm(p + fbm(p)))`
The rotation matrix `mat2(0.80, 0.60, -0.60, 0.80)` (approx 36.87 deg) is the most widely used decorrelation transform.
## Implementation Steps
### Step 1: Hash Function
```glsl
// Map 2D integer coordinates to a pseudo-random float
float hash(vec2 p) {
p = fract(p * 0.6180339887); // golden ratio pre-perturbation
p *= 25.0;
return fract(p.x * p.y * (p.x + p.y));
}
```
> The classic `fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453)` also works; the sin-free version above has more stable precision on some GPUs.
### Step 2: Value Noise
```glsl
// Hash values at integer lattice points, Hermite smooth interpolation
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
```
### Step 3: FBM
```glsl
const mat2 mtx = mat2(0.80, 0.60, -0.60, 0.80); // rotation approx 36.87 deg
float fbm(vec2 p) {
float f = 0.0;
f += 0.500000 * noise(p); p = mtx * p * 2.02;
f += 0.250000 * noise(p); p = mtx * p * 2.03;
f += 0.125000 * noise(p); p = mtx * p * 2.01;
f += 0.062500 * noise(p); p = mtx * p * 2.04;
f += 0.031250 * noise(p); p = mtx * p * 2.01;
f += 0.015625 * noise(p);
return f / 0.96875;
}
```
> Lacunarity uses 2.01~2.04 rather than exactly 2.0 to avoid visual artifacts caused by lattice regularity.
### Step 4: Domain Warping (Core)
```glsl
// Classic three-layer domain warping
float pattern(vec2 p) {
return fbm(p + fbm(p + fbm(p)));
}
```
### Step 5: Time Animation
```glsl
// Inject time into the first and last octaves: low frequency drives overall flow, high frequency adds detail variation
float fbm(vec2 p) {
float f = 0.0;
f += 0.500000 * noise(p + iTime); // lowest frequency: slow overall flow
p = mtx * p * 2.02;
f += 0.250000 * noise(p); p = mtx * p * 2.03;
f += 0.125000 * noise(p); p = mtx * p * 2.01;
f += 0.062500 * noise(p); p = mtx * p * 2.04;
f += 0.031250 * noise(p); p = mtx * p * 2.01;
f += 0.015625 * noise(p + sin(iTime)); // highest frequency: subtle detail motion
return f / 0.96875;
}
```
### Step 6: Coloring
```glsl
// Map scalar field (0~1) to color using a mix chain
// IMPORTANT: Note: GLSL is strictly typed. Variable declarations must be complete, e.g. vec3 col = vec3(0.2, 0.1, 0.4)
// IMPORTANT: Decimals must be written as 0.x, not .x (division by zero errors)
vec3 palette(float t) {
vec3 col = vec3(0.2, 0.1, 0.4); // deep purple base
col = mix(col, vec3(0.3, 0.05, 0.05), t); // dark red
col = mix(col, vec3(0.9, 0.9, 0.9), t * t); // high values toward white
col = mix(col, vec3(0.0, 0.2, 0.4), smoothstep(0.6, 0.8, t)); // blue highlight
return col * t * 2.0;
}
```
## Full Code Template
```glsl
// Domain Warping — Full Runnable Template (ShaderToy)
#define WARP_DEPTH 3 // Warp nesting depth (1=subtle, 2=moderate, 3=classic)
#define NUM_OCTAVES 6 // FBM octave count (4=coarse fast, 6=fine)
#define TIME_SCALE 1.0 // Animation speed (0.05=very slow, 1.0=fluid, 2.0=fast)
#define WARP_STRENGTH 1.0 // Warp intensity (0.5=subtle, 1.0=standard, 2.0=strong)
#define BASE_SCALE 3.0 // Overall noise scale (larger = denser texture)
const mat2 mtx = mat2(0.80, 0.60, -0.60, 0.80);
float hash(vec2 p) {
p = fract(p * 0.6180339887);
p *= 25.0;
return fract(p.x * p.y * (p.x + p.y));
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i + vec2(0.0, 0.0)), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
float fbm(vec2 p) {
float f = 0.0;
float amp = 0.5;
float freq = 1.0;
float norm = 0.0;
for (int i = 0; i < NUM_OCTAVES; i++) {
float t = 0.0;
if (i == 0) t = iTime * TIME_SCALE;
if (i == NUM_OCTAVES - 1) t = sin(iTime * TIME_SCALE);
f += amp * noise(p + t);
norm += amp;
p = mtx * p * 2.02;
amp *= 0.5;
}
return f / norm;
}
float pattern(vec2 p) {
float val = fbm(p);
#if WARP_DEPTH >= 2
val = fbm(p + WARP_STRENGTH * val);
#endif
#if WARP_DEPTH >= 3
val = fbm(p + WARP_STRENGTH * val);
#endif
return val;
}
vec3 palette(float t) {
vec3 col = vec3(0.2, 0.1, 0.4);
col = mix(col, vec3(0.3, 0.05, 0.05), t);
col = mix(col, vec3(0.9, 0.9, 0.9), t * t);
col = mix(col, vec3(0.0, 0.2, 0.4), smoothstep(0.6, 0.8, t));
return col * t * 2.0;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
uv *= BASE_SCALE;
float shade = pattern(uv);
vec3 col = palette(shade);
// Vignette effect
vec2 q = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * sqrt(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Multi-Resolution Layered Warping
Different warp layers use FBM with different octave counts, outputting `vec2` for dual-axis displacement, with intermediate variables used for coloring.
```glsl
float fbm4(vec2 p) {
float f = 0.0;
f += 0.5000 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.02;
f += 0.2500 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.03;
f += 0.1250 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.01;
f += 0.0625 * (-1.0 + 2.0 * noise(p));
return f / 0.9375;
}
float fbm6(vec2 p) {
float f = 0.0;
f += 0.500000 * noise(p); p = mtx * p * 2.02;
f += 0.250000 * noise(p); p = mtx * p * 2.03;
f += 0.125000 * noise(p); p = mtx * p * 2.01;
f += 0.062500 * noise(p); p = mtx * p * 2.04;
f += 0.031250 * noise(p); p = mtx * p * 2.01;
f += 0.015625 * noise(p);
return f / 0.96875;
}
vec2 fbm4_2(vec2 p) {
return vec2(fbm4(p + vec2(1.0)), fbm4(p + vec2(6.2)));
}
vec2 fbm6_2(vec2 p) {
return vec2(fbm6(p + vec2(9.2)), fbm6(p + vec2(5.7)));
}
float func(vec2 q, out vec2 o, out vec2 n) {
q += 0.05 * sin(vec2(0.11, 0.13) * iTime + length(q) * 4.0);
o = 0.5 + 0.5 * fbm4_2(q);
o += 0.02 * sin(vec2(0.13, 0.11) * iTime * length(o));
n = fbm6_2(4.0 * o);
vec2 p = q + 2.0 * n + 1.0;
float f = 0.5 + 0.5 * fbm4(2.0 * p);
f = mix(f, f * f * f * 3.5, f * abs(n.x));
return f;
}
// Coloring uses intermediate variables o, n
vec3 col = vec3(0.2, 0.1, 0.4);
col = mix(col, vec3(0.3, 0.05, 0.05), f);
col = mix(col, vec3(0.9, 0.9, 0.9), dot(n, n));
col = mix(col, vec3(0.5, 0.2, 0.2), 0.5 * o.y * o.y);
col = mix(col, vec3(0.0, 0.2, 0.4), 0.5 * smoothstep(1.2, 1.3, abs(n.y) + abs(n.x)));
col *= f * 2.0;
```
### Variant 2: Turbulence/Ridged Warping (Electric Arc/Plasma Effect)
In FBM, apply `abs(noise - 0.5)` to produce ridged textures, with dual-axis independent displacement + time-reversed drift.
```glsl
float fbm_ridged(vec2 p) {
float z = 2.0;
float rz = 0.0;
for (float i = 1.0; i < 6.0; i++) {
rz += abs((noise(p) - 0.5) * 2.0) / z;
z *= 2.0;
p *= 2.0;
}
return rz;
}
float dualfbm(vec2 p) {
vec2 p2 = p * 0.7;
vec2 basis = vec2(
fbm_ridged(p2 - iTime * 0.24),
fbm_ridged(p2 + iTime * 0.26)
);
basis = (basis - 0.5) * 0.2;
p += basis;
return fbm_ridged(p * makem2(iTime * 0.03));
}
// Electric arc coloring
vec3 col = vec3(0.2, 0.1, 0.4) / rz;
```
### Variant 3: Pseudo-3D Lit Domain Warping
Estimate screen-space normals via finite differences, apply directional lighting for an embossed effect.
```glsl
float e = 2.0 / iResolution.y;
vec3 nor = normalize(vec3(
pattern(p + vec2(e, 0.0)) - shade,
2.0 * e,
pattern(p + vec2(0.0, e)) - shade
));
vec3 lig = normalize(vec3(0.9, 0.2, -0.4));
float dif = clamp(0.3 + 0.7 * dot(nor, lig), 0.0, 1.0);
vec3 lin = vec3(0.70, 0.90, 0.95) * (nor.y * 0.5 + 0.5);
lin += vec3(0.15, 0.10, 0.05) * dif;
col *= 1.2 * lin;
col = 1.0 - col;
col = 1.1 * col * col;
```
### Variant 4: Flow Field Iterative Warping (Gas Giant Effect)
Compute the FBM gradient field, Euler-integrate to iteratively advect coordinates, simulating fluid convection vortices.
```glsl
#define ADVECT_ITERATIONS 5
vec2 field(vec2 p) {
float t = 0.2 * iTime;
p.x += t;
float n = fbm(p, t);
float e = 0.25;
float nx = fbm(p + vec2(e, 0.0), t);
float ny = fbm(p + vec2(0.0, e), t);
return vec2(n - ny, nx - n) / e;
}
vec3 distort(vec2 p) {
for (float i = 0.0; i < float(ADVECT_ITERATIONS); i++) {
p += field(p) / float(ADVECT_ITERATIONS);
}
return vec3(fbm(p, 0.0));
}
```
### Variant 5: 3D Volumetric Domain Warping (Explosion/Fireball Effect)
Displace a sphere SDF with 3D FBM, rendered via volumetric ray marching.
```glsl
#define NOISE_FREQ 4.0
#define NOISE_AMP -0.5
mat3 m3 = mat3(0.00, 0.80, 0.60,
-0.80, 0.36,-0.48,
-0.60,-0.48, 0.64);
float noise3D(vec3 p) {
vec3 fl = floor(p);
vec3 fr = fract(p);
fr = fr * fr * (3.0 - 2.0 * fr);
float n = fl.x + fl.y * 157.0 + 113.0 * fl.z;
return mix(mix(mix(hash(n+0.0), hash(n+1.0), fr.x),
mix(hash(n+157.0), hash(n+158.0), fr.x), fr.y),
mix(mix(hash(n+113.0), hash(n+114.0), fr.x),
mix(hash(n+270.0), hash(n+271.0), fr.x), fr.y), fr.z);
}
float fbm3D(vec3 p) {
float f = 0.0;
f += 0.5000 * noise3D(p); p = m3 * p * 2.02;
f += 0.2500 * noise3D(p); p = m3 * p * 2.03;
f += 0.1250 * noise3D(p); p = m3 * p * 2.01;
f += 0.0625 * noise3D(p); p = m3 * p * 2.02;
f += 0.03125 * abs(noise3D(p));
return f / 0.9375;
}
float distanceFunc(vec3 p, out float displace) {
float d = length(p) - 0.5;
displace = fbm3D(p * NOISE_FREQ + vec3(0, -1, 0) * iTime);
d += displace * NOISE_AMP;
return d;
}
```
## Performance & Composition
### Performance Tips
- Three warp layers x 6 octaves = 18 noise samples per pixel; adding lit finite differences can reach 54
- **Reduce octaves**: 4 instead of 6, ~33% performance gain with minimal visual difference
- **Reduce warp depth**: two layers `fbm(p + fbm(p))` is already organic enough, saving ~33%
- **sin-product noise**: `sin(p.x)*sin(p.y)` is branchless and memory-free, suitable for mobile
- **GPU built-in derivatives**: `dFdx/dFdy` instead of finite differences, 3x faster
- **Texture noise**: pre-bake noise textures, trading computation for memory reads
- **LOD adaptive**: reduce octave count for distant pixels
- **Supersampling**: only use 2x2 when anti-aliasing is needed, 4x performance cost
### Composition Suggestions
- **Ray marching**: warped scalar field as SDF displacement function -> fire, explosions, organic forms
- **Polar coordinate transform**: domain warping in polar space -> vortices, nebulae, spirals
- **Cosine palette**: `a + b*cos(2*pi*(c*t+d))` is more flexible than mix chains
- **Post-processing**: bloom glow, tone mapping `col/(1+col)`, chromatic aberration (RGB channel offset sampling)
- **Particles/geometry**: scalar field driving particle velocity fields, vertex displacement, UV animation
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/domain-warping.md)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,436 @@
# Fractal Rendering Skill
## Use Cases
- Rendering self-similar mathematical structures: Mandelbrot/Julia sets (2D), Mandelbulb (3D), IFS fractals (Menger/Apollonian)
- Procedural textures or backgrounds requiring infinite detail
- Real-time generation of complex geometric visual effects (music visualization, sci-fi scenes, abstract art)
- Suitable for ShaderToy, demo scene, procedural content generation
## Core Principles
Fractal rendering is essentially **visualization of iterative systems**, falling into three categories:
### 1. Escape-Time Algorithm
Iterate `Z <- Z^2 + c`, count escape steps. Distance estimation by simultaneously tracking the derivative `Z'`:
```
Z <- Z^2 + c
Z' <- 2*Z*Z' + 1
d(c) = |Z|*log|Z| / |Z'|
```
### 2. Iterated Function System (IFS / KIFS)
Fold-sort-scale-offset iteration produces self-similar structures:
```
p = abs(p) // fold
sort p.xyz descending // sort
p = Scale * p - Offset * (Scale-1) // scale and offset
```
### 3. Spherical Inversion Fractals
`fract()` space folding + spherical inversion `p *= s/dot(p,p)`:
```
p = -1.0 + 2.0 * fract(0.5*p + 0.5)
k = s / dot(p, p)
p *= k; scale *= k
```
All 3D fractals are rendered via **Sphere Tracing (Ray Marching)**.
## Implementation Steps
### Step 1: Coordinate Normalization
```glsl
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
```
### Step 2: 2D Mandelbrot Escape-Time Iteration
```glsl
float distanceToMandelbrot(in vec2 c) {
vec2 z = vec2(0.0);
vec2 dz = vec2(0.0);
float m2 = 0.0;
for (int i = 0; i < MAX_ITER; i++) {
if (m2 > BAILOUT * BAILOUT) break;
// Z' -> 2*Z*Z' + 1
dz = 2.0 * vec2(z.x*dz.x - z.y*dz.y,
z.x*dz.y + z.y*dz.x) + vec2(1.0, 0.0);
// Z -> Z^2 + c
z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;
m2 = dot(z, z);
}
return 0.5 * sqrt(dot(z,z) / dot(dz,dz)) * log(dot(z,z));
}
```
### Step 3: Mandelbulb Distance Field (Spherical Coordinate Power-N)
```glsl
float mandelbulb(vec3 p) {
vec3 z = p;
float dr = 1.0;
float r;
for (int i = 0; i < FRACTAL_ITER; i++) {
r = length(z);
if (r > BAILOUT) break;
float theta = atan(z.y, z.x);
float phi = asin(z.z / r);
dr = pow(r, POWER - 1.0) * dr * POWER + 1.0;
r = pow(r, POWER);
theta *= POWER;
phi *= POWER;
z = r * vec3(cos(theta)*cos(phi),
sin(theta)*cos(phi),
sin(phi)) + p;
}
return 0.5 * log(r) * r / dr;
}
```
### Step 4: Menger Sponge Distance Field (KIFS)
```glsl
float mengerDE(vec3 z) {
z = abs(1.0 - mod(z, 2.0)); // infinite tiling
float d = 1000.0;
for (int n = 0; n < IFS_ITER; n++) {
z = abs(z);
if (z.x < z.y) z.xy = z.yx;
if (z.x < z.z) z.xz = z.zx;
if (z.y < z.z) z.yz = z.zy;
z = SCALE * z - OFFSET * (SCALE - 1.0);
if (z.z < -0.5 * OFFSET.z * (SCALE - 1.0))
z.z += OFFSET.z * (SCALE - 1.0);
d = min(d, length(z) * pow(SCALE, float(-n) - 1.0));
}
return d - 0.001;
}
```
### Step 5: Apollonian Distance Field (Spherical Inversion)
```glsl
vec4 orb; // orbit trap
float apollonianDE(vec3 p, float s) {
float scale = 1.0;
orb = vec4(1000.0);
for (int i = 0; i < INVERSION_ITER; i++) {
p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
float r2 = dot(p, p);
orb = min(orb, vec4(abs(p), r2));
float k = s / r2;
p *= k;
scale *= k;
}
return 0.25 * abs(p.y) / scale;
}
```
### Step 6: Ray Marching
```glsl
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.01;
for (int i = 0; i < MAX_STEPS; i++) {
float precis = PRECISION * t;
float h = map(ro + rd * t);
if (h < precis || t > MAX_DIST) break;
t += h * FUDGE_FACTOR;
}
return (t > MAX_DIST) ? -1.0 : t;
}
```
### Step 7: Normal Calculation
```glsl
// 4-tap tetrahedral method (recommended)
vec3 calcNormal(vec3 pos, float t) {
float precis = 0.001 * t;
vec2 e = vec2(1.0, -1.0) * precis;
return normalize(
e.xyy * map(pos + e.xyy) +
e.yyx * map(pos + e.yyx) +
e.yxy * map(pos + e.yxy) +
e.xxx * map(pos + e.xxx));
}
```
### Step 8: Shading & Lighting
```glsl
vec3 shade(vec3 pos, vec3 nor, vec3 rd, vec4 trap) {
vec3 light1 = normalize(LIGHT_DIR);
float diff = clamp(dot(light1, nor), 0.0, 1.0);
float amb = 0.7 + 0.3 * nor.y;
float ao = pow(clamp(trap.w * 2.0, 0.0, 1.0), 1.2);
vec3 brdf = vec3(0.4) * amb * ao + vec3(1.0) * diff * ao;
vec3 rgb = vec3(1.0);
rgb = mix(rgb, vec3(1.0, 0.8, 0.2), clamp(6.0*trap.y, 0.0, 1.0));
rgb = mix(rgb, vec3(1.0, 0.55, 0.0), pow(clamp(1.0-2.0*trap.z, 0.0, 1.0), 8.0));
return rgb * brdf;
}
```
### Step 9: Camera
```glsl
void setupCamera(vec2 uv, vec3 ro, vec3 ta, float cr, out vec3 rd) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = normalize(cross(cu, cw));
rd = normalize(uv.x * cu + uv.y * cv + 2.0 * cw);
}
```
## Complete Code Template
3D Apollonian fractal (spherical inversion type) with full ray marching pipeline, orbit trap coloring, and AO. Ready to run in ShaderToy.
```glsl
// Fractal Rendering — Apollonian (Spherical Inversion) Template
#define MAX_STEPS 200
#define MAX_DIST 30.0
#define PRECISION 0.001
#define INVERSION_ITER 8 // Tunable: 5-12
#define AA 1 // Tunable: 1=no AA, 2=4xSSAA
vec4 orb;
float map(vec3 p, float s) {
float scale = 1.0;
orb = vec4(1000.0);
for (int i = 0; i < INVERSION_ITER; i++) {
p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
float r2 = dot(p, p);
orb = min(orb, vec4(abs(p), r2));
float k = s / r2;
p *= k;
scale *= k;
}
return 0.25 * abs(p.y) / scale;
}
float trace(vec3 ro, vec3 rd, float s) {
float t = 0.01;
for (int i = 0; i < MAX_STEPS; i++) {
float precis = PRECISION * t;
float h = map(ro + rd * t, s);
if (h < precis || t > MAX_DIST) break;
t += h;
}
return (t > MAX_DIST) ? -1.0 : t;
}
vec3 calcNormal(vec3 pos, float t, float s) {
float precis = PRECISION * t;
vec2 e = vec2(1.0, -1.0) * precis;
return normalize(
e.xyy * map(pos + e.xyy, s) +
e.yyx * map(pos + e.yyx, s) +
e.yxy * map(pos + e.yxy, s) +
e.xxx * map(pos + e.xxx, s));
}
vec3 render(vec3 ro, vec3 rd, float anim) {
vec3 col = vec3(0.0);
float t = trace(ro, rd, anim);
if (t > 0.0) {
vec4 tra = orb;
vec3 pos = ro + t * rd;
vec3 nor = calcNormal(pos, t, anim);
vec3 light1 = normalize(vec3(0.577, 0.577, -0.577));
vec3 light2 = normalize(vec3(-0.707, 0.0, 0.707));
float key = clamp(dot(light1, nor), 0.0, 1.0);
float bac = clamp(0.2 + 0.8 * dot(light2, nor), 0.0, 1.0);
float amb = 0.7 + 0.3 * nor.y;
float ao = pow(clamp(tra.w * 2.0, 0.0, 1.0), 1.2);
vec3 brdf = vec3(0.40) * amb * ao
+ vec3(1.00) * key * ao
+ vec3(0.40) * bac * ao;
vec3 rgb = vec3(1.0);
rgb = mix(rgb, vec3(1.0, 0.80, 0.2), clamp(6.0 * tra.y, 0.0, 1.0));
rgb = mix(rgb, vec3(1.0, 0.55, 0.0), pow(clamp(1.0 - 2.0*tra.z, 0.0, 1.0), 8.0));
col = rgb * brdf * exp(-0.2 * t);
}
return sqrt(col);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
float time = iTime * 0.25;
float anim = 1.1 + 0.5 * smoothstep(-0.3, 0.3, cos(0.1 * iTime));
vec3 tot = vec3(0.0);
#if AA > 1
for (int jj = 0; jj < AA; jj++)
for (int ii = 0; ii < AA; ii++)
#else
int ii = 1, jj = 1;
#endif
{
vec2 q = fragCoord.xy + vec2(float(ii), float(jj)) / float(AA);
vec2 p = (2.0 * q - iResolution.xy) / iResolution.y;
vec3 ro = vec3(2.8*cos(0.1 + 0.33*time),
0.4 + 0.3*cos(0.37*time),
2.8*cos(0.5 + 0.35*time));
vec3 ta = vec3(1.9*cos(1.2 + 0.41*time),
0.4 + 0.1*cos(0.27*time),
1.9*cos(2.0 + 0.38*time));
float roll = 0.2 * cos(0.1 * time);
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(roll), cos(roll), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = normalize(cross(cu, cw));
vec3 rd = normalize(p.x*cu + p.y*cv + 2.0*cw);
tot += render(ro, rd, anim);
}
tot /= float(AA * AA);
fragColor = vec4(tot, 1.0);
}
```
## Common Variants
### 1. 2D Mandelbrot (Distance Estimation Coloring)
Pure 2D, no ray marching needed. Complex iteration + distance coloring.
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0*fragCoord - iResolution.xy) / iResolution.y;
float tz = 0.5 - 0.5*cos(0.225*iTime);
float zoo = pow(0.5, 13.0*tz);
vec2 c = vec2(-0.05, 0.6805) + p * zoo; // Tunable: zoom center point
vec2 z = vec2(0.0), dz = vec2(0.0);
for (int i = 0; i < 300; i++) {
if (dot(z,z) > 1024.0) break;
dz = 2.0*vec2(z.x*dz.x-z.y*dz.y, z.x*dz.y+z.y*dz.x) + vec2(1.0,0.0);
z = vec2(z.x*z.x-z.y*z.y, 2.0*z.x*z.y) + c;
}
float d = 0.5*sqrt(dot(z,z)/dot(dz,dz))*log(dot(z,z));
d = clamp(pow(4.0*d/zoo, 0.2), 0.0, 1.0);
fragColor = vec4(vec3(d), 1.0);
}
```
### 2. Mandelbulb Power-N
Spherical coordinate trigonometric functions; `POWER` parameter controls morphology.
```glsl
#define POWER 8.0 // Tunable: 2-16
#define FRACTAL_ITER 4 // Tunable: 2-8
float mandelbulbDE(vec3 p) {
vec3 z = p;
float dr = 1.0, r;
for (int i = 0; i < FRACTAL_ITER; i++) {
r = length(z);
if (r > 2.0) break;
float theta = atan(z.y, z.x);
float phi = asin(z.z / r);
dr = pow(r, POWER - 1.0) * dr * POWER + 1.0;
r = pow(r, POWER);
theta *= POWER; phi *= POWER;
z = r * vec3(cos(theta)*cos(phi), sin(theta)*cos(phi), sin(phi)) + p;
}
return 0.5 * log(r) * r / dr;
}
```
### 3. Menger Sponge (KIFS)
`abs()` folding + conditional sorting, regular geometric fractal.
```glsl
#define SCALE 3.0
#define OFFSET vec3(0.92858,0.92858,0.32858)
#define IFS_ITER 7
float mengerDE(vec3 z) {
z = abs(1.0 - mod(z, 2.0));
float d = 1000.0;
for (int n = 0; n < IFS_ITER; n++) {
z = abs(z);
if (z.x < z.y) z.xy = z.yx;
if (z.x < z.z) z.xz = z.zx;
if (z.y < z.z) z.yz = z.zy;
z = SCALE * z - OFFSET * (SCALE - 1.0);
if (z.z < -0.5*OFFSET.z*(SCALE-1.0))
z.z += OFFSET.z*(SCALE-1.0);
d = min(d, length(z) * pow(SCALE, float(-n)-1.0));
}
return d - 0.001;
}
```
### 4. Quaternion Julia Set
Quaternion `Z <- Z^2 + c` (4D), with fixed `c` parameter; visualized by taking a 3D slice.
```glsl
vec4 qsqr(vec4 a) {
return vec4(a.x*a.x - a.y*a.y - a.z*a.z - a.w*a.w,
2.0*a.x*a.y, 2.0*a.x*a.z, 2.0*a.x*a.w);
}
float juliaDE(vec3 p, vec4 c) {
vec4 z = vec4(p, 0.0);
float md2 = 1.0, mz2 = dot(z, z);
for (int i = 0; i < 11; i++) {
md2 *= 4.0 * mz2;
z = qsqr(z) + c;
mz2 = dot(z, z);
if (mz2 > 4.0) break;
}
return 0.25 * sqrt(mz2 / md2) * log(mz2);
}
// Animated c: vec4 c = 0.45*cos(vec4(0.5,3.9,1.4,1.1)+time*vec4(1.2,1.7,1.3,2.5))-vec4(0.3,0,0,0);
```
### 5. Minimal IFS Field (2D, No Ray Marching)
`abs(p)/dot(p,p) + offset` iteration, weighted accumulation produces a density field.
```glsl
float field(vec3 p) {
float strength = 7.0 + 0.03 * log(1.e-6 + fract(sin(iTime) * 4373.11));
float accum = 0.0, prev = 0.0, tw = 0.0;
for (int i = 0; i < 32; ++i) {
float mag = dot(p, p);
p = abs(p) / mag + vec3(-0.5, -0.4, -1.5); // Tunable: offset values
float w = exp(-float(i) / 7.0);
accum += w * exp(-strength * pow(abs(mag - prev), 2.3));
tw += w;
prev = mag;
}
return max(0.0, 5.0 * accum / tw - 0.7);
}
```
## Performance & Composition
### Performance Tips
- Core bottleneck: outer ray marching x inner fractal iteration (e.g., `200 x 8 = 1600` map calls per pixel)
- Reduce `MAX_STEPS` to 60-100, compensate with fudge factor 0.7-0.9
- Hit threshold `precis = 0.001 * t` relaxes with distance
- Fractal iteration: break immediately when `|z|^2 > bailout`
- Reducing iterations from 8 to 4-5 has minimal visual impact
- Use 4-tap normals instead of 6-tap to save 33%
- Use AA=1 during development, AA=2 for release (AA=3 = 9x overhead)
- Avoid `pow()` inside loops; manually expand for low powers
### Composition Techniques
- **Volumetric light**: accumulate `exp(-10.0 * h)` during ray march for god rays
- **Tone Mapping**: ACES + sRGB gamma for handling high-frequency detail
- **Transparent refraction**: negative distance field reverse ray march + Beer's law absorption
- **Orbit Trap coloring**: map trap values to HSV or emissive colors
- **Soft shadows**: ray march toward light, accumulate `min(k * h / t)` for soft shadows
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/fractal-rendering.md)

View File

@@ -0,0 +1,527 @@
# Lighting Models Skill
## Use Cases
- Adding realistic lighting to raymarched or rasterized scenes
- Simulating light interaction with various materials (metal, dielectric, water, skin, etc.)
- From simple diffuse/specular to full PBR
- Multi-light compositing (sun, sky, ambient)
- Adding material appearance to SDF scenes in ShaderToy
## Core Principles
Lighting = Diffuse + Specular Reflection:
- **Diffuse**: Lambert's law `I = max(0, N·L)`
- **Specular**: Empirical model uses Blinn-Phong `pow(max(0, N·H), shininess)`; physically-based model uses Cook-Torrance BRDF
### Key Formulas
```
Lambert: L_diffuse = albedo * lightColor * max(0, N·L)
Blinn-Phong: H = normalize(V + L); L_specular = lightColor * pow(max(0, N·H), shininess)
Cook-Torrance: f_specular = D(h) * F(v,h) * G(l,v,h) / (4 * (N·L) * (N·V))
Fresnel: F = F0 + (1 - F0) * (1 - V·H)^5
```
- **D** = GGX/Trowbridge-Reitz normal distribution
- **F** = Schlick Fresnel approximation
- **G** = Smith geometric shadowing
- F0: dielectric ~0.04, metals use baseColor
## Implementation Steps
### Step 1: Scene Basics (Normal + Vector Setup)
```glsl
// SDF normal (finite difference method)
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
vec3 N = calcNormal(pos); // surface normal
vec3 V = -rd; // view direction
vec3 L = normalize(lightPos - pos); // light direction (point light)
// directional light: vec3 L = normalize(vec3(0.6, 0.8, -0.5));
```
### Step 2: Lambert Diffuse
```glsl
float NdotL = max(0.0, dot(N, L));
vec3 diffuse = albedo * lightColor * NdotL;
// energy-conserving version
vec3 diffuse_conserved = albedo / PI * lightColor * NdotL;
// Half-Lambert (reduces over-darkening on backlit faces, commonly used for SSS approximation)
float halfLambert = NdotL * 0.5 + 0.5;
vec3 diffuse_wrapped = albedo * lightColor * halfLambert;
```
### Step 3: Blinn-Phong Specular
```glsl
vec3 H = normalize(V + L);
float NdotH = max(0.0, dot(N, H));
float SHININESS = 32.0; // 4.0 (rough) ~ 256.0 (smooth)
// with normalization factor for energy conservation
float normFactor = (SHININESS + 8.0) / (8.0 * PI);
float spec = normFactor * pow(NdotH, SHININESS);
vec3 specular = lightColor * spec;
```
### Step 4: Fresnel-Schlick
```glsl
vec3 fresnelSchlick(vec3 F0, float cosTheta) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// metallic workflow
vec3 F0 = mix(vec3(0.04), baseColor, metallic);
// computed with V·H (specular reflection BRDF)
float VdotH = max(0.0, dot(V, H));
vec3 F = fresnelSchlick(F0, VdotH);
// computed with N·V (environment reflection, rim light)
float NdotV = max(0.0, dot(N, V));
vec3 F_env = fresnelSchlick(F0, NdotV);
```
### Step 5: GGX Normal Distribution (D Term)
```glsl
float distributionGGX(float NdotH, float roughness) {
float a = roughness * roughness; // roughness must be squared first
float a2 = a * a;
float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * denom * denom);
}
```
### Step 6: Geometric Shadowing (G Term)
```glsl
// Method 1: Schlick-GGX
float geometrySchlickGGX(float NdotV, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float geometrySmith(float NdotV, float NdotL, float roughness) {
return geometrySchlickGGX(NdotV, roughness) * geometrySchlickGGX(NdotL, roughness);
}
// Method 2: Height-Correlated Smith (more accurate, directly returns the visibility term)
float visibilitySmith(float NdotV, float NdotL, float roughness) {
float a2 = roughness * roughness;
float gv = NdotL * sqrt(NdotV * (NdotV - NdotV * a2) + a2);
float gl = NdotV * sqrt(NdotL * (NdotL - NdotL * a2) + a2);
return 0.5 / max(gv + gl, 0.00001);
}
// Method 3: Simplified approximation
float G1V(float dotNV, float k) {
return 1.0 / (dotNV * (1.0 - k) + k);
}
// Usage: float vis = G1V(NdotL, k) * G1V(NdotV, k); where k = roughness/2
```
### Step 7: Assembling Cook-Torrance BRDF
```glsl
vec3 cookTorranceBRDF(vec3 N, vec3 V, vec3 L, float roughness, vec3 F0) {
vec3 H = normalize(V + L);
float NdotL = max(0.0, dot(N, L));
float NdotV = max(0.0, dot(N, V));
float NdotH = max(0.0, dot(N, H));
float VdotH = max(0.0, dot(V, H));
float D = distributionGGX(NdotH, roughness);
vec3 F = fresnelSchlick(F0, VdotH);
float Vis = visibilitySmith(NdotV, NdotL, roughness);
// Vis version already includes the 4*NdotV*NdotL denominator
vec3 specular = D * F * Vis;
// Or with standard G term: specular = (D * F * G) / max(4.0 * NdotV * NdotL, 0.001);
return specular * NdotL;
}
```
### Step 8: Multi-Light Accumulation and Compositing
```glsl
vec3 shade(vec3 pos, vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) {
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 diffuseColor = albedo * (1.0 - metallic); // metals have no diffuse
vec3 color = vec3(0.0);
// primary light (sun)
vec3 sunDir = normalize(vec3(0.6, 0.8, -0.5));
vec3 sunColor = vec3(1.0, 0.95, 0.85) * 2.0;
vec3 H = normalize(V + sunDir);
float NdotL = max(0.0, dot(N, sunDir));
float NdotV = max(0.0, dot(N, V));
float VdotH = max(0.0, dot(V, H));
vec3 F = fresnelSchlick(F0, VdotH);
vec3 kD = (1.0 - F) * (1.0 - metallic); // energy conservation
color += kD * diffuseColor / PI * sunColor * NdotL;
color += cookTorranceBRDF(N, V, sunDir, roughness, F0) * sunColor;
// sky light (hemisphere approximation)
vec3 skyColor = vec3(0.2, 0.5, 1.0) * 0.3;
float skyDiffuse = 0.5 + 0.5 * N.y;
color += diffuseColor * skyColor * skyDiffuse;
// back light / rim light
vec3 backDir = normalize(vec3(-sunDir.x, 0.0, -sunDir.z));
float backDiffuse = clamp(dot(N, backDir) * 0.5 + 0.5, 0.0, 1.0);
color += diffuseColor * vec3(0.25, 0.15, 0.1) * backDiffuse;
return color;
}
```
### Step 9: Ambient Occlusion (AO)
```glsl
// Raymarching AO (using SDF queries)
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(pos + h * nor);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
float ao = calcAO(pos, N);
diffuseLight *= ao;
// specular AO (more subtle):
specularLight *= clamp(pow(NdotV + ao, roughness * roughness) - 1.0 + ao, 0.0, 1.0);
```
### Outdoor Three-Light Model
The go-to lighting setup for outdoor SDF scenes. Uses three directional sources to approximate full global illumination with minimal cost:
```glsl
// === Outdoor Three-Light Lighting ===
// Compute material, occlusion, and shadow first
vec3 material = getMaterial(pos, nor); // albedo, keep ≤ 0.2 for realism
float occ = calcAO(pos, nor); // ambient occlusion
float sha = calcSoftShadow(pos, sunDir, 0.02, 8.0);
// Three light contributions
float sun = clamp(dot(nor, sunDir), 0.0, 1.0); // direct sunlight
float sky = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0); // hemisphere sky light
float ind = clamp(dot(nor, normalize(sunDir * vec3(-1.0, 0.0, -1.0))), 0.0, 1.0); // indirect bounce
// Combine with colored shadows (key technique: shadow penumbra tints blue)
vec3 lin = vec3(0.0);
lin += sun * vec3(1.64, 1.27, 0.99) * pow(vec3(sha), vec3(1.0, 1.2, 1.5)); // warm sun, colored shadow
lin += sky * vec3(0.16, 0.20, 0.28) * occ; // cool sky fill
lin += ind * vec3(0.40, 0.28, 0.20) * occ; // warm ground bounce
vec3 color = material * lin;
```
Key principles:
- **Colored shadow penumbra**: `pow(vec3(sha), vec3(1.0, 1.2, 1.5))` makes shadow edges slightly blue/cool, mimicking real subsurface scattering in penumbra regions
- **Material albedo rule**: Keep diffuse albedo ≤ 0.2; adjust light intensities for brightness, not material values. Real-world surfaces rarely exceed 0.3 albedo
- **Linear workflow**: All computations in linear space, apply gamma `pow(color, vec3(1.0/2.2))` at the very end
- **Sky light approximation**: `0.5 + 0.5 * nor.y` is a cheap hemisphere integral — surfaces pointing up get full sky, pointing down get none
- Do NOT apply ambient occlusion to the sun/key light — shadows handle that
## Complete Code Template
```glsl
// Lighting Model Complete Template - Runs directly in ShaderToy
// Progressive implementation from Lambert to Cook-Torrance PBR
#define PI 3.14159265359
// ========== Adjustable Parameters ==========
#define ROUGHNESS 0.35
#define METALLIC 0.0
#define ALBEDO vec3(0.8, 0.2, 0.2)
#define SUN_DIR normalize(vec3(0.6, 0.8, -0.5))
#define SUN_COLOR vec3(1.0, 0.95, 0.85) * 2.0
#define SKY_COLOR vec3(0.2, 0.5, 1.0) * 0.4
#define BACKGROUND_TOP vec3(0.5, 0.7, 1.0)
#define BACKGROUND_BOT vec3(0.8, 0.85, 0.9)
// ========== SDF Scene ==========
float map(vec3 p) {
float sphere = length(p - vec3(0.0, 0.0, 0.0)) - 1.0;
float ground = p.y + 1.0;
return min(sphere, ground);
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
// ========== AO ==========
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0;
float sca = 1.0;
for (int i = 0; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(pos + h * nor);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
// ========== Soft Shadow ==========
float softShadow(vec3 ro, vec3 rd, float mint, float maxt) {
float res = 1.0;
float t = mint;
for (int i = 0; i < 24; i++) {
float h = map(ro + rd * t);
res = min(res, 8.0 * h / t);
t += clamp(h, 0.02, 0.2);
if (res < 0.001 || t > maxt) break;
}
return clamp(res, 0.0, 1.0);
}
// ========== PBR BRDF Components ==========
float D_GGX(float NdotH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
vec3 F_Schlick(vec3 F0, float cosTheta) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
float V_SmithGGX(float NdotV, float NdotL, float roughness) {
float a2 = roughness * roughness;
a2 *= a2;
float gv = NdotL * sqrt(NdotV * NdotV * (1.0 - a2) + a2);
float gl = NdotV * sqrt(NdotL * NdotL * (1.0 - a2) + a2);
return 0.5 / max(gv + gl, 1e-5);
}
// ========== Complete Lighting ==========
vec3 shade(vec3 pos, vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) {
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 diffuseColor = albedo * (1.0 - metallic);
float NdotV = max(dot(N, V), 1e-4);
float ao = calcAO(pos, N);
vec3 color = vec3(0.0);
// sunlight
{
vec3 L = SUN_DIR;
vec3 H = normalize(V + L);
float NdotL = max(dot(N, L), 0.0);
float NdotH = max(dot(N, H), 0.0);
float VdotH = max(dot(V, H), 0.0);
float D = D_GGX(NdotH, roughness);
vec3 F = F_Schlick(F0, VdotH);
float Vis = V_SmithGGX(NdotV, NdotL, roughness);
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec3 diffuse = kD * diffuseColor / PI;
vec3 specular = D * F * Vis;
float shadow = softShadow(pos, L, 0.02, 5.0);
color += (diffuse + specular) * SUN_COLOR * NdotL * shadow;
}
// sky light (hemisphere approximation)
{
float skyDiff = 0.5 + 0.5 * N.y;
color += diffuseColor * SKY_COLOR * skyDiff * ao;
}
// back light / rim light
{
vec3 backDir = normalize(vec3(-SUN_DIR.x, 0.0, -SUN_DIR.z));
float backDiff = clamp(dot(N, backDir) * 0.5 + 0.5, 0.0, 1.0);
color += diffuseColor * vec3(0.15, 0.1, 0.08) * backDiff * ao;
}
// environment reflection (simplified)
{
vec3 R = reflect(-V, N);
vec3 envColor = mix(BACKGROUND_BOT, BACKGROUND_TOP, clamp(R.y * 0.5 + 0.5, 0.0, 1.0));
vec3 F_env = F_Schlick(F0, NdotV);
float envOcc = clamp(pow(NdotV + ao, roughness * roughness) - 1.0 + ao, 0.0, 1.0);
color += F_env * envColor * envOcc * (1.0 - roughness * 0.7);
}
return color;
}
// ========== Raymarching ==========
float raymarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < 128; i++) {
float d = map(ro + rd * t);
if (d < 0.001) return t;
t += d;
if (t > 50.0) break;
}
return -1.0;
}
// ========== Background ==========
vec3 background(vec3 rd) {
vec3 col = mix(BACKGROUND_BOT, BACKGROUND_TOP, clamp(rd.y * 0.5 + 0.5, 0.0, 1.0));
float sun = clamp(dot(rd, SUN_DIR), 0.0, 1.0);
col += SUN_COLOR * 0.3 * pow(sun, 8.0);
col += SUN_COLOR * 1.0 * pow(sun, 256.0);
return col;
}
// ========== Main Function ==========
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float angle = iTime * 0.3;
vec3 ro = vec3(3.0 * cos(angle), 1.5, 3.0 * sin(angle));
vec3 ta = vec3(0.0, 0.0, 0.0);
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = cross(uu, ww);
vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.5 * ww);
vec3 col = background(rd);
float t = raymarch(ro, rd);
if (t > 0.0) {
vec3 pos = ro + t * rd;
vec3 N = calcNormal(pos);
vec3 V = -rd;
vec3 albedo = ALBEDO;
float roughness = ROUGHNESS;
float metallic = METALLIC;
if (pos.y < -0.99) {
roughness = 0.8;
metallic = 0.0;
float checker = mod(floor(pos.x) + floor(pos.z), 2.0);
albedo = mix(vec3(0.3), vec3(0.6), checker);
}
col = shade(pos, N, V, albedo, roughness, metallic);
}
col = col / (col + vec3(1.0)); // Tone mapping (Reinhard)
col = pow(col, vec3(1.0 / 2.2)); // Gamma
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Classic Phong (Non-PBR)
```glsl
vec3 R = reflect(-L, N);
float spec = pow(max(0.0, dot(R, V)), 32.0);
vec3 color = albedo * lightColor * NdotL + lightColor * spec;
```
### Variant 2: Point Light Attenuation
```glsl
float dist = length(lightPos - pos);
float attenuation = 1.0 / (1.0 + dist * 0.1 + dist * dist * 0.01);
color *= attenuation;
```
### Variant 3: IBL (Image-Based Lighting)
```glsl
// diffuse IBL: spherical harmonics
vec3 diffuseIBL = diffuseColor * SHIrradiance(N);
// specular IBL: EnvBRDFApprox
vec3 EnvBRDFApprox(vec3 specColor, float roughness, float NdotV) {
vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022);
vec4 c1 = vec4(1, 0.0425, 1.04, -0.04);
vec4 r = roughness * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y;
vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
return specColor * AB.x + AB.y;
}
vec3 R = reflect(-V, N);
vec3 envColor = textureLod(envMap, R, roughness * 7.0).rgb;
vec3 specularIBL = EnvBRDFApprox(F0, roughness, NdotV) * envColor;
```
### Variant 4: Subsurface Scattering Approximation (SSS)
```glsl
// SDF-based interior probing
float subsurface(vec3 pos, vec3 L) {
float sss = 0.0;
for (int i = 0; i < 5; i++) {
float h = 0.05 + float(i) * 0.1;
float d = map(pos + L * h);
sss += max(0.0, h - d);
}
return clamp(1.0 - sss * 4.0, 0.0, 1.0);
}
// Henyey-Greenstein phase function
float HenyeyGreenstein(float cosTheta, float g) {
float g2 = g * g;
return (1.0 - g2) / (pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5) * 4.0 * PI);
}
float sssAmount = HenyeyGreenstein(dot(V, L), 0.5);
color += sssColor * sssAmount * NdotL;
```
### Variant 5: Beer's Law Water Lighting
```glsl
vec3 waterExtinction(float depth) {
float opticalDepth = depth * 6.0;
vec3 extinctColor = 1.0 - vec3(0.5, 0.4, 0.1);
return exp2(-opticalDepth * extinctColor);
}
vec3 underwaterColor = objectColor * waterExtinction(depth);
vec3 inscatter = waterDiffuse * (1.0 - exp(-depth * 0.1));
underwaterColor += inscatter;
```
## Performance & Composition
- **Fresnel optimization**: Use `x2*x2*x` instead of `pow(x, 5.0)`
- **Visibility term**: Use `V_SmithGGX` to directly return `G/(4*NdotV*NdotL)`, avoiding separate division
- **AO sampling**: 5 samples is sufficient; can reduce to 3 at far distances
- **Soft shadow**: `clamp(h, 0.02, 0.2)` limits step size; 14~24 steps usually sufficient; `8.0*h/t` controls softness
- **Simplified IBL**: Without cubemap, approximate with `mix(groundColor, skyColor, R.y*0.5+0.5)`
- **Branch culling**: Skip specular calculation when `NdotL <= 0`
- **Raymarching integration**: Use SDF finite differences for normals, query SDF directly for AO/shadows
- **Volume rendering integration**: Beer's Law attenuation + Henyey-Greenstein phase function; FBM noise procedural normals can be passed directly to lighting functions
- **Post-processing integration**: ACES `(col*(2.51*col+0.03))/(col*(2.43*col+0.59)+0.14)` / Reinhard `col/(col+1)` + Gamma
- **Reflection integration**: `reflect(rd, N)` to query scene again, blend result with Fresnel weighting
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/lighting-model.md)

View File

@@ -0,0 +1,455 @@
# Matrix Transforms & Camera
## Use Cases
- Camera systems in 3D scenes (orbit camera, fly camera, path camera)
- SDF object domain transforms via translation, rotation, and scale matrices
- Generating 3D rays from screen pixels (perspective / orthographic projection)
- Hierarchical rotation transforms for joint animation
- Rotation in noise domain warping, IFS fractal iterations
## Core Principles
The essence of matrix transforms is coordinate system transformation. In a ray marching pipeline:
1. **Camera matrix**: Screen pixels → world-space ray direction (view-to-world)
2. **Object transform matrix**: World-space sample point → object local space (world-to-local, domain transform)
### Key Formulas
**2D Rotation** R(θ) = `[[cosθ, -sinθ], [sinθ, cosθ]]`
**3D Rotation Around Y-Axis** Ry(θ) = `[[cosθ, 0, sinθ], [0, 1, 0], [-sinθ, 0, cosθ]]`
**Rodrigues (Arbitrary Axis k, Angle θ)**: `R = cosθ·I + (1-cosθ)·k⊗k + sinθ·K`
**LookAt Camera**:
```
forward = normalize(target - eye)
right = normalize(cross(forward, worldUp))
up = cross(right, forward)
viewMatrix = mat3(right, up, forward)
```
**Perspective Ray**: `rd = normalize(camMatrix * vec3(uv, focalLength))`
## Implementation Steps
### Step 1: Screen Coordinate Normalization
```glsl
// Range [-aspect, aspect] x [-1, 1]
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
```
### Step 2: Rotation Matrices
```glsl
// 2D rotation (mat2)
mat2 rot2D(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c);
}
// 3D single-axis rotation (mat3)
mat3 rotX(float a) {
float s = sin(a), c = cos(a);
return mat3(1, 0, 0, 0, c, s, 0, -s, c);
}
mat3 rotY(float a) {
float s = sin(a), c = cos(a);
return mat3(c, 0, s, 0, 1, 0, -s, 0, c);
}
mat3 rotZ(float a) {
float s = sin(a), c = cos(a);
return mat3(c, s, 0, -s, c, 0, 0, 0, 1);
}
// Euler angles → mat3 (yaw/pitch/roll)
mat3 fromEuler(vec3 ang) {
vec2 a1 = vec2(sin(ang.x), cos(ang.x));
vec2 a2 = vec2(sin(ang.y), cos(ang.y));
vec2 a3 = vec2(sin(ang.z), cos(ang.z));
mat3 m;
m[0] = vec3( a1.y*a3.y + a1.x*a2.x*a3.x,
a1.y*a2.x*a3.x + a3.y*a1.x,
-a2.y*a3.x);
m[1] = vec3(-a2.y*a1.x, a1.y*a2.y, a2.x);
m[2] = vec3( a3.y*a1.x*a2.x + a1.y*a3.x,
a1.x*a3.x - a1.y*a3.y*a2.x,
a2.y*a3.y);
return m;
}
// Rodrigues arbitrary-axis rotation (mat3)
mat3 rotationMatrix(vec3 axis, float angle) {
axis = normalize(axis);
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
return mat3(
oc*axis.x*axis.x + c, oc*axis.x*axis.y - axis.z*s, oc*axis.z*axis.x + axis.y*s,
oc*axis.x*axis.y + axis.z*s, oc*axis.y*axis.y + c, oc*axis.y*axis.z - axis.x*s,
oc*axis.z*axis.x - axis.y*s, oc*axis.y*axis.z + axis.x*s, oc*axis.z*axis.z + c
);
}
```
### Step 3: LookAt Camera
```glsl
// Classic setCamera, cr = camera roll
mat3 setCamera(in vec3 ro, in vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = normalize(cross(cu, cw));
return mat3(cu, cv, cw);
}
// mat4 LookAt (with translation, for homogeneous coordinate scenes)
mat4 LookAt(vec3 pos, vec3 target, vec3 up) {
vec3 dir = normalize(target - pos);
vec3 x = normalize(cross(dir, up));
vec3 y = cross(x, dir);
return mat4(vec4(x, 0), vec4(y, 0), vec4(dir, 0), vec4(pos, 1));
}
```
### Step 4: Perspective Ray Generation
```glsl
// mat3 camera — focalLength controls FOV: 1.0≈90°, 2.0≈53°, 4.0≈28°
#define FOCAL_LENGTH 2.0
mat3 cam = setCamera(ro, ta, 0.0);
vec3 rd = cam * normalize(vec3(uv, FOCAL_LENGTH));
// Manual basis vector composition
#define FOV 1.0
vec3 rd = normalize(camDir + (uv.x * camRight + uv.y * camUp) * FOV);
// mat4 homogeneous coordinates
mat4 viewToWorld = LookAt(camPos, camTarget, camUp);
vec3 rd = (viewToWorld * normalize(vec4(uv, 1.0, 0.0))).xyz;
```
### Step 5: Mouse-Interactive Camera
```glsl
// Spherical coordinate orbit camera
#define CAM_DIST 5.0
#define CAM_HEIGHT 1.0
vec2 mouse = iMouse.xy / iResolution.xy;
float angleH = mouse.x * 6.2832;
float angleV = mouse.y * 3.1416 - 1.5708;
if (iMouse.z <= 0.0) {
angleH = iTime * 0.5;
angleV = 0.3;
}
vec3 ro = vec3(
CAM_DIST * cos(angleH) * cos(angleV),
CAM_DIST * sin(angleV) + CAM_HEIGHT,
CAM_DIST * sin(angleH) * cos(angleV)
);
vec3 ta = vec3(0.0);
```
### Step 6: SDF Domain Transforms
```glsl
// Translation
float d = sdSphere(p - vec3(2.0, 0.0, 0.0), 1.0);
// Rotation (orthogonal matrix inverse = transpose)
float d = sdBox(rotY(0.5) * p, vec3(1.0));
// Scale (divide by scale factor, multiply back into distance)
#define SCALE 2.0
float d = sdSphere(p / SCALE, 1.0) * SCALE;
// mat4 SRT composition
mat4 Loc4(vec3 d) {
d *= -1.0;
return mat4(1,0,0,d.x, 0,1,0,d.y, 0,0,1,d.z, 0,0,0,1);
}
mat4 transposeM4(in mat4 m) {
return mat4(
vec4(m[0].x, m[1].x, m[2].x, m[3].x),
vec4(m[0].y, m[1].y, m[2].y, m[3].y),
vec4(m[0].z, m[1].z, m[2].z, m[3].z),
vec4(m[0].w, m[1].w, m[2].w, m[3].w));
}
vec3 opTx(vec3 p, mat4 m) {
return (transposeM4(m) * vec4(p, 1.0)).xyz;
}
// First translate to (3,0,0), then rotate 45° around Y-axis
mat4 xform = Rot4Y(0.785) * Loc4(vec3(3.0, 0.0, 0.0));
float d = sdBox(opTx(p, xform), vec3(1.0));
```
### Step 7: Quaternion Rotation
```glsl
vec4 axisAngleToQuat(vec3 axis, float angleDeg) {
float half_angle = angleDeg * 3.14159265 / 360.0;
vec2 sc = sin(vec2(half_angle, half_angle + 1.5707963));
return vec4(normalize(axis) * sc.x, sc.y);
}
vec3 quatRotate(vec3 pos, vec3 axis, float angleDeg) {
vec4 q = axisAngleToQuat(axis, angleDeg);
return pos + 2.0 * cross(q.xyz, cross(q.xyz, pos) + q.w * pos);
}
// Hierarchical rotation in joint animation
vec3 limbPos = quatRotate(p - shoulderOffset, vec3(1,0,0), swingAngle);
float d = sdEllipsoid(limbPos, limbSize);
```
## Complete Code Template
Can be run directly in ShaderToy, demonstrating LookAt camera + multi-object domain transforms + mouse interaction.
```glsl
// === Matrix Transforms & Camera - Complete Template ===
#define PI 3.14159265
#define MAX_STEPS 128
#define MAX_DIST 50.0
#define SURF_DIST 0.001
#define FOCAL_LENGTH 2.0
#define CAM_DIST 6.0
#define AUTO_SPEED 0.4
// ---------- Rotation Matrix Utilities ----------
mat2 rot2D(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c);
}
mat3 rotX(float a) {
float s = sin(a), c = cos(a);
return mat3(1,0,0, 0,c,s, 0,-s,c);
}
mat3 rotY(float a) {
float s = sin(a), c = cos(a);
return mat3(c,0,s, 0,1,0, -s,0,c);
}
mat3 rotZ(float a) {
float s = sin(a), c = cos(a);
return mat3(c,s,0, -s,c,0, 0,0,1);
}
mat3 rotAxis(vec3 axis, float angle) {
axis = normalize(axis);
float s = sin(angle), c = cos(angle), oc = 1.0 - c;
return mat3(
oc*axis.x*axis.x+c, oc*axis.x*axis.y-axis.z*s, oc*axis.z*axis.x+axis.y*s,
oc*axis.x*axis.y+axis.z*s, oc*axis.y*axis.y+c, oc*axis.y*axis.z-axis.x*s,
oc*axis.z*axis.x-axis.y*s, oc*axis.y*axis.z+axis.x*s, oc*axis.z*axis.z+c
);
}
// ---------- LookAt Camera ----------
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = normalize(cross(cu, cw));
return mat3(cu, cv, cw);
}
// ---------- SDF Primitives ----------
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
// ---------- Scene (Domain Transform Demo) ----------
float map(vec3 p) {
float d = p.y + 1.0; // Ground plane
// Static sphere
d = min(d, sdSphere(p, 0.5));
// Rotating box (spinning around Y-axis)
vec3 p2 = p - vec3(2.5, 0.0, 0.0);
p2 = rotY(iTime * 0.8) * p2;
d = min(d, sdBox(p2, vec3(0.6)));
// Arbitrary-axis rotating torus
vec3 p3 = p - vec3(-2.5, 0.5, 0.0);
p3 = rotAxis(vec3(1,1,0), iTime * 0.6) * p3;
d = min(d, sdTorus(p3, vec2(0.6, 0.2)));
// Scaled + rotated sphere
vec3 p4 = p - vec3(0.0, 0.5, 2.5);
p4 = rotZ(iTime * 1.2) * rotX(iTime * 0.7) * p4;
float scale = 1.5;
d = min(d, sdSphere(p4 / scale, 0.4) * scale);
return d;
}
// ---------- Normal ----------
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
// ---------- Ray March ----------
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if (d < SURF_DIST) break;
t += d;
if (t > MAX_DIST) break;
}
return t;
}
// ---------- Main Function ----------
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// Mouse-interactive orbit camera
float angleH, angleV;
if (iMouse.z > 0.0) {
vec2 m = iMouse.xy / iResolution.xy;
angleH = m.x * 2.0 * PI;
angleV = (m.y - 0.5) * PI;
} else {
angleH = iTime * AUTO_SPEED;
angleV = 0.35;
}
vec3 ro = vec3(
CAM_DIST * cos(angleH) * cos(angleV),
CAM_DIST * sin(angleV) + 1.0,
CAM_DIST * sin(angleH) * cos(angleV)
);
vec3 ta = vec3(0.0);
mat3 cam = setCamera(ro, ta, 0.0);
vec3 rd = cam * normalize(vec3(uv, FOCAL_LENGTH));
float t = rayMarch(ro, rd);
vec3 col = vec3(0.0);
if (t < MAX_DIST) {
vec3 p = ro + rd * t;
vec3 n = calcNormal(p);
vec3 lightDir = normalize(vec3(1.0, 2.0, -1.0));
float diff = max(dot(n, lightDir), 0.0);
col = vec3(0.8, 0.85, 0.9) * (diff + 0.15);
if (p.y < -0.99) {
float checker = mod(floor(p.x) + floor(p.z), 2.0);
col *= 0.5 + 0.3 * checker;
}
} else {
col = vec3(0.4, 0.6, 0.9) - rd.y * 0.3;
}
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Orthographic Projection Camera
```glsl
#define ORTHO_SIZE 5.0
mat3 cam = setCamera(ro, ta, 0.0);
vec3 rd = cam * vec3(0.0, 0.0, 1.0); // Fixed direction
ro += cam * vec3(uv * ORTHO_SIZE, 0.0); // Offset origin
```
### Euler Angle Full Rotation Camera
```glsl
vec3 ang = vec3(pitch, yaw, roll);
mat3 rot = fromEuler(ang);
vec3 ori = vec3(0.0, 0.0, 3.0) * rot;
vec3 rd = normalize(vec3(uv, -2.0)) * rot;
```
### Quaternion Joint Rotation
```glsl
vec3 legP = quatRotate(p - hipOffset, vec3(1,0,0), legAngle);
float dLeg = sdEllipsoid(legP, vec3(0.2, 0.6, 0.25));
```
### mat4 SRT Pipeline
```glsl
mat4 Rot4Y(float a) {
float c = cos(a), s = sin(a);
return mat4(c,0,s,0, 0,1,0,0, -s,0,c,0, 0,0,0,1);
}
mat4 xform = Rot4Y(angle) * Loc4(vec3(3.0, 0.0, 0.0));
float d = sdBox(opTx(p, xform), boxSize);
```
### Path Camera (Animated Flight)
```glsl
vec2 pathCenter(float z) {
return vec2(sin(z * 0.17) * 3.0, sin(z * 0.1 + 4.0) * 2.0);
}
float z_offset = iTime * 10.0;
vec3 camPos = vec3(pathCenter(z_offset), 0.0);
vec3 camTarget = vec3(pathCenter(z_offset + 5.0), 5.0);
mat4 viewToWorld = LookAt(camPos, camTarget, camUp);
vec3 rd = (viewToWorld * normalize(vec4(uv, 1.0, 0.0))).xyz;
```
## Performance & Composition
**Performance**:
- Compute `sin/cos` of the same angle only once: `vec2 sc = sin(vec2(a, a + 1.5707963));`
- Use `mat3` instead of `mat4` for pure rotation (saves 7 multiply-adds)
- Inverse of orthogonal rotation matrix = transpose; use `transpose(m)` or `v * m`
- Pre-compute matrices that don't depend on `p` outside `map()`
- Pre-multiply multiple rotations into a single matrix
**Composition**:
- **SDF / Ray Marching**: Camera generates rays + domain transforms place objects (fundamental pipeline)
- **Noise / fBm**: Rotate sampling coordinates to break axis-aligned regularity `fbm(rot * p)`
- **Fractals / IFS**: Embed rotation in iterations to create complex geometry
- **Lighting**: Normal transform for pure rotation matrices is the same as vertex transform
- **Post-Processing**: FOV for depth of field; `mat2` for chromatic aberration/motion blur direction
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/matrix-transform.md)

View File

@@ -0,0 +1,922 @@
### Standalone HTML Complete Shader Template (Must Be Strictly Followed)
**IMPORTANT: The following template can be copied directly; every line must be strictly followed**:
**Vertex Shader** (common to all shaders):
```glsl
#version 300 es
in vec4 iPosition;
void main() {
gl_Position = iPosition;
}
```
**Fragment Shader Buffer A Example** (particle physics simulation):
```glsl
#version 300 es
precision highp float;
// IMPORTANT: Critical: uniforms must be declared; ShaderToy's iTime/iResolution etc. are global variables
uniform float iTime;
uniform vec2 iResolution;
uniform int iFrame;
uniform vec4 iMouse;
// IMPORTANT: Critical: mainImage parameters need manual extraction
// ShaderToy: void mainImage(out vec4 fragColor, in vec2 fragCoord)
// Adapted to:
out vec4 fragColor;
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = fragCoord / iResolution;
// IMPORTANT: Critical: texture2D → texture
vec4 prev = texture(iChannel0, uv);
// ... particle physics logic ...
fragColor = vec4(pos, vel);
}
```
**Fragment Shader Image Example**:
```glsl
#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform int iFrame;
uniform vec4 iMouse;
uniform sampler2D iChannel0;
out vec4 fragColor;
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 uv = fragCoord / iResolution;
// IMPORTANT: Critical: texture2D → texture, mainImage → standard main
vec4 col = texture(iChannel0, uv);
// Rendering logic
col = col / (1.0 + col); // Tone mapping
fragColor = col;
}
```
**IMPORTANT: Common GLSL ES 3.00 Errors** (must be avoided):
1. **#version must be on the first line** - Any comments/blank lines will cause "version directive must occur on the first line" error
2. **in/out qualifiers** - WebGL1's attribute/varying must be changed to in/out in ES3
3. **texture function** - ES3 uses `texture(sampler, uv)`, not `texture2D(sampler, uv)`
4. **Type strictness** - `vec4 = float` is illegal, must use `vec4(v, v, v, v)` or `vec4(v)` or `vec4(vec3(v), 1.0)`
## Standalone HTML Multi-Channel Framebuffer Implementation
**IMPORTANT: Multi-Channel Rendering Pipeline Core Pitfalls**: ShaderToy code requires manual Framebuffer rendering pipeline implementation. The following template demonstrates the correct approach:
```javascript
// Correct multi-channel Framebuffer creation
const NUM_BUFFERS = 2; // Buffer A, Buffer B
const buffers = [];
const textures = [];
// Check float texture linear filtering extension
const ext = gl.getExtension('EXT_color_buffer_float');
const floatLinear = gl.getExtension('OES_texture_float_linear');
// Each Buffer needs an independent Framebuffer + texture
for (let i = 0; i < NUM_BUFFERS; i++) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// IMPORTANT: Critical: Must use UNSIGNED_BYTE format without EXT_color_buffer_float extension!
// RGBA16F/RGBA32F require the extension, otherwise GL_INVALID_OPERATION
// Float textures need EXT_color_buffer_float; RGBA16F supports HDR data
if (ext) {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.FLOAT, null);
} else {
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
}
// IMPORTANT: Critical: Texture parameters must be set, otherwise GL_INVALID_FRAMEBUFFER
// IMPORTANT: Float textures use NEAREST, or require OES_texture_float_linear extension for LINEAR
// IMPORTANT: Critical: Float textures must use CLAMP_TO_EDGE wrap mode; REPEAT is not supported for float textures
// IMPORTANT: Critical: Must fall back to UNSIGNED_BYTE format without EXT_color_buffer_float extension
const filterMode = (ext && floatLinear) ? gl.LINEAR : gl.NEAREST;
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode);
// IMPORTANT: Must use CLAMP_TO_EDGE: float textures do not support REPEAT
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
// IMPORTANT: Critical: Check Framebuffer completeness
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
console.error("Framebuffer incomplete:", status);
}
textures.push(texture);
buffers.push(fbo);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Render loop: render to Buffer first, then render to screen
function render() {
// 1. Render to Buffer A (self-feedback reads previous Buffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]);
gl.viewport(0, 0, width, height);
// Bind previous frame texture to iChannel0
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[1]); // Read from other Buffer
// Set uniforms etc...
// Execute shader rendering
// 2. Swap Buffers (simulate self-feedback)
// IMPORTANT: Critical: Must swap textures for next frame reading; FBO handles remain unchanged
[textures[0], textures[1]] = [textures[1], textures[0]];
// 3. Render to screen
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Bind Buffer result to texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[0]);
// Execute Image pass shader
}
```
**IMPORTANT: Common Errors** (JavaScript/WebGL side):
1. **Missing texture parameters** - Must set `TEXTURE_MIN_FILTER`, `TEXTURE_MAG_FILTER`, `TEXTURE_WRAP_S/T`
2. **Missing Framebuffer completeness check** - `gl.checkFramebufferStatus()` must return `FRAMEBUFFER_COMPLETE` before use
3. **Float texture extension** - `gl.RGBA16F` requires `EXT_color_buffer_float` extension, otherwise fall back to `gl.UNSIGNED_BYTE`
4. **Buffer ping-pong error** - Self-feedback must use 2 independent FBOs alternating read/write; a single FBO + texture swap causes "Feedback loop" error
5. **Particle system empty texture initialization** - Textures are empty before the first frame; shaders reading default values cause render failure — must execute initPass() to pre-render
# Multi-Pass Buffer Techniques
## Use Cases
When single-frame computation cannot achieve the desired effect and cross-frame data persistence or multi-stage processing pipelines are needed, use multi-pass buffers:
- **Temporal accumulation**: Motion blur, TAA, progressive rendering
- **Physics simulation**: Fluids, reaction-diffusion, particle systems
- **Persistent state**: Game state, particle positions/velocities, interaction history
- **Deferred rendering**: G-Buffer → post-processing → compositing
- **Post-processing chains**: HDR Bloom (downsample → blur → composite)
- **Iterative solvers**: Poisson solver, vorticity confinement, multi-scale computation
## Core Principles
Multi-pass buffers split the rendering pipeline into multiple Buffers, each outputting a texture as input for the next stage.
### Self-Feedback
A Buffer reads its own previous frame output, achieving cross-frame state persistence: `x(n+1) = f(x(n))`
```
Buffer A (frame N) reads → Buffer A (frame N-1) output
```
### Pipeline Chaining
Multiple Buffers process in sequence:
```
Buffer A (geometry) → Buffer B (blur H) → Buffer C (blur V) → Image (compositing)
```
### Structured Data Storage
Specific pixels serve as data registers, read precisely via `texelFetch`:
```
texel (0,0) = ball position+velocity (vec4)
texel (1,0) = paddle position
texel (x,1)-(x,12) = brick grid state
```
### Key Mathematical Patterns
- **Fluid self-advection**: `newPos = texture(buf, uv - dt * velocity * texelSize)`
- **Gaussian blur**: `sum += texture(buf, uv + offset_i) * weight_i`
- **Temporal blending**: `result = mix(newFrame, prevFrame, blendWeight)`
- **Vorticity confinement**: `vortForce = curl × normalize(gradient(|curl|))`
## Implementation Steps
### Step 1: Minimal Self-Feedback Loop
Buffer A (iChannel0 → Buffer A self-feedback):
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec4 prev = texture(iChannel0, uv);
// New content: procedural noise contour lines
float n = noise(vec3(uv * 8.0, 0.1 * iTime));
float v = sin(6.2832 * 10.0 * n);
v = smoothstep(1.0, 0.0, 0.5 * abs(v) / fwidth(v));
vec4 newContent = 0.5 + 0.5 * sin(12.0 * n + vec4(0, 2.1, -2.1, 0));
// Decay + offset blending
vec4 decayed = exp(-33.0 / iResolution.y) * texture(iChannel0, (fragCoord + vec2(1.0, sin(iTime))) / iResolution.xy);
fragColor = mix(decayed, newContent, v);
// Initialization guard
if (iFrame < 4) fragColor = vec4(0.5);
}
```
Image (iChannel0 → Buffer A):
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
fragColor = texture(iChannel0, fragCoord / iResolution.xy);
}
```
### Step 2: Fluid Self-Advection
Buffer A (iChannel0 → Buffer A self-feedback):
```glsl
#define ROT_NUM 5
#define SCALE_NUM 20
const float ang = 6.2832 / float(ROT_NUM);
mat2 m = mat2(cos(ang), sin(ang), -sin(ang), cos(ang));
float getRot(vec2 pos, vec2 b) {
vec2 p = b;
float rot = 0.0;
for (int i = 0; i < ROT_NUM; i++) {
rot += dot(texture(iChannel0, fract((pos + p) / iResolution.xy)).xy - vec2(0.5),
p.yx * vec2(1, -1));
p = m * p;
}
return rot / float(ROT_NUM) / dot(b, b);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 pos = fragCoord;
float rnd = fract(sin(float(iFrame) * 12.9898) * 43758.5453);
vec2 b = vec2(cos(ang * rnd), sin(ang * rnd));
// Multi-scale rotation sampling
vec2 v = vec2(0);
float bbMax = 0.7 * iResolution.y;
bbMax *= bbMax;
for (int l = 0; l < SCALE_NUM; l++) {
if (dot(b, b) > bbMax) break;
vec2 p = b;
for (int i = 0; i < ROT_NUM; i++) {
v += p.yx * getRot(pos + p, b);
p = m * p;
}
b *= 2.0;
}
// Self-advection
fragColor = texture(iChannel0, fract((pos + v * vec2(-1, 1) * 2.0) / iResolution.xy));
// Center driving force
vec2 scr = (fragCoord / iResolution.xy) * 2.0 - 1.0;
fragColor.xy += 0.01 * scr / (dot(scr, scr) / 0.1 + 0.3);
if (iFrame <= 4) fragColor = texture(iChannel1, fragCoord / iResolution.xy);
}
```
### Step 3-4: Navier-Stokes Solver + Chained Acceleration
Buffer A / B / C use identical code (via Common tab's `solveFluid`):
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 w = 1.0 / iResolution.xy;
vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0);
vec4 data = solveFluid(iChannel0, uv, w, iTime, iMouse.xyz, lastMouse.xyz);
if (iFrame < 20) data = vec4(0.5, 0, 0, 0);
if (fragCoord.y < 1.0) data = iMouse; // Mouse state storage
fragColor = data;
}
```
iChannel bindings: A→C(prev frame), B→A, C→B — 3 iterations per frame.
### Step 5: Separable Gaussian Blur
Buffer B (horizontal, iChannel0 → source Buffer) — Buffer C vertical direction is analogous, using y-axis offset:
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 pixelSize = 1.0 / iResolution.xy;
vec2 uv = fragCoord * pixelSize;
float h = pixelSize.x;
vec4 sum = vec4(0.0);
// 9-tap Gaussian (sigma ≈ 2.0)
sum += texture(iChannel0, fract(vec2(uv.x - 4.0*h, uv.y))) * 0.05;
sum += texture(iChannel0, fract(vec2(uv.x - 3.0*h, uv.y))) * 0.09;
sum += texture(iChannel0, fract(vec2(uv.x - 2.0*h, uv.y))) * 0.12;
sum += texture(iChannel0, fract(vec2(uv.x - 1.0*h, uv.y))) * 0.15;
sum += texture(iChannel0, fract(vec2(uv.x, uv.y))) * 0.16;
sum += texture(iChannel0, fract(vec2(uv.x + 1.0*h, uv.y))) * 0.15;
sum += texture(iChannel0, fract(vec2(uv.x + 2.0*h, uv.y))) * 0.12;
sum += texture(iChannel0, fract(vec2(uv.x + 3.0*h, uv.y))) * 0.09;
sum += texture(iChannel0, fract(vec2(uv.x + 4.0*h, uv.y))) * 0.05;
fragColor = vec4(sum.xyz / 0.98, 1.0);
}
```
### Step 6: Structured State Storage
```glsl
// Register address definitions
const ivec2 txBallPosVel = ivec2(0, 0);
const ivec2 txPaddlePos = ivec2(1, 0);
const ivec2 txPoints = ivec2(2, 0);
const ivec2 txState = ivec2(3, 0);
const ivec4 txBricks = ivec4(0, 1, 13, 12);
vec4 loadValue(ivec2 addr) {
return texelFetch(iChannel0, addr, 0);
}
void storeValue(ivec2 addr, vec4 val, inout vec4 fragColor, ivec2 currentPixel) {
fragColor = (currentPixel == addr) ? val : fragColor;
}
void storeValue(ivec4 rect, vec4 val, inout vec4 fragColor, ivec2 currentPixel) {
fragColor = (currentPixel.x >= rect.x && currentPixel.y >= rect.y &&
currentPixel.x <= rect.z && currentPixel.y <= rect.w) ? val : fragColor;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
ivec2 px = ivec2(fragCoord - 0.5);
if (fragCoord.x > 14.0 || fragCoord.y > 14.0) discard;
vec4 ballPosVel = loadValue(txBallPosVel);
float paddlePos = loadValue(txPaddlePos).x;
float points = loadValue(txPoints).x;
if (iFrame == 0) {
ballPosVel = vec4(0.0, -0.8, 0.6, 1.0);
paddlePos = 0.0;
points = 0.0;
}
// ... game logic update ...
fragColor = loadValue(px);
storeValue(txBallPosVel, ballPosVel, fragColor, px);
storeValue(txPaddlePos, vec4(paddlePos, 0, 0, 0), fragColor, px);
storeValue(txPoints, vec4(points, 0, 0, 0), fragColor, px);
}
```
### Step 7: Mouse State Inter-Frame Tracking
```glsl
// Method 1: First-row pixel storage
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 w = 1.0 / iResolution.xy;
vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0);
// ... simulation logic ...
if (fragCoord.y < 1.0) fragColor = iMouse;
}
// Method 2: Fixed UV region storage
vec2 mouseDelta() {
vec2 pixelSize = 1.0 / iResolution.xy;
float eighth = 1.0 / 8.0;
vec4 oldMouse = texture(iChannel2, vec2(7.5 * eighth, 2.5 * eighth));
vec4 nowMouse = vec4(iMouse.xy / iResolution.xy, iMouse.zw / iResolution.xy);
if (oldMouse.z > pixelSize.x && oldMouse.w > pixelSize.y &&
nowMouse.z > pixelSize.x && nowMouse.w > pixelSize.y) {
return nowMouse.xy - oldMouse.xy;
}
return vec2(0.0);
}
```
## Complete Code Template
A fully runnable fluid simulation shader (self-feedback + vorticity confinement + mouse interaction + color advection).
### Common tab
```glsl
#define DT 0.15
#define VORTICITY_AMOUNT 0.11
#define VISCOSITY 0.55
#define PRESSURE_K 0.2
#define FORCE_RADIUS 0.001
#define FORCE_STRENGTH 0.001
#define VELOCITY_DECAY 1e-4
float mag2(vec2 p) { return dot(p, p); }
vec2 emitter1(float t) { t *= 0.62; return vec2(0.12, 0.5 + sin(t) * 0.2); }
vec2 emitter2(float t) { t *= 0.62; return vec2(0.88, 0.5 + cos(t + 1.5708) * 0.2); }
vec4 solveFluid(sampler2D smp, vec2 uv, vec2 w, float time, vec3 mouse, vec3 lastMouse) {
vec4 data = textureLod(smp, uv, 0.0);
vec4 tr = textureLod(smp, uv + vec2(w.x, 0), 0.0);
vec4 tl = textureLod(smp, uv - vec2(w.x, 0), 0.0);
vec4 tu = textureLod(smp, uv + vec2(0, w.y), 0.0);
vec4 td = textureLod(smp, uv - vec2(0, w.y), 0.0);
vec3 dx = (tr.xyz - tl.xyz) * 0.5;
vec3 dy = (tu.xyz - td.xyz) * 0.5;
vec2 densDif = vec2(dx.z, dy.z);
data.z -= DT * dot(vec3(densDif, dx.x + dy.y), data.xyz);
vec2 laplacian = tu.xy + td.xy + tr.xy + tl.xy - 4.0 * data.xy;
vec2 viscForce = vec2(VISCOSITY) * laplacian;
data.xyw = textureLod(smp, uv - DT * data.xy * w, 0.0).xyw;
vec2 newForce = vec2(0);
newForce += 0.75 * vec2(0.0003, 0.00015) / (mag2(uv - emitter1(time)) + 0.0001);
newForce -= 0.75 * vec2(0.0003, 0.00015) / (mag2(uv - emitter2(time)) + 0.0001);
if (mouse.z > 1.0 && lastMouse.z > 1.0) {
vec2 vv = clamp((mouse.xy * w - lastMouse.xy * w) * 400.0, -6.0, 6.0);
newForce += FORCE_STRENGTH / (mag2(uv - mouse.xy * w) + FORCE_RADIUS) * vv;
}
data.xy += DT * (viscForce - PRESSURE_K / DT * densDif + newForce);
data.xy = max(vec2(0), abs(data.xy) - VELOCITY_DECAY) * sign(data.xy);
data.w = (tr.y - tl.y - tu.x + td.x);
vec2 vort = vec2(abs(tu.w) - abs(td.w), abs(tl.w) - abs(tr.w));
vort *= VORTICITY_AMOUNT / length(vort + 1e-9) * data.w;
data.xy += vort;
data.y *= smoothstep(0.5, 0.48, abs(uv.y - 0.5));
data = clamp(data, vec4(vec2(-10), 0.5, -10.0), vec4(vec2(10), 3.0, 10.0));
return data;
}
```
### Buffer A / B / C (Fluid Sub-Steps 1/2/3)
iChannel bindings: A←C(prev frame), B←A, C←B
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 w = 1.0 / iResolution.xy;
vec4 lastMouse = texelFetch(iChannel0, ivec2(0, 0), 0);
vec4 data = solveFluid(iChannel0, uv, w, iTime, iMouse.xyz, lastMouse.xyz);
if (iFrame < 20) data = vec4(0.5, 0, 0, 0);
if (fragCoord.y < 1.0) data = iMouse;
fragColor = data;
}
```
### Buffer D (Color Advection, iChannel0 → Buffer C, iChannel1 → Buffer D self-feedback)
```glsl
#define COLOR_DECAY 0.004
#define COLOR_ADVECT_SCALE 3.0
vec3 getPalette(float x, vec3 c1, vec3 c2, vec3 p1, vec3 p2) {
float x2 = fract(x / 2.0);
x = fract(x);
mat3 m = mat3(c1, p1, c2);
mat3 m2 = mat3(c2, p2, c1);
float omx = 1.0 - x;
vec3 pws = vec3(omx * omx, 2.0 * omx * x, x * x);
return clamp(mix(m * pws, m2 * pws, step(x2, 0.5)), 0.0, 1.0);
}
vec4 palette1(float x) {
return vec4(getPalette(-x, vec3(0.2, 0.5, 0.7), vec3(0.9, 0.4, 0.1),
vec3(1.0, 1.2, 0.5), vec3(1.0, -0.4, 0.0)), 1.0);
}
vec4 palette2(float x) {
return vec4(getPalette(-x, vec3(0.4, 0.3, 0.5), vec3(0.9, 0.75, 0.4),
vec3(0.1, 0.8, 1.3), vec3(1.25, -0.1, 0.1)), 1.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 w = 1.0 / iResolution.xy;
vec2 velo = textureLod(iChannel0, uv, 0.0).xy;
vec4 col = textureLod(iChannel1, uv - DT * velo * w * COLOR_ADVECT_SCALE, 0.0);
vec2 mo = iMouse.xy / iResolution.xy;
vec4 lastMouse = texelFetch(iChannel1, ivec2(0, 0), 0);
if (iMouse.z > 1.0 && lastMouse.z > 1.0) {
float str = smoothstep(-0.5, 1.0, length(mo - lastMouse.xy / iResolution.xy));
col += str * 0.0009 / (pow(length(uv - mo), 1.7) + 0.002) * palette2(-iTime * 0.7);
}
col += 0.0025 / (0.0005 + pow(length(uv - emitter1(iTime)), 1.75)) * DT * 0.12 * palette1(iTime * 0.05);
col += 0.0025 / (0.0005 + pow(length(uv - emitter2(iTime)), 1.75)) * DT * 0.12 * palette2(iTime * 0.05 + 0.675);
if (iFrame < 20) col = vec4(0.0);
col = clamp(col, 0.0, 5.0);
col = max(col - (0.0001 + col * COLOR_DECAY) * 0.5, 0.0);
if (fragCoord.y < 1.0 && fragCoord.x < 1.0) col = iMouse;
fragColor = col;
}
```
### Image (iChannel0 → Buffer D)
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec4 col = textureLod(iChannel0, fragCoord / iResolution.xy, 0.0);
if (fragCoord.y < 1.0 || fragCoord.y >= iResolution.y - 1.0) col = vec4(0);
fragColor = col;
}
```
## Common Variants
### Variant 1: TAA Temporal Accumulation Anti-Aliasing
```glsl
// Buffer A: Sub-pixel jittered rendering
vec2 jitter = vec2(rand(uv + sin(iTime)), rand(uv + 1.0 + sin(iTime))) / iResolution.xy;
vec3 eyevec = normalize(vec3(((uv + jitter) * 2.0 - 1.0) * vec2(aspect, 1.0), fov));
float blendWeight = 0.9;
color = mix(color, texture(iChannel_self, uv).rgb, blendWeight);
// Buffer C (TAA): YCoCg neighborhood clamping to prevent ghosting
vec3 newYCC = RGBToYCoCg(newFrame);
vec3 histYCC = RGBToYCoCg(history);
vec3 colorAvg = ...; vec3 colorVar = ...;
vec3 sigma = sqrt(max(vec3(0), colorVar - colorAvg * colorAvg));
histYCC = clamp(histYCC, colorAvg - 0.75 * sigma, colorAvg + 0.75 * sigma);
result = YCoCgToRGB(mix(newYCC, histYCC, 0.95));
```
### Variant 2: Deferred Rendering G-Buffer
```glsl
// Buffer A: G-Buffer output
col.xy = (normal * camMat * 0.5 + 0.5).xy; // Normal
col.z = 1.0 - abs((t * rd) * camMat).z / DMAX; // Depth
col.w = dot(lightDir, nor) * 0.5 + 0.5; // Diffuse
// Buffer B: Edge detection
float checkSame(vec4 center, vec4 sample) {
vec2 diffNormal = abs(center.xy - sample.xy) * Sensitivity.x;
float diffDepth = abs(center.z - sample.z) * Sensitivity.y;
return (diffNormal.x + diffNormal.y < 0.1 && diffDepth < 0.1) ? 1.0 : 0.0;
}
```
### Variant 3: HDR Bloom
```glsl
// Buffer B: MIP pyramid (multi-level downsampling packed into one texture)
vec2 CalcOffset(float octave) {
vec2 offset = vec2(0);
vec2 padding = vec2(10.0) / iResolution.xy;
offset.x = -min(1.0, floor(octave / 3.0)) * (0.25 + padding.x);
offset.y = -(1.0 - 1.0 / exp2(octave)) - padding.y * octave;
offset.y += min(1.0, floor(octave / 3.0)) * 0.35;
return offset;
}
// Image: Accumulate multi-level bloom + Reinhard tone mapping
bloom += Grab(coord, 1.0, CalcOffset(0.0)) * 1.0;
bloom += Grab(coord, 2.0, CalcOffset(1.0)) * 1.5;
color = pow(color, vec3(1.5));
color = color / (1.0 + color);
```
### Variant 4: Reaction-Diffusion System
```glsl
// Buffer A: Gray-Scott reaction-diffusion
vec2 uv_red = uv + vec2(dx.x, dy.x) * pixelSize * 8.0;
float new_val = texture(iChannel0, fract(uv_red)).x;
new_val += (noise.x - 0.5) * 0.0025 - 0.002;
new_val -= (texture(iChannel_blur, fract(uv_red)).x -
texture(iChannel_self, fract(uv_red)).x) * 0.047;
```
### Variant 5: Multi-Scale MIP Fluid
```glsl
for (int i = 0; i < NUM_SCALES; i++) {
float mip = float(i);
float stride = float(1 << i);
vec4 t = stride * vec4(texel, -texel.y, 0);
vec2 d = textureLod(sampler, fract(uv + t.ww), mip).xy;
float w = WEIGHT_FUNCTION;
result += w * computation(neighbors);
}
```
### Variant 6: Particle System (Position-Velocity Storage)
**IMPORTANT: Particle System Implementation Key**: Particle state is stored in texture pixels, one particle per pixel. Rendering must iterate over the particle texture for sampling.
**Buffer A (Particle Physics Simulation)**:
```glsl
// Each texture pixel stores one particle: xy=position, zw=velocity
// IMPORTANT: Critical: hash function must return vec2! Returning float causes type mismatch errors
vec2 hash2(vec2 p) {
return fract(sin(mat2(127.1, 311.7, 269.5, 183.3) * p) * 43758.5453);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec4 prev = texture(iChannel0, uv);
vec2 pos = prev.xy;
vec2 vel = prev.zw;
// IMPORTANT: Initialization guard: use integer comparison + pixel-coordinate-based random (avoids particle overlap when time is too small)
if (iFrame < 3) {
// Use fragCoord (pixel coordinates) to ensure each particle has a unique position, independent of time
// IMPORTANT: Critical: hash2 returns vec2, assign directly to pos/vel
pos = hash2(fragCoord * 0.01 + vec2(1.7, 9.3));
vel = (hash2(fragCoord * 0.01 + vec2(5.3, 2.8)) - 0.5) * 0.02;
fragColor = vec4(pos, vel);
return;
}
// Physics update
vel *= 0.98; // Damping
// Mouse interaction
vec2 mouse = iMouse.xy / iResolution.xy;
if (iMouse.z > 0.0) {
vec2 toMouse = mouse - pos;
vel += normalize(toMouse + 0.001) * 0.0005 / (length(toMouse) + 0.1);
}
// Motion
pos += vel * 60.0 * 0.016;
pos = fract(pos); // Boundary wrapping
fragColor = vec4(pos, vel);
}
```
**Image (Render Particles)**:
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 w = 1.0 / iResolution.xy;
vec3 color = vec3(0.02, 0.02, 0.05); // Dark background
// Iterate over particle texture for sampling (performance-sensitive, balance sample count)
float glow = 0.0;
for (float y = 0.0; y < 1.0; y += 0.02) { // IMPORTANT: Step size determines sampling density
for (float x = 0.0; x < 1.0; x += 0.02) {
vec4 particle = texture(iChannel0, vec2(x, y));
vec2 pPos = particle.xy;
float dist = length(uv - pPos);
float size = 0.01 + length(particle.zw) * 0.3;
glow += exp(-dist * dist / (size * size)) * 0.15;
}
}
// Particle glow
color += vec3(0.3, 0.6, 1.0) * glow;
// Vignette
color *= 1.0 - length(uv - 0.5) * 0.8;
// Tone mapping
color = color / (1.0 + color);
fragColor = vec4(color, 1.0);
}
```
**Key Points**:
- Buffer A self-feedback: iChannel0 → Buffer A
- Image reads: iChannel0 → Buffer A (particle state)
- Step size 0.02 produces 2500 samples; adjust based on performance
- Particle size varies with velocity: `size = 0.01 + length(vel) * 0.3`
**Complete JavaScript Rendering Pipeline (Particle System 3-Pass)**:
```javascript
// Particle system needs 4 Framebuffers (2 each for Buffer A and Buffer B ping-pong) + screen output
// Buffer A: Particle physics (self-feedback) - uses FBO 0/1 ping-pong
// Buffer B: Density accumulation (reads Buffer A) - uses FBO 2/3 ping-pong
// Image: Final rendering (reads Buffer A + Buffer B)
// IMPORTANT: Critical: Must use 2 FBOs for ping-pong! Single FBO + texture swap causes
// "Feedback loop formed between Framebuffer and active Texture" error
const buffers = [null, null, null, null]; // [A_FBO0, A_FBO1, B_FBO0, B_FBO1]
const textures = [null, null, null, null]; // [A_tex0, A_tex1, B_tex0, B_tex1]
function createBuffers() {
// Buffer A: 2 FBOs for ping-pong
for (let i = 0; i < 2; i++) {
const tex = createTexture();
textures[i] = tex;
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
buffers[i] = fbo;
}
// Buffer B: 2 FBOs for ping-pong
for (let i = 0; i < 2; i++) {
const tex = createTexture();
textures[2 + i] = tex;
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
buffers[2 + i] = fbo;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
// IMPORTANT: Critical: Initialization pre-rendering - must execute before the first frame!
// Empty textures cause particle initialization failure (reading 0,0,0,0 makes all particles overlap)
let aReadIdx = 0; // Current read FBO index (0 or 1)
let bReadIdx = 0; // Buffer B current read FBO index (0 or 1)
function initPass() {
// ===== Buffer A Initialization =====
// Render first frame using FBO 0
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]);
gl.viewport(0, 0, width, height);
gl.useProgram(programBufferA);
setupAttribute(programBufferA);
// Bind FBO 1's texture as input (not yet rendered, but avoids binding errors)
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[1]);
gl.uniform1i(gl.getUniformLocation(programBufferA, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(programBufferA, 'iResolution'), width, height);
gl.uniform1f(gl.getUniformLocation(programBufferA, 'iTime'), 0);
gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 0);
gl.uniform4f(gl.getUniformLocation(programBufferA, 'iMouse'), 0, 0, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Render second frame using FBO 1 (iFrame=1)
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[1]);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[0]); // Read FBO 0's result
gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 1);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Render one more frame to ensure initialization is complete
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[0]);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[1]);
gl.uniform1i(gl.getUniformLocation(programBufferA, 'iFrame'), 2);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// ===== Buffer B Initialization =====
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[2]); // B_FBO0
gl.viewport(0, 0, width, height);
gl.useProgram(programBufferB);
setupAttribute(programBufferB);
// Bind latest Buffer A result (FBO 0's result)
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[0]);
gl.uniform1i(gl.getUniformLocation(programBufferB, 'iChannel0'), 0);
// Bind Buffer B previous frame (FBO 3's texture, not yet rendered)
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures[3]);
gl.uniform1i(gl.getUniformLocation(programBufferB, 'iChannel1'), 1);
gl.uniform2f(gl.getUniformLocation(programBufferB, 'iResolution'), width, height);
gl.uniform1f(gl.getUniformLocation(programBufferB, 'iTime'), 0);
gl.uniform1i(gl.getUniformLocation(programBufferB, 'iFrame'), 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Buffer B second frame
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[3]); // B_FBO1
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[1]); // Buffer A latest
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures[2]); // Buffer B FBO0 result
gl.uniform1i(gl.getUniformLocation(programBufferB, 'iFrame'), 1);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Initialize ping-pong indices
aReadIdx = 0; // Next frame reads FBO 0
bReadIdx = 0; // Next frame reads FBO 2
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
function render() {
// ===== Pass 1: Buffer A (Particle Physics Self-Feedback) =====
// aReadIdx = 0: read FBO 0, write FBO 1
// aReadIdx = 1: read FBO 1, write FBO 0
const aWriteIdx = 1 - aReadIdx;
// Write to target FBO (not the current read FBO)
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[aWriteIdx]);
gl.viewport(0, 0, width, height);
// Read previous frame Buffer A texture (from current read FBO's texture)
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]);
gl.uniform1i(uniformsBufferA.iChannel0, 0);
gl.uniform2f(uniformsBufferA.iResolution, width, height);
gl.uniform1f(uniformsBufferA.iTime, time);
gl.uniform1i(uniformsBufferA.iFrame, frameCount);
gl.uniform4f(uniformsBufferA.iMouse, mouse.x, mouse.y, mouse.z, mouse.w);
// Render particle physics
gl.useProgram(programBufferA);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Switch read index
aReadIdx = aWriteIdx;
// ===== Pass 2: Buffer B (Density Field) =====
const bWriteIdx = 1 - bReadIdx;
gl.bindFramebuffer(gl.FRAMEBUFFER, buffers[2 + bWriteIdx]); // B_FBO0 or B_FBO1
gl.viewport(0, 0, width, height);
// Bind current Buffer A particle state (use latest Buffer A result)
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]); // A latest result
gl.uniform1i(uniformsBufferB.iChannel0, 0);
// Bind previous frame Buffer B density (for accumulation)
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures[2 + bReadIdx]); // B_read
gl.uniform1i(uniformsBufferB.iChannel1, 1);
gl.uniform2f(uniformsBufferB.iResolution, width, height);
gl.uniform1f(uniformsBufferB.iTime, time);
gl.uniform1i(uniformsBufferB.iFrame, frameCount);
// Render density accumulation
gl.useProgram(programBufferB);
gl.drawArrays(gl.TRIANGLES, 0, 6);
// Switch Buffer B read index
bReadIdx = bWriteIdx;
// ===== Pass 3: Image (Final Rendering to Screen) =====
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, width, height);
// Bind Buffer A particles (use latest Buffer A result)
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, textures[aReadIdx]);
gl.uniform1i(uniformsImage.iChannel0, 0);
// Bind Buffer B density (use latest Buffer B result)
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, textures[2 + bReadIdx]);
gl.uniform1i(uniformsImage.iChannel1, 1);
gl.uniform2f(uniformsImage.iResolution, width, height);
gl.uniform1f(uniformsImage.iTime, time);
gl.uniform1i(uniformsImage.iFrame, frameCount);
gl.uniform4f(uniformsImage.iMouse, mouse.x, mouse.y, mouse.z, mouse.w);
// Render to screen
gl.useProgram(programImage);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
```
**IMPORTANT: Key Points**:
- **Must use 2 FBOs for ping-pong**: Each Buffer needs two independent FBOs (read FBO + write FBO); a single FBO + texture swap causes "Feedback loop" error
- Use FBO index switching (not texture swapping): bind target FBO when writing, bind source texture when reading
- Image pass binds the latest Buffer results (obtained via read index)
## Performance & Composition
**Performance Optimization**:
- Separable blur: N² → 2N samples
- Bilinear tap trick: 5 samples replace 9-tap Gaussian
- MIP sampling replaces large kernels: `textureLod` at high MIP levels ≈ large-range average
- `discard` outside data regions to skip unnecessary computation
- RGBA channel packing: velocity(xy) + density(z) + curl(w) in one vec4
- Chained sub-steps: A→B→C same code for 3x simulation speed
- `if (dot(b,b) > bbMax) break;` adaptive early exit
- `iFrame < 20` progressive initialization to prevent explosion
**Typical Composition Patterns**:
- **Fluid + Lighting**: Fluid buffer → Image computes gradient normals → diffuse + specular
- **Fluid + Color Advection**: Separate Buffer tracks color field, advected by velocity field
- **Scene + Bloom + TAA**: 4-Buffer pipeline (render → downsample → blur → composite tone mapping)
- **G-Buffer + Screen-Space Effects**: 2-Buffer without temporal feedback (geometry → edge/SSAO/SSR → stylized compositing)
- **State Storage + Visualization Separation**: Buffer A pure logic + Image pure rendering (`texelFetch` reads state + distance field drawing)
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/multipass-buffer.md)

View File

@@ -0,0 +1,318 @@
## WebGL2 Adaptation Requirements
**IMPORTANT: GLSL Type Strictness Warning**:
- GLSL is a strongly typed language with **no `string` type**; using string types is forbidden
- Common illegal types: `string`, `int` (can only use `int` literals, cannot declare variable types as `int`)
- vec2/vec3/vec4 cannot be implicitly converted between each other; explicit construction is required
- Float precision: `highp float` (recommended), `mediump float`, `lowp float`
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
- Use `canvas.getContext("webgl2")`
- Shader first line: `#version 300 es`, add `precision highp float;` in fragment shader
- Vertex shader: `attribute` -> `in`, `varying` -> `out`
- Fragment shader: `varying` -> `in`, `gl_FragColor` -> custom `out vec4 fragColor`, `texture2D()` -> `texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to the standard `void main()` entry point
# SDF Normal Estimation
## Use Cases
- Lighting calculations in raymarching rendering pipelines (diffuse, specular, Fresnel, etc.)
- Any 3D scene based on SDF distance fields (fractals, parametric surfaces, boolean geometry, procedural terrain)
- Edge detection and contour rendering (Laplacian value as a byproduct of normal sampling)
- Prerequisite for ambient occlusion (AO) computation
## Core Principles
The gradient of an SDF `nabla f(p)` points in the direction of fastest distance increase, which is the outward surface normal. Numerical differentiation approximates the gradient:
$$\vec{n} = \text{normalize}\left(\nabla f(p)\right)$$
Three main strategies:
| Method | Samples | Accuracy | Recommendation |
|--------|---------|----------|----------------|
| Forward difference | 4 | O(epsilon) | Simple scenes |
| Central difference | 6 | O(epsilon^2) | When symmetry is needed |
| **Tetrahedron method** | **4** | **Between the two** | **Preferred** |
Key parameter epsilon: commonly `0.0005 ~ 0.001`; for advanced scenes, multiply by ray distance `t` for adaptive scaling.
## Implementation Steps
### Step 1: Define SDF Scene Function
```glsl
float map(vec3 p) {
float d = length(p) - 1.0; // unit sphere
return d;
}
```
### Step 2: Choose Differentiation Method
#### Method A: Forward Difference -- 4 Samples
```glsl
const float EPSILON = 1e-3;
vec3 getNormal(vec3 p) {
vec3 n;
n.x = map(vec3(p.x + EPSILON, p.y, p.z));
n.y = map(vec3(p.x, p.y + EPSILON, p.z));
n.z = map(vec3(p.x, p.y, p.z + EPSILON));
return normalize(n - map(p));
}
```
#### Method B: Central Difference -- 6 Samples
```glsl
vec3 getNormal(vec3 p) {
vec2 o = vec2(0.001, 0.0);
return normalize(vec3(
map(p + o.xyy) - map(p - o.xyy),
map(p + o.yxy) - map(p - o.yxy),
map(p + o.yyx) - map(p - o.yyx)
));
}
```
#### Method C: Tetrahedron Method -- 4 Samples (Recommended)
```glsl
// Classic tetrahedron method, coefficient 0.5773 ~ 1/sqrt(3)
vec3 calcNormal(vec3 pos) {
float eps = 0.0005;
vec2 e = vec2(1.0, -1.0) * 0.5773;
return normalize(
e.xyy * map(pos + e.xyy * eps) +
e.yyx * map(pos + e.yyx * eps) +
e.yxy * map(pos + e.yxy * eps) +
e.xxx * map(pos + e.xxx * eps)
);
}
```
### Step 3: Apply to Lighting
```glsl
vec3 pos = ro + rd * t; // hit point
vec3 nor = calcNormal(pos); // surface normal
vec3 lightDir = normalize(vec3(1.0, 4.0, -4.0));
float diff = max(dot(nor, lightDir), 0.0);
vec3 col = vec3(0.8) * diff;
```
## Complete Code Template
```glsl
// SDF Normal Estimation — Complete ShaderToy Template
#define MAX_STEPS 128
#define MAX_DIST 100.0
#define SURF_DIST 0.001
#define NORMAL_METHOD 2 // 0=forward diff, 1=central diff, 2=tetrahedron
// ---- SDF Scene Definition ----
float map(vec3 p) {
float sphere = length(p - vec3(0.0, 1.0, 0.0)) - 1.0;
float ground = p.y;
return min(sphere, ground);
}
// ---- Normal Estimation ----
vec3 normalForward(vec3 p) {
float eps = 0.001;
float d = map(p);
return normalize(vec3(
map(p + vec3(eps, 0.0, 0.0)),
map(p + vec3(0.0, eps, 0.0)),
map(p + vec3(0.0, 0.0, eps))
) - d);
}
vec3 normalCentral(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
vec3 normalTetra(vec3 p) {
float eps = 0.0005;
vec2 e = vec2(1.0, -1.0) * 0.5773;
return normalize(
e.xyy * map(p + e.xyy * eps) +
e.yyx * map(p + e.yyx * eps) +
e.yxy * map(p + e.yxy * eps) +
e.xxx * map(p + e.xxx * eps)
);
}
vec3 calcNormal(vec3 p) {
#if NORMAL_METHOD == 0
return normalForward(p);
#elif NORMAL_METHOD == 1
return normalCentral(p);
#else
return normalTetra(p);
#endif
}
// ---- Raymarching ----
float raymarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + rd * t;
float d = map(p);
if (d < SURF_DIST || t > MAX_DIST) break;
t += d;
}
return t;
}
// ---- Main Function ----
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
vec3 ro = vec3(0.0, 2.0, -5.0);
vec3 rd = normalize(vec3(uv, 1.5));
float t = raymarch(ro, rd);
vec3 col = vec3(0.0);
if (t < MAX_DIST) {
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
vec3 sunDir = normalize(vec3(0.8, 0.4, -0.6));
float diff = clamp(dot(nor, sunDir), 0.0, 1.0);
float amb = 0.5 + 0.5 * nor.y;
vec3 ref = reflect(rd, nor);
float spec = pow(clamp(dot(ref, sunDir), 0.0, 1.0), 16.0);
col = vec3(0.18) * amb + vec3(1.0, 0.95, 0.85) * diff + vec3(0.5) * spec;
} else {
col = vec3(0.5, 0.7, 1.0) - 0.5 * rd.y;
}
col = pow(col, vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: NuSan Reverse-Offset Forward Difference
```glsl
// Reverse-offset forward difference
vec2 noff = vec2(0.001, 0.0);
vec3 normal = normalize(
map(pos) - vec3(
map(pos - noff.xyy),
map(pos - noff.yxy),
map(pos - noff.yyx)
)
);
```
### Variant 2: Adaptive Epsilon (Distance Scaling)
```glsl
// Adaptive epsilon based on ray distance
vec3 calcNormal(vec3 pos, float t) {
float precis = 0.001 * t;
vec2 e = vec2(1.0, -1.0) * precis;
return normalize(
e.xyy * map(pos + e.xyy) +
e.yyx * map(pos + e.yyx) +
e.yxy * map(pos + e.yxy) +
e.xxx * map(pos + e.xxx)
);
}
```
### Variant 3: Large Epsilon for Rounding / Anti-Aliasing
```glsl
// Large epsilon for rounding / anti-aliasing
vec3 getNormal(vec3 p) {
vec2 e = vec2(0.015, -0.015); // intentionally large epsilon
return normalize(
e.xyy * map(p + e.xyy) +
e.yyx * map(p + e.yyx) +
e.yxy * map(p + e.yxy) +
e.xxx * map(p + e.xxx)
);
}
```
### Variant 4: Anti-Inlining Loop
```glsl
// Anti-inlining loop — reduces compile time for complex SDFs
#define ZERO (min(iFrame, 0))
vec3 calcNormal(vec3 p, float t) {
vec3 n = vec3(0.0);
for (int i = ZERO; i < 4; i++) {
vec3 e = 0.5773 * (2.0 * vec3(
(((i + 3) >> 1) & 1),
((i >> 1) & 1),
(i & 1)
) - 1.0);
n += e * map(p + e * 0.001 * t);
}
return normalize(n);
}
```
### Variant 5: Normal + Edge Detection
```glsl
// Central difference + Laplacian edge detection
float edge = 0.0;
vec3 normal(vec3 p) {
vec3 e = vec3(0.0, det * 5.0, 0.0);
float d1 = de(p - e.yxx), d2 = de(p + e.yxx);
float d3 = de(p - e.xyx), d4 = de(p + e.xyx);
float d5 = de(p - e.xxy), d6 = de(p + e.xxy);
float d = de(p);
edge = abs(d - 0.5 * (d2 + d1))
+ abs(d - 0.5 * (d4 + d3))
+ abs(d - 0.5 * (d6 + d5));
edge = min(1.0, pow(edge, 0.55) * 15.0);
return normalize(vec3(d1 - d2, d3 - d4, d5 - d6));
}
```
## Performance & Composition
**Performance**:
- Default to tetrahedron method (4 samples, better accuracy than forward difference)
- Only switch to central difference (6 samples) when jagged normal artifacts appear
- Use anti-inlining loop (Variant 4) for complex SDFs to avoid compile time explosion
- Epsilon recommended `0.0005 ~ 0.001`; best practice is adaptive `eps * t`
- Too small (< 1e-5) produces floating-point noise; too large (> 0.05) loses detail
- Reuse SDF sampling results when multiple types of information are needed at the same position (e.g., Variant 5)
**Common combinations**:
- **Normal + Soft Shadow**: `calcSoftShadow(pos + nor * 0.01, sunDir, 16.0)` -- normal offset at start point to avoid self-intersection
- **Normal + AO**: Multi-step SDF sampling along the normal to estimate occlusion
- **Normal + Fresnel**: `pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0)`
- **Normal + Bump Mapping**: Overlay texture gradient perturbation on SDF normals
- **Normal + Triplanar Mapping**: Use `abs(nor)` components as triplanar blend weights
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/normal-estimation.md)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,623 @@
Path tracing requires multi-pass rendering: Buffer A traces and accumulates samples each frame (iChannel0=self), Image Pass reads accumulated data and applies tone mapping for display. Below is the JS skeleton for standalone HTML:
### Standalone HTML Multi-Pass Template (Ping-Pong Accumulation)
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Path Tracer</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
let frameCount = 0;
let mouse = [0, 0, 0, 0];
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2');
const ext = gl.getExtension('EXT_color_buffer_float');
function createShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error(gl.getShaderInfoLog(s));
return s;
}
function createProgram(vsSrc, fsSrc) {
const p = gl.createProgram();
gl.attachShader(p, createShader(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, createShader(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(p);
return p;
}
const vsSource = `#version 300 es
in vec2 pos;
void main(){ gl_Position=vec4(pos,0,1); }`;
// fsBuffer: path tracing + accumulation (see "Complete Code Template - Buffer A" below)
// fsImage: ACES tone mapping + gamma (see "Complete Code Template - Image Pass" below)
const progBuf = createProgram(vsSource, fsBuffer);
const progImg = createProgram(vsSource, fsImage);
function createFBO(w, h) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
// Key: check float texture extension, fall back to RGBA8 if not supported
// Path tracing accumulation needs high precision, but RGBA8 works too (with slight banding)
const fmt = ext ? gl.RGBA16F : gl.RGBA;
const typ = ext ? gl.FLOAT : gl.UNSIGNED_BYTE;
gl.texImage2D(gl.TEXTURE_2D, 0, fmt, w, h, 0, gl.RGBA, typ, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { fbo, tex };
}
let W, H, bufA, bufB;
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function resize() {
canvas.width = W = innerWidth;
canvas.height = H = innerHeight;
bufA = createFBO(W, H);
bufB = createFBO(W, H);
frameCount = 0;
}
addEventListener('resize', resize);
resize();
canvas.addEventListener('mousedown', e => { mouse[2] = e.clientX; mouse[3] = H - e.clientY; });
canvas.addEventListener('mouseup', () => { mouse[2] = 0; mouse[3] = 0; });
canvas.addEventListener('mousemove', e => { mouse[0] = e.clientX; mouse[1] = H - e.clientY; });
function render(t) {
t *= 0.001;
// Buffer pass: read bufA (previous frame accumulation) -> write bufB (current frame accumulation)
gl.useProgram(progBuf);
gl.bindFramebuffer(gl.FRAMEBUFFER, bufB.fbo);
gl.viewport(0, 0, W, H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, bufA.tex);
gl.uniform1i(gl.getUniformLocation(progBuf, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(progBuf, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progBuf, 'iTime'), t);
gl.uniform1i(gl.getUniformLocation(progBuf, 'iFrame'), frameCount);
gl.uniform4f(gl.getUniformLocation(progBuf, 'iMouse'), ...mouse);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
[bufA, bufB] = [bufB, bufA];
// Image pass: read bufA (accumulated result) -> screen (tone mapped)
gl.useProgram(progImg);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, W, H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, bufA.tex);
gl.uniform1i(gl.getUniformLocation(progImg, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(progImg, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progImg, 'iTime'), t);
gl.uniform1i(gl.getUniformLocation(progImg, 'iFrame'), frameCount);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
frameCount++;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
```
# Path Tracing & Global Illumination
## Use Cases
- Physically accurate global illumination: indirect lighting, color bleeding, caustics
- Complex light transport with reflection, refraction, and diffuse interreflection
- Progressive high-quality rendering with multi-frame accumulation in ShaderToy
- Scenes requiring precise light interactions such as Cornell Box and glassware
## Core Principles
Path tracing solves the rendering equation via Monte Carlo methods. For each pixel, a ray is cast from the camera and bounced through the scene; at each bounce: intersect -> shade -> sample next direction -> accumulate contribution.
Core formulas:
- **Rendering equation**: $L_o = L_e + \int f_r \cdot L_i \cdot \cos\theta \, d\omega$
- **MC estimate**: $L \approx \frac{1}{N} \sum \frac{f_r \cdot L_i \cdot \cos\theta}{p(\omega)}$
- **Schlick Fresnel**: $F = F_0 + (1 - F_0)(1 - \cos\theta)^5$
- **Cosine-weighted PDF**: $p(\omega) = \cos\theta / \pi$
Use iterative loops instead of recursion: `acc` (accumulated radiance) and `throughput` (path attenuation) track path contributions.
## Implementation Steps
### Step 1: PRNG
```glsl
// Integer hash (recommended, good quality)
int iSeed;
int irand() { iSeed = iSeed * 0x343fd + 0x269ec3; return (iSeed >> 16) & 32767; }
float frand() { return float(irand()) / 32767.0; }
void srand(ivec2 p, int frame) {
int n = frame;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
n += p.y;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
n += p.x;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
iSeed = n;
}
// Alternative: sin-hash (simpler)
float seed;
float rand() { return fract(sin(seed++) * 43758.5453123); }
```
### Step 2: Ray-Scene Intersection
```glsl
// Analytic sphere intersection
struct Ray { vec3 o, d; };
struct Sphere { float r; vec3 p, e, c; int refl; };
float iSphere(Sphere s, Ray r) {
vec3 op = s.p - r.o;
float b = dot(op, r.d);
float det = b * b - dot(op, op) + s.r * s.r;
if (det < 0.) return 0.;
det = sqrt(det);
float t = b - det;
if (t > 1e-3) return t;
t = b + det;
return t > 1e-3 ? t : 0.;
}
// SDF ray marching (complex geometry)
float map(vec3 p) { /* return distance to nearest surface */ }
float raymarch(vec3 ro, vec3 rd, float tmax) {
float t = 0.01;
for (int i = 0; i < 256; i++) {
float h = map(ro + rd * t);
if (abs(h) < 0.0001 || t > tmax) break;
t += h;
}
return t;
}
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.0001, 0.);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)));
}
```
### Step 3: Cosine-Weighted Hemisphere Sampling
```glsl
// fizzer method (most concise)
vec3 cosineDirection(vec3 n) {
float u = frand(), v = frand();
float a = 6.2831853 * v;
float b = 2.0 * u - 1.0;
vec3 dir = vec3(sqrt(1.0 - b * b) * vec2(cos(a), sin(a)), b);
return normalize(n + dir);
}
// ONB construction method (more intuitive)
vec3 cosineDirectionONB(vec3 n) {
vec2 r = vec2(frand(), frand());
vec3 u = normalize(cross(n, vec3(0., 1., 1.)));
vec3 v = cross(u, n);
float ra = sqrt(r.y);
return normalize(ra * cos(6.2831853 * r.x) * u + ra * sin(6.2831853 * r.x) * v + sqrt(1.0 - r.y) * n);
}
```
### Step 4: Materials and BRDF
```glsl
#define MAT_DIFF 0
#define MAT_SPEC 1
#define MAT_REFR 2
// Diffuse: throughput *= albedo; dir = cosineDirection(nl)
// Specular: throughput *= albedo; dir = reflect(rd, n)
// Refraction (glass)
void handleDielectric(inout Ray r, vec3 n, vec3 x, float ior, vec3 albedo, inout vec3 mask) {
float a = dot(n, r.d), ddn = abs(a);
float nnt = mix(1.0 / ior, ior, float(a > 0.));
float cos2t = 1. - nnt * nnt * (1. - ddn * ddn);
r = Ray(x, reflect(r.d, n));
if (cos2t > 0.) {
vec3 tdir = normalize(r.d * nnt + sign(a) * n * (ddn * nnt + sqrt(cos2t)));
float R0 = (ior - 1.) * (ior - 1.) / ((ior + 1.) * (ior + 1.));
float c = 1. - mix(ddn, dot(tdir, n), float(a > 0.));
float Re = R0 + (1. - R0) * c * c * c * c * c;
float P = .25 + .5 * Re;
if (frand() < P) { mask *= Re / P; }
else { mask *= albedo * (1. - Re) / (1. - P); r = Ray(x, tdir); }
}
}
```
### Step 5: Direct Light Sampling (NEE)
```glsl
// Spherical light solid angle sampling
vec3 coneSample(vec3 d, float phi, float sina, float cosa) {
vec3 w = normalize(d);
vec3 u = normalize(cross(w.yzx, w));
vec3 v = cross(w, u);
return (u * cos(phi) + v * sin(phi)) * sina + w * cosa;
}
// Called at diffuse shading points:
vec3 l0 = lightPos - x;
float cos_a_max = sqrt(1. - clamp(lightR * lightR / dot(l0, l0), 0., 1.));
float cosa = mix(cos_a_max, 1., frand());
vec3 l = coneSample(l0, 6.2831853 * frand(), sqrt(1. - cosa * cosa), cosa);
// After shadow test passes:
float omega = 6.2831853 * (1. - cos_a_max);
vec3 directLight = lightEmission * clamp(dot(l, nl), 0., 1.) * omega / PI;
```
### Step 6: Path Tracing Main Loop
```glsl
#define MAX_BOUNCES 8
vec3 pathtrace(Ray r) {
vec3 acc = vec3(0.), throughput = vec3(1.);
for (int depth = 0; depth < MAX_BOUNCES; depth++) {
// 1. Intersect
float t; vec3 n, albedo, emission; int matType;
if (!intersectScene(r, t, n, albedo, emission, matType)) break;
vec3 x = r.o + r.d * t;
vec3 nl = dot(n, r.d) < 0. ? n : -n;
// 2. Accumulate self-emission
acc += throughput * emission;
// 3. Russian roulette (starting from bounce 3)
if (depth > 2) {
float p = max(throughput.r, max(throughput.g, throughput.b));
if (frand() > p) break;
throughput /= p;
}
// 4. Material branching
if (matType == MAT_DIFF) {
acc += throughput * directLighting(x, nl, albedo, ...); // NEE
throughput *= albedo;
r = Ray(x + nl * 1e-3, cosineDirection(nl));
} else if (matType == MAT_SPEC) {
throughput *= albedo;
r = Ray(x + nl * 1e-3, reflect(r.d, n));
} else {
handleDielectric(r, n, x, 1.5, albedo, throughput);
}
}
return acc;
}
```
### Step 7: Progressive Accumulation and Display
```glsl
// Buffer A: path tracing + accumulation
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
srand(ivec2(fragCoord), iFrame);
// ... camera setup, ray generation ...
vec3 color = pathtrace(ray);
vec4 prev = texelFetch(iChannel0, ivec2(fragCoord), 0);
if (iFrame == 0) prev = vec4(0.);
fragColor = prev + vec4(color, 1.0);
}
// Image Pass: ACES tone mapping + Gamma
vec3 ACES(vec3 x) {
float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
return (x * (a * x + b)) / (x * (c * x + d) + e);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec4 data = texelFetch(iChannel0, ivec2(fragCoord), 0);
vec3 col = data.rgb / max(data.a, 1.0);
col = ACES(col);
col = pow(clamp(col, 0., 1.), vec3(1.0 / 2.2));
vec2 uv = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.1);
fragColor = vec4(col, 1.0);
}
```
## Complete Code Template
ShaderToy dual pass: Buffer A (path tracing + accumulation, iChannel0=self), Image (display).
**Buffer A:**
```glsl
#define PI 3.14159265359
#define MAX_BOUNCES 6
#define SAMPLES_PER_FRAME 2
#define NUM_SPHERES 9
#define IOR_GLASS 1.5
#define ENABLE_NEE
#define MAT_DIFF 0
#define MAT_SPEC 1
#define MAT_REFR 2
int iSeed;
int irand() { iSeed = iSeed * 0x343fd + 0x269ec3; return (iSeed >> 16) & 32767; }
float frand() { return float(irand()) / 32767.0; }
void srand(ivec2 p, int frame) {
int n = frame;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
n += p.y;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
n += p.x;
n = (n << 13) ^ n; n = n * (n * n * 15731 + 789221) + 1376312589;
iSeed = n;
}
struct Ray { vec3 o, d; };
struct Sphere { float r; vec3 p, e, c; int refl; };
Sphere spheres[NUM_SPHERES];
void initScene() {
spheres[0] = Sphere(1e5, vec3(-1e5+1., 40.8, 81.6), vec3(0.), vec3(.75,.25,.25), MAT_DIFF);
spheres[1] = Sphere(1e5, vec3( 1e5+99., 40.8, 81.6), vec3(0.), vec3(.25,.25,.75), MAT_DIFF);
spheres[2] = Sphere(1e5, vec3(50., 40.8, -1e5), vec3(0.), vec3(.75), MAT_DIFF);
spheres[3] = Sphere(1e5, vec3(50., 40.8, 1e5+170.), vec3(0.), vec3(0.), MAT_DIFF);
spheres[4] = Sphere(1e5, vec3(50., -1e5, 81.6), vec3(0.), vec3(.75), MAT_DIFF);
spheres[5] = Sphere(1e5, vec3(50., 1e5+81.6, 81.6), vec3(0.), vec3(.75), MAT_DIFF);
spheres[6] = Sphere(16.5, vec3(27., 16.5, 47.), vec3(0.), vec3(1.), MAT_SPEC);
spheres[7] = Sphere(16.5, vec3(73., 16.5, 78.), vec3(0.), vec3(.7,1.,.9), MAT_REFR);
spheres[8] = Sphere(600., vec3(50., 681.33, 81.6), vec3(12.), vec3(0.), MAT_DIFF);
}
float iSphere(Sphere s, Ray r) {
vec3 op = s.p - r.o;
float b = dot(op, r.d);
float det = b * b - dot(op, op) + s.r * s.r;
if (det < 0.) return 0.;
det = sqrt(det);
float t = b - det;
if (t > 1e-3) return t;
t = b + det;
return t > 1e-3 ? t : 0.;
}
int intersect(Ray r, out float t, out Sphere s, int avoid) {
int id = -1; t = 1e5;
for (int i = 0; i < NUM_SPHERES; ++i) {
float d = iSphere(spheres[i], r);
if (i != avoid && d > 0. && d < t) { t = d; id = i; s = spheres[i]; }
}
return id;
}
vec3 cosineDirection(vec3 n) {
float u = frand(), v = frand();
float a = 6.2831853 * v;
float b = 2.0 * u - 1.0;
vec3 dir = vec3(sqrt(1.0 - b * b) * vec2(cos(a), sin(a)), b);
return normalize(n + dir);
}
vec3 coneSample(vec3 d, float phi, float sina, float cosa) {
vec3 w = normalize(d);
vec3 u = normalize(cross(w.yzx, w));
vec3 v = cross(w, u);
return (u * cos(phi) + v * sin(phi)) * sina + w * cosa;
}
vec3 radiance(Ray r) {
vec3 acc = vec3(0.), mask = vec3(1.);
int id = -1;
for (int depth = 0; depth < MAX_BOUNCES; ++depth) {
float t; Sphere obj;
if ((id = intersect(r, t, obj, id)) < 0) break;
vec3 x = r.o + r.d * t;
vec3 n = normalize(x - obj.p);
vec3 nl = n * sign(-dot(n, r.d));
if (depth > 3) {
float p = max(obj.c.r, max(obj.c.g, obj.c.b));
if (frand() > p) { acc += mask * obj.e; break; }
mask /= p;
}
if (obj.refl == MAT_DIFF) {
vec3 d = cosineDirection(nl);
vec3 e = vec3(0.);
#ifdef ENABLE_NEE
{
Sphere ls = spheres[8];
vec3 l0 = ls.p - x;
float cos_a_max = sqrt(1. - clamp(ls.r * ls.r / dot(l0, l0), 0., 1.));
float cosa = mix(cos_a_max, 1., frand());
vec3 l = coneSample(l0, 6.2831853 * frand(), sqrt(1. - cosa * cosa), cosa);
float st; Sphere dummy;
if (intersect(Ray(x, l), st, dummy, id) == 8) {
float omega = 6.2831853 * (1. - cos_a_max);
e = ls.e * clamp(dot(l, nl), 0., 1.) * omega / PI;
}
}
#endif
acc += mask * obj.e + mask * obj.c * e;
mask *= obj.c;
r = Ray(x + nl * 1e-3, d);
} else if (obj.refl == MAT_SPEC) {
acc += mask * obj.e;
mask *= obj.c;
r = Ray(x + nl * 1e-3, reflect(r.d, n));
} else {
acc += mask * obj.e;
float a = dot(n, r.d), ddn = abs(a);
float nc = 1., nt = IOR_GLASS;
float nnt = mix(nc / nt, nt / nc, float(a > 0.));
float cos2t = 1. - nnt * nnt * (1. - ddn * ddn);
r = Ray(x, reflect(r.d, n));
if (cos2t > 0.) {
vec3 tdir = normalize(r.d * nnt + sign(a) * n * (ddn * nnt + sqrt(cos2t)));
float R0 = (nt - nc) * (nt - nc) / ((nt + nc) * (nt + nc));
float c = 1. - mix(ddn, dot(tdir, n), float(a > 0.));
float Re = R0 + (1. - R0) * c * c * c * c * c;
float P = .25 + .5 * Re;
if (frand() < P) { mask *= Re / P; }
else { mask *= obj.c * (1. - Re) / (1. - P); r = Ray(x, tdir); }
}
}
}
return acc;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
initScene();
srand(ivec2(fragCoord), iFrame);
vec2 uv = 2. * fragCoord / iResolution.xy - 1.;
vec3 camPos = vec3(50., 40.8, 169.);
vec3 cz = normalize(vec3(50., 40., 81.6) - camPos);
vec3 cx = vec3(1., 0., 0.);
vec3 cy = normalize(cross(cx, cz));
cx = cross(cz, cy);
vec3 color = vec3(0.);
for (int i = 0; i < SAMPLES_PER_FRAME; i++) {
vec2 jitter = vec2(frand(), frand()) - 0.5;
vec2 suv = uv + jitter * 2.0 / iResolution.xy;
float fov = 0.53135;
vec3 rd = normalize(fov * (iResolution.x / iResolution.y * suv.x * cx + suv.y * cy) + cz);
color += radiance(Ray(camPos, rd));
}
vec4 prev = texelFetch(iChannel0, ivec2(fragCoord), 0);
if (iFrame == 0) prev = vec4(0.);
fragColor = prev + vec4(color, float(SAMPLES_PER_FRAME));
}
```
**Image Pass** (iChannel0 = Buffer A):
```glsl
vec3 ACES(vec3 x) {
float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
return (x * (a * x + b)) / (x * (c * x + d) + e);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec4 data = texelFetch(iChannel0, ivec2(fragCoord), 0);
vec3 col = data.rgb / max(data.a, 1.0);
col = ACES(col);
col = pow(clamp(col, 0., 1.), vec3(1.0 / 2.2));
vec2 uv = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.1);
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### 1. SDF Scene Path Tracing
```glsl
float map(vec3 p) {
float d = p.y + 0.5;
d = min(d, length(p - vec3(0., 0.4, 0.)) - 0.4);
return d;
}
float intersectScene(vec3 ro, vec3 rd, float tmax) {
float t = 0.01;
for (int i = 0; i < 128; i++) {
float h = map(ro + rd * t);
if (h < 0.0001 || t > tmax) break;
t += h;
}
return t < tmax ? t : -1.0;
}
```
### 2. Disney BRDF Path Tracing
```glsl
struct Material { vec3 albedo; float metallic, roughness; };
float D_GGX(float a2, float NoH) {
float d = NoH * NoH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float G_Smith(float NoV, float NoL, float a2) {
float g1 = (2.0 * NoV) / (NoV + sqrt(a2 + (1.0 - a2) * NoV * NoV));
float g2 = (2.0 * NoL) / (NoL + sqrt(a2 + (1.0 - a2) * NoL * NoL));
return g1 * g2;
}
vec3 SampleGGXVNDF(vec3 V, float ax, float ay, float r1, float r2) {
vec3 Vh = normalize(vec3(ax * V.x, ay * V.y, V.z));
float lensq = Vh.x * Vh.x + Vh.y * Vh.y;
vec3 T1 = lensq > 0. ? vec3(-Vh.y, Vh.x, 0) * inversesqrt(lensq) : vec3(1, 0, 0);
vec3 T2 = cross(Vh, T1);
float r = sqrt(r1), phi = 2.0 * PI * r2;
float t1 = r * cos(phi), t2 = r * sin(phi);
float s = 0.5 * (1.0 + Vh.z);
t2 = (1.0 - s) * sqrt(1.0 - t1 * t1) + s * t2;
vec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0., 1. - t1*t1 - t2*t2)) * Vh;
return normalize(vec3(ax * Nh.x, ay * Nh.y, max(0., Nh.z)));
}
```
### 3. Depth of Field
```glsl
#define APERTURE 0.12
#define FOCUS_DIST 8.0
vec2 uniformDisk() {
vec2 r = vec2(frand(), frand());
return sqrt(r.y) * vec2(cos(6.2831853 * r.x), sin(6.2831853 * r.x));
}
// After generating the ray:
vec3 focalPoint = ro + rd * FOCUS_DIST;
vec3 offset = ca * vec3(uniformDisk() * APERTURE, 0.);
ro += offset;
rd = normalize(focalPoint - ro);
```
### 4. MIS (Multiple Importance Sampling)
```glsl
float misWeight(float pdfA, float pdfB) {
float a2 = pdfA * pdfA, b2 = pdfB * pdfB;
return a2 / (a2 + b2);
}
// BRDF sample hits light -> misWeight(brdfPdf, lightPdf)
// Light sample -> misWeight(lightPdf, brdfPdf)
```
### 5. Volumetric Path Tracing (Participating Media)
```glsl
vec3 transmittance = exp(-extinction * distance);
float scatterDist = -log(frand()) / extinctionMajorant;
if (scatterDist < hitDist) {
pos += ray.d * scatterDist;
ray.d = uniformSphereSample(); // or Henyey-Greenstein
throughput *= albedo;
}
```
## Performance & Composition
- 1-4 spp per frame + inter-frame accumulation for convergence; Russian roulette from bounce 3-4, survival probability = max throughput component
- NEE significantly accelerates small light sources; offset along normal by 1e-3~1e-4 or record hit ID to prevent self-intersection
- `min(color, 10.)` to prevent fireflies; SDF limited to 128-256 steps + reasonable tmax; integer hash preferred over sin-hash
- **Composition**: SDF modeling / HDR environment maps / Disney BRDF (GGX+VNDF) / volume rendering (Beer-Lambert) / spectral rendering (Sellmeier+CIE XYZ) / TAA (temporal reprojection)
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/path-tracing-gi.md)

View File

@@ -0,0 +1,373 @@
## WebGL2 Adaptation Requirements
Code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt to WebGL2:
- Use `canvas.getContext("webgl2")`
- **IMPORTANT: Version directive must strictly be on the first line**: When injecting shader code into HTML, ensure nothing precedes `#version 300 es` — no newlines, spaces, comments, or other characters. Common pitfall: accidentally adding `\n` when concatenating template strings, causing the version directive to appear on line 2-3
- First line of shader: `#version 300 es`, add `precision highp float;` for fragment shaders
- Vertex shader: `attribute``in`, `varying``out`
- Fragment shader: `varying``in`, `gl_FragColor` → custom `out vec4 fragColor`, `texture2D()``texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to standard `void main()` entry
**IMPORTANT: GLSL Type Strictness Warning**:
- `vec2 = float` is illegal: types must match exactly, e.g., `float r = length(uv)` not `vec2 r = length(uv)`
- Function return types must match: commonly used `fbm()` / `noise()` return `float`, cannot be assigned to `vec2`
- If you need a vec2 type, use `vec2(fbm(...), fbm(...))` or `vec2(value)` constructor
# Polar Coordinates & UV Manipulation
## Use Cases
- Radially symmetric effects: flowers, kaleidoscopes, gears, radial patterns
- Spiral patterns: galaxies, vortices, spiral staircases
- Ring/tunnel effects: tube flying, torus twisting, circular UI elements
- Polar coordinate shapes: cardioid, rose curves, stars, and other shapes defined by r(θ)
- Vortex animations: swirls, rotational warping, card game backgrounds (e.g., Balatro)
- Fractal/repetitive structures: recursive symmetric patterns based on angular subdivision
## Core Principles
Polar coordinates convert (x, y) to (r, θ):
- **r = length(p)** — distance to origin
- **θ = atan(y, x)** — angle from positive x-axis, range [-π, π]
Inverse transform: x = r·cos(θ), y = r·sin(θ)
Manipulation effects:
- Modifying θ → rotation, warping, kaleidoscope
- Modifying r → scaling, radial ripples
- θ += f(r) → spiral effect
| Spiral Type | Equation | Code |
|------------|----------|------|
| Archimedean spiral | r = a + bθ | `theta += radius` |
| Logarithmic spiral | r = ae^(bθ) | `theta += log(radius)` |
| Rose curve | r = cos(nθ) | `r - A*sin(n*theta)` |
## Implementation Steps
### Step 1: UV Normalization and Centering
```glsl
// Range [-1, 1], most commonly used
vec2 uv = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
// Range [-aspect, aspect] x [-1, 1]
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// Pixelated style (Balatro style)
float pixel_size = length(iResolution.xy) / PIXEL_FILTER;
vec2 uv = (floor(fragCoord * (1.0/pixel_size)) * pixel_size - 0.5*iResolution.xy) / length(iResolution.xy);
```
### Step 2: Cartesian → Polar Coordinates
```glsl
float r = length(uv);
float theta = atan(uv.y, uv.x); // [-PI, PI]
// Reusable function
vec2 toPolar(vec2 p) { return vec2(length(p), atan(p.y, p.x)); }
// Normalized angle to [0, 1]
vec2 polar = vec2(atan(uv.y, uv.x) / 6.283 + 0.5, length(uv));
```
### Step 3: Polar Space Operations
**3a. Radial Swirl**
```glsl
float spin_amount = 0.25;
float new_theta = theta - spin_amount * 20.0 * r;
```
**3b. Angular Twist**
```glsl
float twist_angle = theta + 2.0 * iTime + sin(theta) * sin(iTime) * 3.14159;
```
**3c. Archimedean Spiral**
```glsl
vec2 spiral_uv = vec2(theta_normalized, r);
spiral_uv.y -= spiral_uv.x; // Unwrap into spiral band
```
**3d. Logarithmic Spiral**
```glsl
float shear = 2.0 * log(r);
float c = cos(shear), s = sin(shear);
mat2 spiral_mat = mat2(c, -s, s, c);
```
**3e. Kaleidoscope**
```glsl
float rep = 12.0; // Number of symmetry axes
float sector = TAU / rep;
float a = polar.y;
float c_idx = floor((a + sector * 0.5) / sector);
a = mod(a + sector * 0.5, sector) - sector * 0.5;
a *= mod(c_idx, 2.0) * 2.0 - 1.0; // Mirror
```
**3f. Spiral Arm Compression**
```glsl
float NB_ARMS = 5.0;
float COMPR = 0.1;
float phase = NB_ARMS * (theta - shear);
theta = theta - COMPR * cos(phase);
float arm_density = 1.0 + NB_ARMS * COMPR * sin(phase);
```
### Step 4: Polar → Cartesian Reconstruction
```glsl
vec2 new_uv = vec2(r * cos(new_theta), r * sin(new_theta));
vec2 toRect(vec2 p) { return vec2(p.x * cos(p.y), p.x * sin(p.y)); }
// Balatro-style round-trip (offset to screen center)
vec2 mid = (iResolution.xy / length(iResolution.xy)) / 2.0;
vec2 warped_uv = vec2(r * cos(new_theta) + mid.x, r * sin(new_theta) + mid.y) - mid;
```
### Step 5: Polar Coordinate Shape SDF
```glsl
// Cardioid
float a = atan(p.x, p.y) / 3.141593; // atan(x,y) makes the heart face upward
float h = abs(a);
float heart_r = (13.0*h - 22.0*h*h + 10.0*h*h*h) / (6.0 - 5.0*h);
float dist = r - heart_r;
// Rose curve
float rose_dist = abs(r - A_coeff * sin(PETAL_FREQ * theta) - 0.5);
// Rendering
float shape = smoothstep(0.01, -0.01, dist);
```
### Step 6: Coloring and Anti-Aliasing
```glsl
// fwidth adaptive anti-aliasing
float aa = smoothstep(-1.0, 1.0, value / fwidth(value));
// Resolution-based anti-aliasing
float aa_size = 2.0 / iResolution.y;
float edge = smoothstep(0.5 - aa_size, 0.5 + aa_size, value);
// Radial gradient coloring
vec3 color = vec3(1.0, 0.4 * r, 0.3);
color *= 1.0 - 0.4 * r;
// Inter-spiral-band anti-aliasing
float inter_spiral_aa = 1.0 - pow(abs(2.0 * fract(spiral_uv.y) - 1.0), 10.0);
```
## Complete Code Template
```glsl
// === Polar Coordinates & UV Manipulation Complete Template ===
// Paste directly into ShaderToy to run
#define PI 3.14159265359
#define TAU 6.28318530718
// ===== Adjustable Parameters =====
#define MODE 0 // 0=swirl, 1=spiral, 2=kaleidoscope, 3=rose curve
#define SPIRAL_TYPE 0 // 0=Archimedean, 1=logarithmic (MODE=1)
#define NUM_ARMS 5.0 // Number of spiral arms (MODE=1)
#define KALEID_SEGMENTS 6.0 // Kaleidoscope segments (MODE=2)
#define PETAL_COUNT 5.0 // Number of petals (MODE=3)
#define SWIRL_STRENGTH 3.0 // Swirl intensity (MODE=0)
#define ANIM_SPEED 1.0 // Animation speed
#define COLOR_SCHEME 0 // 0=warm, 1=cool, 2=rainbow
vec2 toPolar(vec2 p) {
return vec2(length(p), atan(p.y, p.x));
}
vec2 toRect(vec2 p) {
return vec2(p.x * cos(p.y), p.x * sin(p.y));
}
vec2 kaleidoscope(vec2 polar, float segments) {
float sector = TAU / segments;
float a = polar.y;
float c = floor((a + sector * 0.5) / sector);
a = mod(a + sector * 0.5, sector) - sector * 0.5;
a *= mod(c, 2.0) * 2.0 - 1.0;
return vec2(polar.x, a);
}
vec3 getColor(float t, int scheme) {
if (scheme == 1) return 0.5 + 0.5 * cos(TAU * (t + vec3(0.0, 0.33, 0.67)));
if (scheme == 2) return 0.5 + 0.5 * cos(TAU * t + vec3(0.0, 2.1, 4.2));
return vec3(1.0, 0.4 + 0.4 * cos(t * TAU), 0.3 + 0.2 * sin(t * TAU));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / min(iResolution.x, iResolution.y);
vec2 polar = toPolar(uv);
float r = polar.x;
float theta = polar.y;
float t = iTime * ANIM_SPEED;
vec3 col = vec3(0.0);
float aa = 2.0 / iResolution.y;
#if MODE == 0
// --- Swirl mode ---
float swirl_theta = theta - SWIRL_STRENGTH * r + t;
vec2 warped = toRect(vec2(r, swirl_theta));
warped *= 10.0;
float pattern = sin(warped.x) * cos(warped.y);
pattern += 0.5 * sin(2.0 * warped.x + t) * cos(2.0 * warped.y - t);
float val = smoothstep(-0.1, 0.1, pattern);
col = mix(
getColor(r * 0.5, COLOR_SCHEME),
getColor(r * 0.5 + 0.5, COLOR_SCHEME),
val
);
col *= exp(-r * 0.5);
#elif MODE == 1
// --- Spiral mode ---
#if SPIRAL_TYPE == 0
float spiral = theta / TAU + 0.5;
float bands = spiral + r;
bands -= t * 0.1;
float arm = fract(bands * NUM_ARMS);
#else
float shear = 2.0 * log(max(r, 0.001));
float phase = NUM_ARMS * (theta - shear);
float arm = 0.5 + 0.5 * cos(phase);
arm *= 1.0 + NUM_ARMS * 0.1 * sin(phase);
#endif
float brightness = smoothstep(0.0, 0.4, arm) * smoothstep(1.0, 0.6, arm);
col = getColor(theta / TAU + t * 0.05, COLOR_SCHEME) * brightness;
col *= exp(-r * r * 0.5);
col += 0.15 * exp(-r * r * 8.0);
#elif MODE == 2
// --- Kaleidoscope mode ---
vec2 kp = kaleidoscope(polar, KALEID_SEGMENTS);
vec2 rect = toRect(kp);
rect *= 4.0;
rect += vec2(t * 0.3, 0.0);
vec2 cell_id = floor(rect + 0.5);
vec2 cell_uv = fract(rect + 0.5) - 0.5;
float cell_hash = fract(sin(dot(cell_id, vec2(127.1, 311.7))) * 43758.5453);
float d = length(cell_uv);
float truchet = abs(d - 0.35);
if (cell_hash > 0.5) {
truchet = min(truchet, abs(length(cell_uv - 0.5) - 0.5));
} else {
truchet = min(truchet, abs(length(cell_uv + 0.5) - 0.5));
}
col = getColor(cell_hash + r * 0.2, COLOR_SCHEME);
col *= smoothstep(0.05, 0.0, truchet - 0.03);
col *= smoothstep(3.0, 0.0, r);
#elif MODE == 3
// --- Rose curve mode ---
float rose_r = 0.6 * cos(PETAL_COUNT * theta + t);
float dist = abs(r - abs(rose_r));
float ribbon_width = 0.04;
float rose_shape = smoothstep(ribbon_width + aa, ribbon_width - aa, dist);
float depth = 0.5 + 0.5 * cos(PETAL_COUNT * theta + t);
col = getColor(theta / TAU, COLOR_SCHEME) * depth;
col *= rose_shape;
float center = smoothstep(0.08 + aa, 0.08 - aa, r);
col += getColor(0.5, COLOR_SCHEME) * center * 0.5;
#endif
col = pow(col, vec3(1.0 / 2.2));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Dynamic Vortex Background (Balatro Style)
Cartesian→Polar→Cartesian round-trip + iterative domain warping
```glsl
float new_angle = atan(uv.y, uv.x) + speed
- SPIN_EASE * 20.0 * (SPIN_AMOUNT * uv_len + (1.0 - SPIN_AMOUNT));
vec2 mid = (screenSize.xy / length(screenSize.xy)) / 2.0;
uv = vec2(uv_len * cos(new_angle) + mid.x,
uv_len * sin(new_angle) + mid.y) - mid;
uv *= 30.0;
for (int i = 0; i < 5; i++) {
uv2 += sin(max(uv.x, uv.y)) + uv;
uv += 0.5 * vec2(cos(5.1123 + 0.353*uv2.y + speed*0.131),
sin(uv2.x - 0.113*speed));
uv -= cos(uv.x + uv.y) - sin(uv.x*0.711 - uv.y);
}
```
### Variant 2: Polar Torus Twist (Ring Twister Style)
Direct rendering in polar space, angular slicing to simulate 3D torus
```glsl
vec2 uvr = vec2(length(uv), atan(uv.y, uv.x) + PI);
uvr.x -= OUT_RADIUS;
float twist = uvr.y + 2.0*iTime + sin(uvr.y)*sin(iTime)*PI;
for (int i = 0; i < NUM_FACES; i++) {
float x0 = IN_RADIUS * sin(twist + TAU * float(i) / float(NUM_FACES));
float x1 = IN_RADIUS * sin(twist + TAU * float(i+1) / float(NUM_FACES));
vec4 face = slice(x0, x1, uvr);
col = mix(col, face.rgb, face.a);
}
```
### Variant 3: Galaxy / Logarithmic Spiral (Galaxy Style)
`log(r)` equiangular spiral + FBM noise + spiral arm compression
```glsl
float rho = length(uv);
float ang = atan(uv.y, uv.x);
float shear = 2.0 * log(rho);
mat2 R = mat2(cos(shear), -sin(shear), sin(shear), cos(shear));
float phase = NB_ARMS * (ang - shear);
ang = ang - COMPR * cos(phase) + SPEED * t;
uv = rho * vec2(cos(ang), sin(ang));
float gaz = fbm_noise(0.09 * R * uv);
```
### Variant 4: Archimedean Spiral Band (Wave Greek Frieze Style)
Polar unwrap into spiral band, creating vortex animation within the band
```glsl
vec2 U = vec2(atan(U.y, U.x)/TAU + 0.5, length(U));
U.y -= U.x; // Archimedean unwrap
U.x = arc_length(ceil(U.y) + U.x) - iTime; // Arc length parameterization
vec2 cell_uv = fract(U) - 0.5;
float vortex = dot(cell_uv,
cos(vec2(-33.0, 0.0)
+ 0.3 * (iTime + cell_id.x)
* max(0.0, 0.5 - length(cell_uv))));
```
### Variant 5: Complex / Polar Duality (Jeweled Vortex Style)
Complex arithmetic replaces explicit trigonometric functions for conformal mapping
```glsl
float e = n * 2.0;
float a = atan(u.y, u.x) - PI/2.0;
float r = exp(log(length(u)) / e); // r^(1/e)
float sc = ceil(r - a/TAU);
float s = pow(sc + a/TAU, 2.0);
col += sin(cr + s/n * TAU / 2.0);
col *= cos(cr + s/n * TAU);
col *= pow(abs(sin((r - a/TAU) * PI)), abs(e) + 5.0);
```
## Performance & Composition
### Performance Tips
- **Pole safety**: `float r = max(length(uv), 1e-6);` to avoid division by zero
- **Trigonometric optimization**: When both sin/cos are needed, use a rotation matrix `mat2 ROT(float a) { float c=cos(a),s=sin(a); return mat2(c,s,-s,c); }`
- **Kaleidoscope is naturally optimized**: All expensive computation happens in a single sector, visual complexity ×N
- **Loop control**: Rose curves and other multi-loop effects work well with 4-8 loops; don't go too high
- **Pixel downsampling**: `floor(fragCoord / pixel_size) * pixel_size` quantizes coordinates to reduce computation
### Composition Tips
- **Polar + FBM**: Sample noise in transformed space → organic spiral textures
- **Polar + Truchet**: Lay Truchet tiles after kaleidoscope folding → geometric tunnel effects
- **Polar + SDF**: `r(θ)` defines contour + SDF boolean operations / glow
- **Polar + Checkerboard**: `sign(sin(u*PI*4.0)*cos(uvr.y*16.0))` → circular checkerboard
- **Polar + Post-Processing**: Gamma + vignette + contrast enhancement for improved visual quality
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/polar-uv-manipulation.md)

View File

@@ -0,0 +1,788 @@
## WebGL2 Adaptation Requirements
**IMPORTANT: Critical Warning for Standalone HTML Deployment**: Post-processing effects require an input texture to work. When generating standalone HTML, you must:
1. Set `#define USE_DEMO_SCENE 1` to use the built-in demo scene (recommended), or
2. Pass a valid input texture to the `iChannel0` channel, otherwise the screen will be completely black
3. **Critical**: When USE_DEMO_SCENE=1, ensure the #else branch code does not reference non-existent uniforms (e.g., iChannel0)
**IMPORTANT: GLSL Type Strictness Rules**:
- `vec2 = float` is illegal — must use `vec2(x, x)` or `vec2(x)`
- Function parameters must be defined before use; using a variable name in its own initializer is forbidden (e.g., `float w = filmicCurve(w, w)` is an error)
- Variables must be declared before use
- **#version must be the very first line of shader code**: No characters (including whitespace or comments) may precede `#version 300 es`
- **Code in preprocessor branches is still compiled**: Even if `#if USE_DEMO_SCENE` is true, the `#else` branch code is still compiled by the GPU — all branches must be valid GLSL code
Code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt to WebGL2:
- Use `canvas.getContext("webgl2")`
- First line of shader: `#version 300 es`, add `precision highp float;` for fragment shaders
- Vertex shader: `attribute``in`, `varying``out`
- Fragment shader: `varying``in`, `gl_FragColor` → custom `out vec4 fragColor`, `texture2D()``texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` must be adapted to standard `void main()` entry
- Must create Framebuffers and render to texture before post-processing
### Complete WebGL2 Standalone HTML Template
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Post-Processing Shader</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<!-- Vertex Shader: #version must be the first line -->
<script id="vs" type="x-shader/x-vertex">
#version 300 es
in vec2 a_position;
out vec2 v_uv;
void main() {
v_uv = a_position * 0.5 + 0.5;
gl_Position = vec4(a_position, 0.0, 1.0);
}
</script>
<!-- Fragment Shader: #version must be the first line, precision follows -->
<script id="fs" type="x-shader/x-fragment">
#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 fragColor;
uniform float iTime;
uniform vec2 iResolution;
// Note: Do not use iChannel0 in standalone HTML unless a valid texture is bound
// Recommended: Use USE_DEMO_SCENE=1 for the built-in demo scene
#define USE_DEMO_SCENE 1 // Recommended: use built-in demo scene
// Demo scene function (replaces iChannel0 sampling)
vec3 demoScene(vec2 uv, float time) {
vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0.0, 2.0, 4.0));
float d = length(uv - 0.5) - 0.12;
col += vec3(3.0, 2.5, 1.8) * smoothstep(0.02, 0.0, d);
return col;
}
// Tone mapping and other post-processing functions...
// Note: Do not reference iChannel0 in the #else branch of #if USE_DEMO_SCENE
// If you need iChannel0, use #ifdef or ensure the texture is bound when USE_DEMO_SCENE=0
void main() {
vec2 uv = v_uv;
vec3 color;
#if USE_DEMO_SCENE
color = demoScene(uv, iTime);
#else
// This branch only executes when USE_DEMO_SCENE=0 and a texture is bound
// Requires binding in JavaScript: gl.bindTexture(gl.TEXTURE_2D, texture);
color = vec3(0.0); // fallback
#endif
fragColor = vec4(color, 1.0);
}
</script>
<script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
// Compile shader
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Create program
const vs = createShader(gl, gl.VERTEX_SHADER, document.getElementById('vs').textContent);
const fs = createShader(gl, gl.FRAGMENT_SHADER, document.getElementById('fs').textContent);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
// Fullscreen quad
const positions = new Float32Array([-1,-1, 1,-1, -1,1, 1,1]);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
// Uniform locations
const timeLoc = gl.getUniformLocation(program, 'iTime');
const resLoc = gl.getUniformLocation(program, 'iResolution');
// Render loop
function render(time) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
gl.useProgram(program);
gl.uniform1f(timeLoc, time * 0.001);
gl.uniform2f(resLoc, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
```
### Multi-Pass Post-Processing HTML Template (with FBO)
Bloom separable blur, TAA, multi-step post-processing pipelines, etc. require rendering to intermediate textures. The following skeleton demonstrates the pattern: render scene to FBO → post-processing reads FBO → output to screen:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multi-Pass Post-Processing</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
let frameCount = 0;
const canvas = document.getElementById('c');
const gl = canvas.getContext('webgl2');
const ext = gl.getExtension('EXT_color_buffer_float');
function createShader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
console.error(gl.getShaderInfoLog(s));
return s;
}
function createProgram(vsSrc, fsSrc) {
const p = gl.createProgram();
gl.attachShader(p, createShader(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, createShader(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(p);
return p;
}
const vsSource = `#version 300 es
in vec2 pos;
void main(){ gl_Position=vec4(pos,0,1); }`;
// fsScene: Scene rendering shader (outputs HDR color to FBO)
// fsPost: Post-processing shader (samples scene texture from iChannel0, applies bloom/tonemap/etc)
const progScene = createProgram(vsSource, fsScene);
const progPost = createProgram(vsSource, fsPost);
function createFBO(w, h) {
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
// IMPORTANT: Critical: Check for float texture extension, fall back to RGBA8 if unsupported
const fmt = ext ? gl.RGBA16F : gl.RGBA;
const typ = ext ? gl.FLOAT : gl.UNSIGNED_BYTE;
gl.texImage2D(gl.TEXTURE_2D, 0, fmt, w, h, 0, gl.RGBA, typ, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { fbo, tex };
}
let W, H, sceneFBO;
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
function resize() {
canvas.width = W = innerWidth;
canvas.height = H = innerHeight;
sceneFBO = createFBO(W, H);
}
addEventListener('resize', resize);
resize();
function render(t) {
t *= 0.001;
// Pass 1: Scene rendering → FBO
gl.useProgram(progScene);
gl.bindFramebuffer(gl.FRAMEBUFFER, sceneFBO.fbo);
gl.viewport(0, 0, W, H);
gl.uniform2f(gl.getUniformLocation(progScene, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progScene, 'iTime'), t);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Pass 2: Post-processing reads scene texture → screen
gl.useProgram(progPost);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, W, H);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, sceneFBO.tex);
gl.uniform1i(gl.getUniformLocation(progPost, 'iChannel0'), 0);
gl.uniform2f(gl.getUniformLocation(progPost, 'iResolution'), W, H);
gl.uniform1f(gl.getUniformLocation(progPost, 'iTime'), t);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
frameCount++;
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
```
# Screen-Space Post-Processing Effects
## Use Cases
Screen-space image enhancement on already-rendered scenes: Tone Mapping, Bloom, Vignette, Chromatic Aberration, Motion Blur, DoF, FXAA/TAA, Color Grading, Film Grain, Lens Flare, etc.
Typical pipeline order: Scene Rendering → AA → Bloom → Chromatic Aberration → Motion Blur/DoF → Tone Mapping → Color Grading → Contrast → Vignette → Film Grain → Gamma → Dithering.
## Core Principles
The essence of post-processing is **per-pixel transformation of an already-rendered image** — input is a framebuffer texture, output is the transformed color value.
- **Tone Mapping**: HDR [0, ∞) → LDR [0, 1]. Reinhard `c/(1+c)`, Filmic Reinhard (white point/shoulder parameters), ACES (3×3 matrix + rational polynomial), generic rational polynomial
- **Gaussian Blur**: 2D Gaussian kernel is separable into two 1D passes, O(n²) → O(2n)
- **Bloom**: Bright-pass extraction → multi-level Gaussian blur → additive blend back to original
- **Vignette**: Brightness falloff based on pixel distance to center. Multiplicative or radial
- **Chromatic Aberration**: Sample the same texture at different scales for R/G/B channels
## Implementation Steps
### Step 1: Tone Mapping
```glsl
// Reinhard
vec3 reinhard(vec3 color) { return color / (1.0 + color); }
// Filmic Reinhard (W=white point, T2=shoulder parameter)
// IMPORTANT: GLSL critical rule: function parameters must be defined before use; using a variable name in its own initializer is forbidden
const float W = 1.2, T2 = 7.5; // adjustable
float filmic_reinhard_curve(float x) {
float q = (T2 * T2 + 1.0) * x * x;
return q / (q + x + T2 * T2);
}
vec3 filmic_reinhard(vec3 x) {
float w = filmic_reinhard_curve(W); // compute w using constant W first
return vec3(filmic_reinhard_curve(x.r), filmic_reinhard_curve(x.g), filmic_reinhard_curve(x.b)) / w;
}
// ACES industry standard
vec3 aces_tonemap(vec3 color) {
mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777);
mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602);
vec3 v = m1 * color;
vec3 a = v * (v + 0.0245786) - 0.000090537;
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
return clamp(m2 * (a / b), 0.0, 1.0);
}
// Generic rational polynomial
vec3 rational_tonemap(vec3 x) {
float a=0.010, b=0.132, c=0.010, d=0.163, e=0.101; // adjustable
return (x * (a * x + b)) / (x * (c * x + d) + e);
}
```
### Step 2: Gamma Correction
```glsl
color = pow(color, vec3(1.0 / 2.2)); // after tone mapping; ACES already includes gamma, skip this step
```
### Step 3: Contrast Enhancement (Hermite S-Curve)
```glsl
color = clamp(color, 0.0, 1.0);
color = color * color * (3.0 - 2.0 * color);
// Controllable intensity: color = mix(color, color*color*(3.0-2.0*color), strength);
// smoothstep equivalent: color = smoothstep(-0.025, 1.0, color);
```
### Step 4: Color Grading
```glsl
color = color * vec3(1.11, 0.89, 0.79); // per-channel multiply (warm tone), adjustable
color = pow(color, vec3(1.3, 1.2, 1.0)); // pow color grading, adjustable
// HSV hue shift: hsv.x = fract(hsv.x + 0.05); hsv.y *= 1.1;
// Desaturation: color = mix(color, vec3(dot(color, vec3(0.299,0.587,0.114))), 0.2);
```
### Step 5: Vignette
```glsl
// Option A: Multiplicative
vec2 q = fragCoord / iResolution.xy;
float vignette = pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.25);
color *= 0.5 + 0.5 * vignette;
// Option B: Radial distance
vec2 centered = (uv - 0.5) * vec2(iResolution.x / iResolution.y, 1.0);
float vig = mix(1.0, max(0.0, 1.0 - pow(length(centered)/1.414 * 0.6, 3.0)), 0.5);
color *= vig;
// Option C: Inverse quadratic falloff
vec2 p = 1.0 - 2.0 * fragCoord / iResolution.xy;
p.y *= iResolution.y / iResolution.x;
float vig2 = 1.25 / (1.1 + 1.1 * dot(p, p)); vig2 *= vig2;
color *= mix(1.0, smoothstep(0.1, 1.1, vig2), 0.25);
```
### Step 6: Gaussian Blur
```glsl
float normpdf(float x, float sigma) {
return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma;
}
vec3 gaussianBlur(sampler2D tex, vec2 fragCoord, vec2 resolution) {
const int KERNEL_SIZE = 11, HALF = 5; // adjustable: KERNEL_SIZE must be odd
float sigma = 7.0; // adjustable
float kernel[KERNEL_SIZE]; float Z = 0.0;
for (int j = 0; j <= HALF; ++j)
kernel[HALF + j] = kernel[HALF - j] = normpdf(float(j), sigma);
for (int j = 0; j < KERNEL_SIZE; ++j) Z += kernel[j];
vec3 result = vec3(0.0);
for (int i = -HALF; i <= HALF; ++i)
for (int j = -HALF; j <= HALF; ++j)
result += kernel[HALF+j] * kernel[HALF+i]
* texture(tex, (fragCoord + vec2(float(i), float(j))) / resolution).rgb;
return result / (Z * Z);
}
```
### Step 7: Bloom (Single Pass, Hardware Mipmap)
```glsl
vec3 simpleBloom(sampler2D tex, vec2 uv) {
vec3 bloom = vec3(0.0); float tw = 0.0; float maxB = 5.0; // adjustable
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++) {
vec2 off = vec2(float(x), float(y)) / iResolution.xy; float w = 1.0;
bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(5.0), 5.0).rgb); tw += w;
bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(6.0), 6.0).rgb); tw += w;
bloom += w * min(vec3(maxB), textureLod(tex, uv+off*exp2(7.0), 7.0).rgb); tw += w;
}
return pow(bloom / tw, vec3(1.5)) * 0.3; // adjustable: gamma and intensity
}
// Usage: color = color * 0.8 + simpleBloom(iChannel0, uv);
```
### Step 8: Chromatic Aberration
```glsl
#define CA_SAMPLES 8 // adjustable
#define CA_STRENGTH 0.003 // adjustable
vec3 chromaticAberration(sampler2D tex, vec2 uv) {
vec2 center = uv - 0.5; vec3 color = vec3(0.0);
float rf = 1.0, gf = 1.0, bf = 1.0, f = 1.0 / float(CA_SAMPLES);
for (int i = 0; i < CA_SAMPLES; ++i) {
color.r += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * rf)).r;
color.g += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * gf)).g;
color.b += f * texture(tex, 0.5 - 0.5 * (center * 2.0 * bf)).b;
rf *= 1.0 - CA_STRENGTH; gf *= 1.0 - CA_STRENGTH*0.3; bf *= 1.0 + CA_STRENGTH*0.4;
}
return clamp(color, 0.0, 1.0);
}
```
### Step 9: Film Grain
```glsl
float hash(float c) { return fract(sin(dot(c, vec2(12.9898, 78.233))) * 43758.5453); }
#define GRAIN_STRENGTH 0.012 // adjustable
color += vec3(GRAIN_STRENGTH * hash(length(fragCoord / iResolution.xy) + iTime));
// Bayer matrix ordered dithering (eliminates color banding)
const mat4 bayerMatrix = mat4(
vec4(0.,8.,2.,10.), vec4(12.,4.,14.,6.), vec4(3.,11.,1.,9.), vec4(15.,7.,13.,5.));
float orderedDither(vec2 fc) {
return (bayerMatrix[int(fc.x)&3][int(fc.y)&3] + 1.0) / 17.0;
}
color += (orderedDither(fragCoord) - 0.5) * 4.0 / 255.0;
```
### Step 10: Demo Scene (Required for Standalone HTML!)
**IMPORTANT: Critical Warning**: Standalone HTML deployment must provide an input texture, otherwise post-processing effects will output solid black.
```glsl
// Demo scene fallback: used when no valid input texture is available
vec3 demoScene(vec2 uv, float time) {
// Dynamic gradient background
vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4));
// Center glowing sphere (for testing bloom)
float d = length(uv - 0.5) - 0.15;
col += vec3(2.0) * smoothstep(0.02, 0.0, d); // extremely bright region
// Moving highlight bar (for testing bloom bleed)
float bar = step(0.48, uv.y) * step(uv.y, 0.52);
bar *= step(0.0, sin(uv.x * 10.0 - time * 2.0));
col += vec3(1.5, 0.8, 0.3) * bar;
// Colored blocks (for testing chromatic aberration and tone mapping)
vec2 id = floor(uv * 4.0);
float rand = fract(sin(dot(id, vec2(12.9898, 78.233))) * 43758.5453);
vec2 rect = fract(uv * 4.0);
float box = step(0.1, rect.x) * step(rect.x, 0.9) * step(0.1, rect.y) * step(rect.y, 0.9);
col += vec3(rand, 1.0 - rand, 0.5) * box * 0.5;
return col;
}
```
### Step 10: Motion Blur
```glsl
#define MB_SAMPLES 32 // adjustable
#define MB_STRENGTH 0.25 // adjustable
vec3 motionBlur(sampler2D tex, vec2 uv, vec2 velocity) {
vec2 dir = velocity * MB_STRENGTH; vec3 color = vec3(0.0); float tw = 0.0;
for (int i = 0; i < MB_SAMPLES; i++) {
float t = float(i) / float(MB_SAMPLES - 1), w = 1.0 - t;
color += w * textureLod(tex, uv + dir * t, 0.0).rgb; tw += w;
}
return color / tw;
}
```
### Step 11: Depth of Field
```glsl
#define DOF_SAMPLES 64
#define DOF_FOCAL_LENGTH 0.03
float getCoC(float depth, float focusDist) {
float aperture = min(1.0, focusDist * focusDist * 0.5);
return abs(aperture * (DOF_FOCAL_LENGTH * (depth - focusDist))
/ (depth * (focusDist - DOF_FOCAL_LENGTH)));
}
float goldenAngle = 3.14159265 * (3.0 - sqrt(5.0));
vec3 depthOfField(sampler2D tex, vec2 uv, float depth, float focusDist) {
float coc = getCoC(depth, focusDist);
vec3 result = texture(tex, uv).rgb * max(0.001, coc);
float tw = max(0.001, coc);
for (int i = 1; i < DOF_SAMPLES; i++) {
float fi = float(i);
float theta = fi * goldenAngle * float(DOF_SAMPLES);
float r = coc * sqrt(fi) / sqrt(float(DOF_SAMPLES));
vec2 tapUV = uv + vec2(sin(theta), cos(theta)) * r;
vec4 s = textureLod(tex, tapUV, 0.0);
float w = max(0.001, getCoC(s.w, focusDist));
result += s.rgb * w; tw += w;
}
return result / tw;
}
```
### Step 12: FXAA
```glsl
vec3 fxaa(sampler2D tex, vec2 fragCoord, vec2 resolution) {
vec2 pp = 1.0 / resolution;
vec4 color = texture(tex, fragCoord * pp);
vec3 luma = vec3(0.299, 0.587, 0.114);
float lumaNW = dot(texture(tex, (fragCoord+vec2(-1.,-1.))*pp).rgb, luma);
float lumaNE = dot(texture(tex, (fragCoord+vec2( 1.,-1.))*pp).rgb, luma);
float lumaSW = dot(texture(tex, (fragCoord+vec2(-1., 1.))*pp).rgb, luma);
float lumaSE = dot(texture(tex, (fragCoord+vec2( 1., 1.))*pp).rgb, luma);
float lumaM = dot(color.rgb, luma);
float lumaMin = min(lumaM, min(min(lumaNW,lumaNE), min(lumaSW,lumaSE)));
float lumaMax = max(lumaM, max(max(lumaNW,lumaNE), max(lumaSW,lumaSE)));
vec2 dir = vec2(-((lumaNW+lumaNE)-(lumaSW+lumaSE)), ((lumaNW+lumaSW)-(lumaNE+lumaSE)));
float dirReduce = max((lumaNW+lumaNE+lumaSW+lumaSE)*0.03125, 1.0/128.0);
dir = clamp(dir * 2.5/(min(abs(dir.x),abs(dir.y))+dirReduce), vec2(-8.0), vec2(8.0)) * pp;
vec3 rgbA = 0.5 * (texture(tex, fragCoord*pp+dir*(1./3.-0.5)).rgb
+ texture(tex, fragCoord*pp+dir*(2./3.-0.5)).rgb);
vec3 rgbB = rgbA*0.5 + 0.25*(texture(tex, fragCoord*pp+dir*-0.5).rgb
+ texture(tex, fragCoord*pp+dir*0.5).rgb);
float lumaB = dot(rgbB, luma);
return (lumaB < lumaMin || lumaB > lumaMax) ? rgbA : rgbB;
}
```
## Complete Code Template
Can be run directly in ShaderToy. `iChannel0` is the scene texture.
**IMPORTANT: Important Warning**: For standalone HTML deployment, you must:
1. Pass a valid input texture to iChannel0 (or uChannel0)
2. Or set `#define USE_DEMO_SCENE 1` to use the built-in demo scene
```glsl
// Post-Processing Pipeline — ShaderToy Template
#define ENABLE_TONEMAP 1
#define ENABLE_BLOOM 1
#define ENABLE_CA 1
#define ENABLE_VIGNETTE 1
#define ENABLE_GRAIN 1
#define ENABLE_CONTRAST 1
#define USE_DEMO_SCENE 1 // set to 1 to use built-in demo scene (required for standalone HTML)
#define TONEMAP_MODE 2 // 0=Reinhard, 1=Filmic, 2=ACES
#define BRIGHTNESS 1.0
#define WHITE_POINT 1.2
#define SHOULDER 7.5
#define BLOOM_STRENGTH 0.08
#define BLOOM_LOD_START 4.0
#define COLOR_TINT vec3(1.11, 0.89, 0.79)
#define CA_SAMPLES 8
#define CA_INTENSITY 0.003
#define VIG_POWER 0.25
#define GRAIN_AMOUNT 0.012
float hash11(float p) { return fract(sin(p * 12.9898) * 43758.5453); }
// Demo scene fallback: used when no input texture is available
vec3 demoScene(vec2 uv, float time) {
// Dynamic gradient background
vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4));
// Center glowing sphere (for testing bloom)
float d = length(uv - 0.5) - 0.15;
col += vec3(2.0) * smoothstep(0.02, 0.0, d);
// Moving highlight bar (for testing bloom bleed)
float bar = step(0.48, uv.y) * step(uv.y, 0.52);
bar *= step(0.0, sin(uv.x * 10.0 - time * 2.0));
col += vec3(1.5, 0.8, 0.3) * bar;
// Colored blocks (for testing chromatic aberration and tone mapping)
vec2 id = floor(uv * 4.0);
float rand = fract(sin(dot(id, vec2(12.9898, 78.233))) * 43758.5453);
vec2 rect = fract(uv * 4.0);
float box = step(0.1, rect.x) * step(rect.x, 0.9) * step(0.1, rect.y) * step(rect.y, 0.9);
col += vec3(rand, 1.0 - rand, 0.5) * box * 0.5;
return col;
}
vec3 tonemapReinhard(vec3 c) { return c / (1.0 + c); }
// IMPORTANT: Critical: filmicCurve takes only one parameter x; w is computed externally via WHITE_POINT
float filmicCurve(float x) {
float q = (SHOULDER*SHOULDER+1.0)*x*x; return q/(q+x+SHOULDER*SHOULDER);
}
vec3 tonemapFilmic(vec3 c) {
float w = filmicCurve(WHITE_POINT); // compute w using WHITE_POINT constant first
return vec3(filmicCurve(c.r), filmicCurve(c.g), filmicCurve(c.b)) / w;
}
vec3 tonemapACES(vec3 color) {
mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777);
mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602);
vec3 v = m1*color;
vec3 a = v*(v+0.0245786)-0.000090537;
vec3 b = v*(0.983729*v+0.4329510)+0.238081;
return clamp(m2*(a/b), 0.0, 1.0);
}
vec3 applyTonemap(vec3 c) {
c *= BRIGHTNESS;
#if TONEMAP_MODE == 0
return tonemapReinhard(c);
#elif TONEMAP_MODE == 1
return tonemapFilmic(c);
#else
return tonemapACES(c);
#endif
}
vec3 sampleBloom(sampler2D tex, vec2 uv) {
vec3 bloom = vec3(0.0); float tw = 0.0;
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++) {
vec2 off = vec2(float(x),float(y))/iResolution.xy; float w = 1.0;
bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START), BLOOM_LOD_START).rgb;
bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START+1.0), BLOOM_LOD_START+1.0).rgb;
bloom += w*textureLod(tex, uv+off*exp2(BLOOM_LOD_START+2.0), BLOOM_LOD_START+2.0).rgb;
tw += w*3.0;
}
return bloom / tw;
}
vec3 applyChromaticAberration(sampler2D tex, vec2 uv) {
vec2 center = 1.0 - 2.0*uv; vec3 color = vec3(0.0);
float rf=1.0, gf=1.0, bf=1.0, f=1.0/float(CA_SAMPLES);
for (int i = 0; i < CA_SAMPLES; ++i) {
color.r += f*texture(tex, 0.5-0.5*(center*rf)).r;
color.g += f*texture(tex, 0.5-0.5*(center*gf)).g;
color.b += f*texture(tex, 0.5-0.5*(center*bf)).b;
rf *= 1.0-CA_INTENSITY; gf *= 1.0-CA_INTENSITY*0.3; bf *= 1.0+CA_INTENSITY*0.4;
}
return clamp(color, 0.0, 1.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
// Get input color: demo scene or input texture
#if USE_DEMO_SCENE
vec3 color = demoScene(uv, iTime);
#else
#if ENABLE_CA
vec3 color = applyChromaticAberration(iChannel0, uv);
#else
vec3 color = texture(iChannel0, uv).rgb;
#endif
#endif
#if ENABLE_BLOOM && !USE_DEMO_SCENE
color += sampleBloom(iChannel0, uv) * BLOOM_STRENGTH;
#else
// In demo scene mode, use simplified bloom sampling from itself
#if ENABLE_BLOOM
vec3 bloom = vec3(0.0); float tw = 0.0;
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++) {
vec2 off = vec2(float(x),float(y))/iResolution.xy * 0.02;
vec3 s = demoScene(uv + off, iTime);
float w = 1.0;
bloom += w * min(vec3(5.0), s); tw += w;
}
color += bloom / tw * BLOOM_STRENGTH;
#endif
#endif
color *= COLOR_TINT;
#if ENABLE_TONEMAP
#if TONEMAP_MODE == 2
color = applyTonemap(color);
#else
color = applyTonemap(color);
color = pow(color, vec3(1.0/2.2));
#endif
#else
color = pow(color, vec3(1.0/2.2));
#endif
#if ENABLE_CONTRAST
color = clamp(color, 0.0, 1.0);
color = color*color*(3.0-2.0*color);
#endif
#if ENABLE_VIGNETTE
vec2 q = fragCoord/iResolution.xy;
color *= 0.5 + 0.5*pow(16.0*q.x*q.y*(1.0-q.x)*(1.0-q.y), VIG_POWER);
#endif
#if ENABLE_GRAIN
color += GRAIN_AMOUNT * hash11(dot(uv, vec2(12.9898,78.233)) + iTime);
#endif
fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
}
```
## Common Variants
### Variant 1: Multi-Pass Separable Bloom
```glsl
// Buffer A: Horizontal Gaussian blur + bright-pass
#define BLOOM_THRESHOLD vec3(0.2)
#define BLOOM_DOWNSAMPLE 3
#define BLUR_RADIUS 16
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
ivec2 xy = ivec2(fragCoord);
if (xy.x >= int(iResolution.x)/BLOOM_DOWNSAMPLE) { fragColor = vec4(0); return; }
vec3 sum = vec3(0.0); float tw = 0.0;
for (int k = -BLUR_RADIUS; k <= BLUR_RADIUS; ++k) {
vec3 texel = max(vec3(0.0), texelFetch(iChannel0, (xy+ivec2(k,0))*BLOOM_DOWNSAMPLE, 0).rgb - BLOOM_THRESHOLD);
float w = exp(-8.0 * pow(abs(float(k))/float(BLUR_RADIUS), 2.0));
sum += texel*w; tw += w;
}
fragColor = vec4(sum/tw, 1.0);
}
// Buffer B: Vertical blur, same as above but with direction changed to ivec2(0, k)
```
### Variant 2: ACES + Full Color Pipeline (with Built-in Gamma)
```glsl
vec3 aces_tonemap(vec3 color) {
mat3 m1 = mat3(0.59719,0.07600,0.02840, 0.35458,0.90834,0.13383, 0.04823,0.01566,0.83777);
mat3 m2 = mat3(1.60475,-0.10208,-0.00327, -0.53108,1.10813,-0.07276, -0.07367,-0.00605,1.07602);
vec3 v = m1*color;
vec3 a = v*(v+0.0245786)-0.000090537;
vec3 b = v*(0.983729*v+0.4329510)+0.238081;
return pow(clamp(m2*(a/b), 0.0, 1.0), vec3(1.0/2.2));
}
```
### Variant 3: DoF + Motion Blur Combination
```glsl
for (int i = 1; i < BLUR_TAPS; i++) {
float t = float(i)/float(BLUR_TAPS);
float randomT = hash(iTime + t + uv.x + uv.y*12.345);
vec2 tapUV = mix(currentUV, prevFrameUV, (randomT-0.5)*shutterAngle); // motion blur
float theta = t*goldenAngle*float(BLUR_TAPS);
float r = coc*sqrt(t*float(BLUR_TAPS))/sqrt(float(BLUR_TAPS));
tapUV += vec2(sin(theta), cos(theta))*r; // DoF
vec4 tap = textureLod(sceneTex, tapUV, 0.0);
float w = max(0.001, getCoC(decodeDepth(tap.w), focusDistance));
result += tap.rgb*w; totalWeight += w;
}
```
### Variant 4: TAA Temporal Anti-Aliasing
```glsl
vec4 current = textureLod(currentFrame, uv - jitterOffset/iResolution.xy, 0.0);
vec3 vMin = vec3(1e5), vMax = vec3(-1e5);
for (int iy = -1; iy <= 1; iy++)
for (int ix = -1; ix <= 1; ix++) {
vec3 s = texelFetch(currentFrame, ivec2(fragCoord)+ivec2(ix,iy), 0).rgb;
vMin = min(vMin, s); vMax = max(vMax, s);
}
vec4 history = textureLod(historyBuffer, reprojectToPrevFrame(worldPos, prevViewProjMatrix), 0.0);
float blend = (all(greaterThanEqual(history.rgb, vMin)) && all(lessThanEqual(history.rgb, vMax))) ? 0.9 : 0.0;
color = mix(current.rgb, history.rgb, blend);
```
### Variant 5: Lens Flare + Starburst
```glsl
#define NUM_APERTURE_BLADES 8.0
vec2 toSun = normalize(sunScreenPos - uv);
float angle = atan(toSun.y, toSun.x);
float starburst = pow(0.5+0.5*cos(1.5*3.14159+angle*NUM_APERTURE_BLADES),
max(1.0, 500.0-sunDist*sunDist*501.0));
float ghost = smoothstep(0.015, 0.0, length(ghostCenter-uv)-ghostRadius);
totalFlare += wavelengthToRGB(300.0+fract((length(ghostCenter-uv)-ghostRadius)*5.0)*500.0) * ghost * 0.25;
```
## Performance & Composition
**Performance**: Separable blur 121→22 samples | `textureLod` hardware mipmap for free downsampling | Downsample 2-4x before blurring | Sample counts: MB 16-32, DoF 32-64, CA 4-8 | Inter-texel sampling = free bilinear | `#define` switches have zero cost | Use `mix`/`step`/`smoothstep` instead of branches
**Composition**: Bloom+ToneMap (compute bloom in HDR space then tonemap, not reversible) | TAA+MB+DoF (shared sampling loop) | CA+Vignette+Grain (lens trio) | ColorGrading+ToneMap+Contrast (grade in linear space → HDR compression → gamma-space S-curve) | Bloom+LensFlare (shared bright-pass) | Multi-pass pipeline: BufA scene → BufB/C Bloom H/V → BufD TAA → Image compositing
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/post-processing.md)

View File

@@ -0,0 +1,346 @@
# 2D Procedural Patterns
## Use Cases
- Repeating/aperiodic 2D patterns: grids, hexagons, Truchet, interference patterns, kaleidoscopes, spirals, Lissajous
- Procedural backgrounds, UI textures, sci-fi HUD/radar
- Fractals, water caustics, and other natural phenomena
- Infinite detail, seamless tiling, parameter-driven visual effects
## Core Principles
2D procedural patterns = **domain transforms + distance fields + color mapping**:
1. **Domain repetition**: `fract()`/`mod()` folds the infinite plane into repeating cells
2. **Cell identification**: `floor()` extracts integer coordinates as hash seeds, driving per-cell random variations
3. **Distance field (SDF)**: mathematical functions compute pixel-to-shape distance, `smoothstep` renders edges
4. **Color mapping**: cosine palette `a + b*cos(2pi(c*t+d))` or HSV
5. **Layer compositing**: multi-layer loop results blended via addition/multiplication/`mix`
Key formulas:
```glsl
// UV normalization
uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
// Domain repetition
cell_uv = fract(uv * SCALE) - 0.5;
cell_id = floor(uv * SCALE);
// Cosine palette
col = a + b * cos(6.28318 * (c * t + d));
// Hexagon SDF
hex(p) = max(dot(abs(p), vec2(0.5, 0.866025)), abs(p).x);
// 2D rotation
mat2(cos(a), -sin(a), sin(a), cos(a));
```
## Implementation Steps
### Step 1: UV Normalization
```glsl
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
```
### Step 2: Domain Repetition
```glsl
#define SCALE 4.0
vec2 cell_uv = fract(uv * SCALE) - 0.5;
vec2 cell_id = floor(uv * SCALE);
```
Hexagonal grid domain repetition:
```glsl
const vec2 s = vec2(1, 1.7320508);
vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
vec4 hex_data = dot(h.xy, h.xy) < dot(h.zw, h.zw)
? vec4(h.xy, hC.xy)
: vec4(h.zw, hC.zw + vec2(0.5, 1.0));
```
### Step 3: Per-Cell Randomization
```glsl
float hash21(vec2 p) {
return fract(sin(dot(p, vec2(141.173, 289.927))) * 43758.5453);
}
float rnd = hash21(cell_id);
float radius = 0.15 + 0.1 * rnd;
```
### Step 4: SDF Shape Drawing
```glsl
// Circle
float d = length(cell_uv) - radius;
// Hexagon
float hex_sdf(vec2 p) {
p = abs(p);
return max(dot(p, vec2(0.5, 0.866025)), p.x);
}
// Line segment
float line_sdf(vec2 a, vec2 b, vec2 p) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
// Anti-aliased rendering
float shape = 1.0 - smoothstep(radius - 0.008, radius + 0.008, length(cell_uv));
```
### Step 5: Polar Coordinate Rings/Arcs
```glsl
vec2 polar = vec2(length(uv), atan(uv.y, uv.x));
float ring_id = floor(polar.x * NUM_RINGS + 0.5) / NUM_RINGS;
float ring = 1.0 - pow(abs(sin(polar.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
float arc_end = polar.y + sin(iTime + ring_id * 5.5) * 1.52 - 1.5;
ring *= smoothstep(0.0, 0.05, arc_end);
```
### Step 6: Cosine Palette
```glsl
vec3 palette(float t) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.263, 0.416, 0.557);
return a + b * cos(6.28318 * (c * t + d));
}
```
### Step 7: Iterative Stacking & Glow
```glsl
#define NUM_LAYERS 4.0
vec3 finalColor = vec3(0.0);
vec2 uv0 = uv;
for (float i = 0.0; i < NUM_LAYERS; i++) {
uv = fract(uv * 1.5) - 0.5;
float d = length(uv) * exp(-length(uv0));
vec3 col = palette(length(uv0) + i * 0.4 + iTime * 0.4);
d = sin(d * 8.0 + iTime) / 8.0;
d = abs(d);
d = pow(0.01 / d, 1.2);
finalColor += col * d;
}
```
### Step 8: Trigonometric Interference
```glsl
#define MAX_ITER 5
vec2 p = mod(uv * TAU, TAU) - 250.0;
vec2 i = p;
float c = 1.0;
float inten = 0.005;
for (int n = 0; n < MAX_ITER; n++) {
float t = iTime * (1.0 - 3.5 / float(n + 1));
i = p + vec2(cos(t - i.x) + sin(t + i.y),
sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten),
p.y / (cos(i.y + t) / inten)));
}
c /= float(MAX_ITER);
c = 1.17 - pow(c, 1.4);
vec3 colour = vec3(pow(abs(c), 8.0));
```
### Step 9: Multi-Layer Depth Compositing
```glsl
#define NUM_DEPTH_LAYERS 4.0
float m = 0.0;
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_DEPTH_LAYERS) {
float z = fract(iTime * 0.1 + i);
float size = mix(15.0, 1.0, z);
float fade = smoothstep(0.0, 0.6, z) * smoothstep(1.0, 0.8, z);
m += fade * patternLayer(uv * size, i, iTime);
}
```
### Step 10: Post-Processing
```glsl
col = pow(clamp(col, 0.0, 1.0), vec3(1.0 / 2.2)); // Gamma
col = col * 0.6 + 0.4 * col * col * (3.0 - 2.0 * col); // Contrast S-curve
col = mix(col, vec3(dot(col, vec3(0.33))), -0.4); // Saturation
vec2 q = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.7); // Vignette
```
## Complete Code Template
```glsl
// ====== 2D Procedural Pattern Template ======
// Ready to run in ShaderToy
#define SCALE 3.0
#define NUM_LAYERS 4.0
#define ZOOM_FACTOR 1.5
#define GLOW_WIDTH 0.01
#define GLOW_POWER 1.2
#define WAVE_FREQ 8.0
#define ANIM_SPEED 0.4
#define RING_COUNT 10.0
vec3 palette(float t) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.263, 0.416, 0.557);
return a + b * cos(6.28318 * (c * t + d));
}
float hash21(vec2 p) {
return fract(sin(dot(p, vec2(141.173, 289.927))) * 43758.5453);
}
mat2 rot2(float a) {
float c = cos(a), s = sin(a);
return mat2(c, -s, s, c);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
vec2 uv0 = uv;
vec3 finalColor = vec3(0.0);
for (float i = 0.0; i < NUM_LAYERS; i++) {
uv = fract(uv * ZOOM_FACTOR) - 0.5;
float d = length(uv) * exp(-length(uv0));
vec3 col = palette(length(uv0) + i * 0.4 + iTime * ANIM_SPEED);
d = sin(d * WAVE_FREQ + iTime) / WAVE_FREQ;
d = abs(d);
d = pow(GLOW_WIDTH / d, GLOW_POWER);
finalColor += col * d;
}
finalColor = pow(clamp(finalColor, 0.0, 1.0), vec3(1.0 / 2.2));
finalColor = finalColor * 0.6 + 0.4 * finalColor * finalColor * (3.0 - 2.0 * finalColor);
vec2 q = fragCoord / iResolution.xy;
finalColor *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.7);
fragColor = vec4(finalColor, 1.0);
}
```
## Common Variants
### Variant 1: Hexagonal Truchet Arcs
```glsl
float hex(vec2 p) {
p = abs(p);
return max(dot(p, vec2(0.5, 0.866025)), p.x);
}
const vec2 s = vec2(1.0, 1.7320508);
vec4 getHex(vec2 p) {
vec4 hC = floor(vec4(p, p - vec2(0.5, 1.0)) / s.xyxy) + 0.5;
vec4 h = vec4(p - hC.xy * s, p - (hC.zw + 0.5) * s);
return dot(h.xy, h.xy) < dot(h.zw, h.zw)
? vec4(h.xy, hC.xy)
: vec4(h.zw, hC.zw + vec2(0.5, 1.0));
}
// Truchet triple arcs
float r = 1.0;
vec2 q1 = p - vec2(0.0, r) / s;
vec2 q2 = rot2(6.28318 / 3.0) * p - vec2(0.0, r) / s;
vec2 q3 = rot2(6.28318 * 2.0 / 3.0) * p - vec2(0.0, r) / s;
float d = min(min(length(q1), length(q2)), length(q3));
d = abs(d - 0.288675) - 0.1;
```
### Variant 2: Water Caustic Interference
```glsl
#define TAU 6.28318530718
#define MAX_ITER 5
vec2 p = mod(uv * TAU, TAU) - 250.0;
vec2 i = p;
float c = 1.0;
float inten = 0.005;
for (int n = 0; n < MAX_ITER; n++) {
float t = iTime * (1.0 - 3.5 / float(n + 1));
i = p + vec2(cos(t - i.x) + sin(t + i.y),
sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten),
p.y / (cos(i.y + t) / inten)));
}
c /= float(MAX_ITER);
c = 1.17 - pow(c, 1.4);
vec3 colour = vec3(pow(abs(c), 8.0));
colour = clamp(colour + vec3(0.0, 0.35, 0.5), 0.0, 1.0);
```
### Variant 3: Polar Concentric Ring Arc Segments
```glsl
#define NUM_RINGS 20.0
#define PALETTE vec3(0.0, 1.4, 2.0) + 1.5
vec2 plr = vec2(length(p), atan(p.y, p.x));
float id = floor(plr.x * NUM_RINGS + 0.5) / NUM_RINGS;
p *= rot2(id * 11.0);
p.y = abs(p.y);
float rz = 1.0 - pow(abs(sin(plr.x * 3.14159 * NUM_RINGS)) * 1.25, 2.5);
float arc = plr.y + sin(iTime + id * 5.5) * 1.52 - 1.5;
rz *= smoothstep(0.0, 0.05, arc);
vec3 col = (sin(PALETTE + id * 5.0 + iTime) * 0.5 + 0.5) * rz;
```
### Variant 4: Multi-Layer Depth Parallax Network
```glsl
#define NUM_DEPTH_LAYERS 4.0
vec2 GetPos(vec2 id, vec2 offs, float t) {
float n = hash21(id + offs);
return offs + vec2(sin(t + n * 6.28), cos(t + fract(n * 100.0) * 6.28)) * 0.4;
}
float df_line(vec2 a, vec2 b, vec2 p) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
float m = 0.0;
for (float i = 0.0; i < 1.0; i += 1.0 / NUM_DEPTH_LAYERS) {
float z = fract(iTime * 0.1 + i);
float size = mix(15.0, 1.0, z);
float fade = smoothstep(0.0, 0.6, z) * smoothstep(1.0, 0.8, z);
m += fade * NetLayer(uv * size, i, iTime);
}
```
### Variant 5: Fractal Apollonian
```glsl
float apollian(vec4 p, float s) {
float scale = 1.0;
for (int i = 0; i < 7; ++i) {
p = -1.0 + 2.0 * fract(0.5 * p + 0.5);
float r2 = dot(p, p);
float k = s / r2;
p *= k;
scale *= k;
}
return abs(p.y) / scale;
}
vec4 pp = vec4(p.x, p.y, 0.0, 0.0) + offset;
pp.w = 0.125 * (1.0 - tanh(length(pp.xyz)));
float d = apollian(pp / 4.0, 1.2) * 4.0;
float hue = fract(0.75 * length(p) - 0.3 * iTime) + 0.3;
float sat = 0.75 * tanh(2.0 * length(p));
vec3 col = hsv2rgb(vec3(hue, sat, 1.0));
```
## Performance & Composition
**Performance:**
- Iteration loops are the biggest bottleneck; `NUM_LAYERS` 4->8 halves performance; mobile should use 3 layers or fewer
- Use `step()`/`smoothstep()`/`mix()` instead of `if/else`
- Merge multiple SDFs with `min()`/`max()`, then apply a single `smoothstep`
- Precompute `sin`/`cos` pairs outside loops; write irrational constants as literal values
- `atan` is expensive; use `dot` approximation when only periodicity is needed
- LOD: reduce iterations for distant objects `int iters = int(mix(3.0, float(MAX_ITER), smoothstep(...)));`
- `smoothstep` is often better than `pow` and inherently clamps to [0,1]
**Combinations:**
- **+ Noise**: `d += triangleNoise(uv * 10.0) * 0.05;` for organic erosion feel
- **+ Cross-hatch**: grayscale thresholds + `sin` lines to simulate hand-drawn style
- **+ SDF Boolean**: `min` (union) / `max` (intersection) / subtraction for complex geometry
- **+ Domain distortion**: `uv += 0.05 * vec2(sin(uv.y*5.+iTime), sin(uv.x*3.+iTime));`
- **+ Radial blur**: multi-sample average along polar coordinate direction
- **+ Pseudo-3D lighting**: SDF gradient as normal, add diffuse/specular for embossed look
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/procedural-2d-pattern.md)

View File

@@ -0,0 +1,554 @@
# Procedural Noise Skill
## Use Cases
Procedural noise is the most fundamental technique in real-time GPU graphics. It applies to natural phenomena (fire, clouds, water, lava), terrain generation, texture synthesis, volume rendering, motion effects, and more.
Core idea: use mathematical functions to generate pseudo-random, spatially continuous signals on the GPU in real time, then produce multi-scale detail through FBM and domain warping.
## Core Principles
### Noise Functions
Generate random values at integer lattice points, then smoothly interpolate between them.
- **Value Noise**: random scalars at lattice points + bilinear Hermite interpolation. `N(p) = mix(mix(h00,h10,u), mix(h01,h11,u), v)`
- **Simplex Noise**: triangular lattice gradient dot products + radial falloff kernel. Skew `K1=(sqrt(3)-1)/2`, unskew `K2=(3-sqrt(3))/6`. Fewer lattice lookups, no axis-aligned artifacts.
### Hash Functions
Map integer coordinates to pseudo-random values:
- **sin-based** (short but precision-sensitive): `fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453)`
- **sin-free** (cross-platform stable): `fract(p * 0.1031)` + dot mixing + fract
### FBM (Fractal Brownian Motion)
Multi-octave noise summation: `FBM(p) = sum of amplitude_i * noise(frequency_i * p)`
- Lacunarity ~2.0, Gain ~0.5, inter-octave rotation to eliminate artifacts
### Domain Warping
Feed noise output back as coordinate offset: `fbm(p + fbm(p))` or cascaded `fbm(p + fbm(p + fbm(p)))`
### FBM Variant Quick Reference
| Variant | Formula | Effect |
|---------|---------|--------|
| Standard | `sum a*noise(p)` | Soft clouds |
| Ridged | `sum a*abs(noise(p))` | Sharp ridges/lightning |
| Sinusoidal ridged | `sum a*sin(noise(p)*k)` | Periodic ridges/lava |
| Erosion | `sum a*noise(p)/(1+dot(d,d))` | Realistic terrain |
| Ocean waves | `sum a*sea_octave(p)` | Peaked wave crests |
## Implementation Code
### Hash Functions
```glsl
// Sin-free hash (Dave Hoskins) — cross-platform stable
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * .1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
// Sin hash — shorter code, precision-sensitive on some GPUs
float hash(vec2 p) {
float h = dot(p, vec2(127.1, 311.7));
return fract(sin(h) * 43758.5453123);
}
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
```
### Value Noise
```glsl
// Hermite smooth bilinear interpolation
float noise(in vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
float a = hash(p + vec2(0.0, 0.0));
float b = hash(p + vec2(1.0, 0.0));
float c = hash(p + vec2(0.0, 1.0));
float d = hash(p + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
```
### Simplex Noise
```glsl
// 2D Simplex (skewed triangular grid + h^4 falloff kernel)
float noise(in vec2 p) {
const float K1 = 0.366025404; // (sqrt(3)-1)/2
const float K2 = 0.211324865; // (3-sqrt(3))/6
vec2 i = floor(p + (p.x + p.y) * K1);
vec2 a = p - i + (i.x + i.y) * K2;
vec2 o = (a.x > a.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec2 b = a - o + K2;
vec2 c = a - 1.0 + 2.0 * K2;
vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
vec3 n = h * h * h * h * vec3(
dot(a, hash2(i + 0.0)),
dot(b, hash2(i + o)),
dot(c, hash2(i + 1.0))
);
return dot(n, vec3(70.0));
}
```
### Standard FBM
```glsl
#define OCTAVES 4
#define GAIN 0.5
mat2 m = mat2(1.6, 1.2, -1.2, 1.6); // rotation+scale, |m|=2.0, ~36.87 deg
float fbm(vec2 p) {
float f = 0.0, a = 0.5;
for (int i = 0; i < OCTAVES; i++) {
f += a * noise(p);
p = m * p;
a *= GAIN;
}
return f;
}
```
Manually unrolled version (slightly varying lacunarity to break self-similarity):
```glsl
const mat2 mtx = mat2(0.80, 0.60, -0.60, 0.80);
float fbm4(vec2 p) {
float f = 0.0;
f += 0.5000 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.02;
f += 0.2500 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.03;
f += 0.1250 * (-1.0 + 2.0 * noise(p)); p = mtx * p * 2.01;
f += 0.0625 * (-1.0 + 2.0 * noise(p));
return f / 0.9375;
}
```
### Ridged FBM
```glsl
// abs() produces V-shaped ridges at zero crossings
float fbm_ridged(in vec2 p) {
float z = 2.0, rz = 0.0;
for (float i = 1.0; i < 6.0; i++) {
rz += abs((noise(p) - 0.5) * 2.0) / z;
z *= 2.0;
p *= 2.0;
}
return rz;
}
// Sinusoidal ridged variant — lava texture
// rz += (sin(noise(p) * 7.0) * 0.5 + 0.5) / z;
```
### Domain Warping
```glsl
// Basic domain warping ("2D Clouds")
float q = fbm(uv * 0.5);
uv -= q - time;
float f = fbm(uv);
// Classic three-level cascade
vec2 fbm4_2(vec2 p) {
return vec2(fbm4(p + vec2(1.0)), fbm4(p + vec2(6.2)));
}
float func(vec2 q, out vec2 o, out vec2 n) {
o = 0.5 + 0.5 * fbm4_2(q);
n = fbm6_2(4.0 * o);
vec2 p = q + 2.0 * n + 1.0;
float f = 0.5 + 0.5 * fbm4(2.0 * p);
f = mix(f, f * f * f * 3.5, f * abs(n.x));
return f;
}
// Dual-axis domain warping
float dualfbm(in vec2 p) {
vec2 p2 = p * 0.7;
vec2 basis = vec2(fbm(p2 - time * 1.6), fbm(p2 + time * 1.7));
basis = (basis - 0.5) * 0.2;
p += basis;
return fbm(p * makem2(time * 0.2));
}
```
### Fluid Noise
```glsl
// Per-octave gradient displacement simulating fluid transport
#define FLOW_SPEED 0.6
#define BASE_SPEED 1.9
#define ADVECTION 0.77
#define GRAD_SCALE 0.5
vec2 gradn(vec2 p) {
float ep = 0.09;
float gradx = noise(vec2(p.x + ep, p.y)) - noise(vec2(p.x - ep, p.y));
float grady = noise(vec2(p.x, p.y + ep)) - noise(vec2(p.x, p.y - ep));
return vec2(gradx, grady);
}
float flow(in vec2 p) {
float z = 2.0, rz = 0.0;
vec2 bp = p;
for (float i = 1.0; i < 7.0; i++) {
p += time * FLOW_SPEED;
bp += time * BASE_SPEED;
vec2 gr = gradn(i * p * 0.34 + time * 1.0);
gr *= makem2(time * 6.0 - (0.05 * p.x + 0.03 * p.y) * 40.0);
p += gr * GRAD_SCALE;
rz += (sin(noise(p) * 7.0) * 0.5 + 0.5) / z;
p = mix(bp, p, ADVECTION);
z *= 1.4;
p *= 2.0;
bp *= 1.9;
}
return rz;
}
```
### Derivative FBM
```glsl
// Value noise with analytic derivatives
vec3 noised(in vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 u = f * f * (3.0 - 2.0 * f);
vec2 du = 6.0 * f * (1.0 - f);
float a = hash(p + vec2(0, 0));
float b = hash(p + vec2(1, 0));
float c = hash(p + vec2(0, 1));
float d = hash(p + vec2(1, 1));
return vec3(
a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y,
du * (vec2(b - a, c - a) + (a - b - c + d) * u.yx)
);
}
// Erosion FBM: higher gradient = lower contribution
float terrainFBM(in vec2 x) {
const mat2 m2 = mat2(0.8, -0.6, 0.6, 0.8);
float a = 0.0, b = 1.0;
vec2 d = vec2(0.0);
for (int i = 0; i < 16; i++) {
vec3 n = noised(x);
d += n.yz;
a += b * n.x / (1.0 + dot(d, d)); // 1/(1+|grad|^2) erosion factor
b *= 0.5;
x = m2 * x * 2.0;
}
return a;
}
```
### Quintic Noise with Analytical Derivatives
C2-continuous noise using quintic interpolation — eliminates visible grid artifacts in derivatives:
```glsl
// Returns vec3(value, dFdx, dFdy) — derivatives are exact, not finite-differenced
vec3 noisedQ(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// Quintic interpolation for C2 continuity
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0);
float a = hash12(i + vec2(0.0, 0.0));
float b = hash12(i + vec2(1.0, 0.0));
float c = hash12(i + vec2(0.0, 1.0));
float d = hash12(i + vec2(1.0, 1.0));
float k0 = a, k1 = b - a, k2 = c - a, k3 = a - b - c + d;
return vec3(
k0 + k1 * u.x + k2 * u.y + k3 * u.x * u.y, // value
du * vec2(k1 + k3 * u.y, k2 + k3 * u.x) // derivatives
);
}
```
### FBM with Derivatives (Erosion Terrain)
Accumulates derivatives across octaves — derivative magnitude dampens amplitude, creating realistic erosion patterns:
```glsl
vec3 fbmDerivative(vec2 p, int octaves) {
float value = 0.0;
vec2 deriv = vec2(0.0);
float amplitude = 0.5;
float frequency = 1.0;
mat2 rot = mat2(0.8, 0.6, -0.6, 0.8); // inter-octave rotation
for (int i = 0; i < octaves; i++) {
vec3 n = noisedQ(p * frequency);
deriv += n.yz;
// Key: divide by (1 + dot(deriv, deriv)) for erosion effect
value += amplitude * n.x / (1.0 + dot(deriv, deriv));
frequency *= 2.0;
amplitude *= 0.5;
p = rot * p; // rotate to break axis-aligned artifacts
}
return vec3(value, deriv);
}
```
Key insights:
- **Quintic interpolation**: `6t^5 - 15t^4 + 10t^3` gives C2 continuous noise (vs Hermite's C1), eliminating visible grid artifacts in derivatives
- **Erosion FBM**: The `1/(1+dot(d,d))` term causes flat areas to accumulate more detail while steep slopes stay smooth — mimicking real erosion
- **Inter-octave rotation**: The 2x2 rotation matrix between octaves prevents axis-aligned patterns especially visible in ridged noise
### Voronoise (Voronoi-Noise Hybrid)
Unified interpolation between value noise and Voronoi patterns:
```glsl
// u=0: Value noise, u=1: Voronoi, v: smoothness (0=sharp cells, 1=smooth)
vec3 hash32(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
p3 += dot(p3, p3.yxz + 33.33);
return fract((p3.xxy + p3.yzz) * p3.zyx);
}
float voronoise(vec2 p, float u, float v) {
float k = 1.0 + 63.0 * pow(1.0 - v, 6.0);
vec2 i = floor(p);
vec2 f = fract(p);
vec2 a = vec2(0.0);
for (int y = -2; y <= 2; y++)
for (int x = -2; x <= 2; x++) {
vec2 g = vec2(float(x), float(y));
vec3 o = hash32(i + g) * vec3(u, u, 1.0);
vec2 d = g - f + o.xy;
float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k);
a += vec2(o.z * w, w);
}
return a.x / a.y;
}
```
Extremely versatile — smoothly interpolates between cellular Voronoi and continuous noise.
### Preventing Aliasing in Procedural Textures
For distant surfaces, high-frequency noise octaves create moiré artifacts. Solutions:
1. **LOD-based octave count**: `int octaves = min(MAX_OCTAVES, int(log2(pixelSize)))` — skip octaves finer than pixel size
2. **Analytical filtering**: For simple patterns (checkers, stripes), use smoothstep with pixel width: `smoothstep(-fw, fw, pattern)` where `fw = fwidth(uv)`
3. **Derivative-based mip**: Use `textureGrad()` with manually computed ray differentials for texture lookups in ray-marched scenes (see texture-mapping-advanced technique)
## Complete Code Template
Ready to run in ShaderToy. Switch between standard FBM / ridged FBM / domain warping modes via `#define`:
```glsl
// ============================================================
// Procedural Noise Skill — Complete Template
// ============================================================
// ========== Mode selection (uncomment to switch) ==========
#define MODE_STANDARD_FBM // Standard FBM clouds
//#define MODE_RIDGED_FBM // Ridged FBM lightning texture
//#define MODE_DOMAIN_WARP // Domain warped organic pattern
// ========== Tunable parameters ==========
#define OCTAVES 6
#define GAIN 0.5
#define LACUNARITY 2.0
#define NOISE_SCALE 3.0
#define ANIM_SPEED 0.3
#define WARP_STRENGTH 0.4
// ========== Hash function ==========
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
// ========== Value noise ==========
float noise(in vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
float a = hash(p + vec2(0.0, 0.0));
float b = hash(p + vec2(1.0, 0.0));
float c = hash(p + vec2(0.0, 1.0));
float d = hash(p + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// ========== Rotation+scale matrix ==========
const mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
// ========== Standard FBM ==========
float fbm(vec2 p) {
float f = 0.0, a = 0.5;
for (int i = 0; i < OCTAVES; i++) {
f += a * (-1.0 + 2.0 * noise(p));
p = m * p;
a *= GAIN;
}
return f;
}
// ========== Ridged FBM ==========
float fbm_ridged(vec2 p) {
float f = 0.0, a = 0.5;
for (int i = 0; i < OCTAVES; i++) {
f += a * abs(-1.0 + 2.0 * noise(p));
p = m * p;
a *= GAIN;
}
return f;
}
// ========== Domain warping vec2 FBM ==========
vec2 fbm2(vec2 p) {
return vec2(fbm(p + vec2(1.7, 9.2)), fbm(p + vec2(8.3, 2.8)));
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
uv *= NOISE_SCALE;
float time = iTime * ANIM_SPEED;
float f = 0.0;
vec3 col = vec3(0.0);
#ifdef MODE_STANDARD_FBM
f = 0.5 + 0.5 * fbm(uv + vec2(0.0, -time));
vec3 sky = mix(vec3(0.4, 0.7, 1.0), vec3(0.2, 0.4, 0.6), fragCoord.y / iResolution.y);
vec3 cloud = vec3(1.1, 1.1, 0.9) * f;
col = mix(sky, cloud, smoothstep(0.4, 0.7, f));
#endif
#ifdef MODE_RIDGED_FBM
f = fbm_ridged(uv + vec2(time * 0.5, time * 0.3));
col = vec3(0.2, 0.1, 0.4) / max(f, 0.05);
col = pow(col, vec3(0.99));
#endif
#ifdef MODE_DOMAIN_WARP
vec2 q = fbm2(uv + time * 0.1);
vec2 r = fbm2(uv + WARP_STRENGTH * q + vec2(1.7, 9.2));
f = 0.5 + 0.5 * fbm(uv + WARP_STRENGTH * r);
f = mix(f, f * f * f * 3.5, f * length(r));
col = vec3(0.2, 0.1, 0.4);
col = mix(col, vec3(0.3, 0.05, 0.05), f);
col = mix(col, vec3(0.9, 0.9, 0.9), dot(r, r));
col = mix(col, vec3(0.5, 0.2, 0.2), 0.5 * q.y * q.y);
col *= f * 2.0;
vec2 eps = vec2(1.0 / iResolution.x, 0.0);
float fx = 0.5 + 0.5 * fbm(uv + eps.xy + WARP_STRENGTH * fbm2(uv + eps.xy + time * 0.1));
float fy = 0.5 + 0.5 * fbm(uv + eps.yx + WARP_STRENGTH * fbm2(uv + eps.yx + time * 0.1));
vec3 nor = normalize(vec3(fx - f, eps.x, fy - f));
vec3 lig = normalize(vec3(0.9, -0.2, -0.4));
float dif = clamp(0.3 + 0.7 * dot(nor, lig), 0.0, 1.0);
col *= vec3(0.85, 0.90, 0.95) * (nor.y * 0.5 + 0.5) + vec3(0.15, 0.10, 0.05) * dif;
#endif
vec2 p = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * sqrt(16.0 * p.x * p.y * (1.0 - p.x) * (1.0 - p.y));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Ridged FBM
```glsl
f += a * abs(noise(p)); // V-shaped ridges
f += a * (sin(noise(p)*7.0)*0.5+0.5); // Sinusoidal ridges (lava)
```
### Domain Warped FBM
```glsl
vec2 o = 0.5 + 0.5 * vec2(fbm(q + vec2(1.0)), fbm(q + vec2(6.2)));
vec2 n = vec2(fbm(4.0 * o + vec2(9.2)), fbm(4.0 * o + vec2(5.7)));
float f = 0.5 + 0.5 * fbm(q + 2.0 * n + 1.0);
```
### Derivative Erosion FBM
```glsl
vec2 d = vec2(0.0);
for (int i = 0; i < N; i++) {
vec3 n = noised(p);
d += n.yz;
a += b * n.x / (1.0 + dot(d, d));
b *= 0.5; p = m2 * p * 2.0;
}
```
### Fluid Noise
```glsl
for (float i = 1.0; i < 7.0; i++) {
vec2 gr = gradn(i * p * 0.34 + time);
gr *= makem2(time * 6.0 - (0.05*p.x+0.03*p.y)*40.0);
p += gr * 0.5;
rz += (sin(noise(p)*7.0)*0.5+0.5) / z;
p = mix(bp, p, 0.77);
}
```
### Ocean Wave Octave Function
```glsl
float sea_octave(vec2 uv, float choppy) {
uv += noise(uv);
vec2 wv = 1.0 - abs(sin(uv));
vec2 swv = abs(cos(uv));
wv = mix(wv, swv, wv);
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}
// Bidirectional propagation in FBM:
d = sea_octave((uv + SEA_TIME) * freq, choppy);
d += sea_octave((uv - SEA_TIME) * freq, choppy);
choppy = mix(choppy, 1.0, 0.2);
```
## Performance & Composition
**Performance optimization:**
- Reducing octave count is the most direct optimization; use fewer octaves for distant objects: `int oct = 5 - int(log2(1.0 + t * 0.5));`
- Multi-level LOD: `terrainL` (3 oct) / `terrainM` (9 oct) / `terrainH` (16 oct)
- Texture sampling instead of math hash: `texture(iChannel0, x * 0.01).x`
- Manually unroll small loops + slightly vary lacunarity
- Adaptive step size: `float dt = max(0.05, 0.02 * t);`
- Directional derivative instead of full gradient (1 sample vs 3)
- Early termination: `if (sum.a > 0.99) break;`
**Common combinations:**
- FBM + Raymarching: noise-driven height/density fields, ray marching for intersection (terrain/ocean)
- FBM + finite-difference normals + lighting: `nor = normalize(vec3(f(p+ex)-f(p), eps, f(p+ey)-f(p)))`
- FBM + color mapping: different power curves mapping to RGB, e.g. flame `vec3(1.5*c, 1.5*c^3, c^6)` or inverse `vec3(k)/rz`
- FBM + Fresnel water surface: `fresnel = pow(1.0 - dot(n, -eye), 3.0)`
- Multi-layer FBM compositing: shape layer (low freq) + ridged layer (mid freq) + color layer (high freq)
- FBM + volumetric lighting: density difference along light direction approximates illumination
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/procedural-noise.md)

View File

@@ -0,0 +1,467 @@
# Ray Marching
## Use Cases
- Rendering implicit surfaces (geometry defined by mathematical functions) without triangle meshes
- Creating fractals, organic forms, liquid metal, and other shapes difficult to express with traditional modeling
- Implementing volumetric effects: fire, smoke, clouds, glow
- Rapid prototyping of procedural scenes: building complex scenes by combining SDF primitives with boolean operations
- Advanced distance-field-based lighting: soft shadows, ambient occlusion, subsurface scattering
## Core Principles
Cast a ray from the camera along each pixel direction, advancing step by step using a **Signed Distance Function (SDF)** (Sphere Tracing). Each step advances by the SDF value at the current point, guaranteeing no surface penetration.
- Ray equation: `P(t) = ro + t * rd`
- Stepping logic: `t += SDF(P(t))`
- Hit test: `SDF(P) < epsilon`
- Normal estimation: `N = normalize(gradient of SDF(P))` (direction of the SDF gradient)
- Volumetric rendering: advance at fixed step size, accumulating density and color per step (front-to-back compositing)
## Implementation Steps
### Step 1: UV Normalization and Ray Direction
```glsl
// Concise version
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
vec3 ro = vec3(0.0, 0.0, -3.0);
vec3 rd = normalize(vec3(uv, 1.0)); // z=1.0 ~ 90 deg FOV
// Precise FOV control
vec2 xy = fragCoord - iResolution.xy / 2.0;
float z = iResolution.y / tan(radians(FOV) / 2.0);
vec3 rd = normalize(vec3(xy, -z));
```
### Step 2: Camera Matrix (Look-At)
```glsl
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
mat3 ca = setCamera(ro, ta, 0.0);
vec3 rd = ca * normalize(vec3(uv, FOCAL_LENGTH)); // 1.0~3.0, larger = narrower FOV
```
### Step 3: Scene SDF
```glsl
// SDF primitives
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float sdTorus(vec3 p, vec2 t) {
return length(vec2(length(p.xz) - t.x, p.y)) - t.y;
}
// Boolean operations
float opUnion(float a, float b) { return min(a, b); }
float opSubtraction(float a, float b) { return max(a, -b); }
float opIntersection(float a, float b) { return max(a, b); }
// Smooth blending, adjustable k: 0.1~0.5
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
// Scene composition
float map(vec3 p) {
float d = sdSphere(p - vec3(0.0, 0.5, 0.0), 0.5);
d = opUnion(d, p.y); // ground
d = smin(d, sdBox(p - vec3(1.0, 0.3, 0.0), vec3(0.3)), 0.2); // smooth blend with box
return d;
}
```
### Step 4: Ray Marching Loop
```glsl
#define MAX_STEPS 128
#define MAX_DIST 100.0
#define SURF_DIST 0.001
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + t * rd;
float d = map(p);
if (d < SURF_DIST) return t;
t += d;
if (t > MAX_DIST) break;
}
return -1.0;
}
```
### Step 5: Normal Estimation
```glsl
// Central differences (6 SDF evaluations)
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
// Tetrahedral trick (4 SDF evaluations, recommended)
vec3 calcNormal(vec3 pos) {
vec3 n = vec3(0.0);
for (int i = 0; i < 4; i++) {
vec3 e = 0.5773 * (2.0 * vec3((((i+3)>>1)&1), ((i>>1)&1), (i&1)) - 1.0);
n += e * map(pos + 0.001 * e);
}
return normalize(n);
}
```
### Step 6: Lighting and Shading
```glsl
vec3 shade(vec3 p, vec3 rd) {
vec3 nor = calcNormal(p);
vec3 lightDir = normalize(vec3(0.6, 0.35, 0.5));
vec3 halfDir = normalize(lightDir - rd);
float diff = clamp(dot(nor, lightDir), 0.0, 1.0);
float spec = pow(clamp(dot(nor, halfDir), 0.0, 1.0), SHININESS); // 8~64
float sky = sqrt(clamp(0.5 + 0.5 * nor.y, 0.0, 1.0));
vec3 col = vec3(0.2, 0.2, 0.25);
vec3 lin = vec3(0.0);
lin += diff * vec3(1.3, 1.0, 0.7) * 2.2;
lin += sky * vec3(0.4, 0.6, 1.15) * 0.6;
lin += vec3(0.25) * 0.55;
col *= lin;
col += spec * vec3(1.3, 1.0, 0.7) * 5.0;
return col;
}
```
### Step 7: Post-Processing
```glsl
col = pow(col, vec3(0.4545)); // Gamma correction (1/2.2)
col = col / (1.0 + col); // Reinhard tone mapping (optional, before gamma)
// Vignette (optional)
vec2 q = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.25);
```
## Full Code Template
Can be pasted directly into ShaderToy. Includes SDF scene, Phong lighting, soft shadows, and ambient occlusion:
```glsl
// ============================================================
// Ray Marching Full Template — ShaderToy
// ============================================================
#define MAX_STEPS 128
#define MAX_DIST 100.0
#define SURF_DIST 0.001
#define SHADOW_STEPS 24
#define AO_STEPS 5
#define FOCAL_LENGTH 2.5
#define SHININESS 16.0
// --- SDF Primitives ---
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float sdTorus(vec3 p, vec2 t) {
return length(vec2(length(p.xz) - t.x, p.y)) - t.y;
}
// --- Boolean Operations ---
float opUnion(float a, float b) { return min(a, b); }
float opSubtraction(float a, float b) { return max(a, -b); }
float opIntersection(float a, float b) { return max(a, b); }
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
mat2 rot2D(float a) {
float c = cos(a), s = sin(a);
return mat2(c, -s, s, c);
}
// --- Scene Definition ---
float map(vec3 p) {
float ground = p.y;
vec3 q = p - vec3(0.0, 0.8, 0.0);
q.xz *= rot2D(iTime * 0.5);
float body = smin(sdSphere(q, 0.5), sdTorus(q, vec2(0.8, 0.15)), 0.3);
return opUnion(ground, body);
}
// --- Normal (Tetrahedral Trick) ---
vec3 calcNormal(vec3 pos) {
vec3 n = vec3(0.0);
for (int i = min(iFrame,0); i < 4; i++) {
vec3 e = 0.5773 * (2.0 * vec3((((i+3)>>1)&1), ((i>>1)&1), (i&1)) - 1.0);
n += e * map(pos + 0.001 * e);
}
return normalize(n);
}
// --- Soft Shadows ---
float calcSoftShadow(vec3 ro, vec3 rd, float tmin, float tmax) {
float res = 1.0, t = tmin;
for (int i = 0; i < SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
float s = clamp(8.0 * h / t, 0.0, 1.0);
res = min(res, s);
t += clamp(h, 0.01, 0.2);
if (res < 0.004 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
// --- Ambient Occlusion ---
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0, sca = 1.0;
for (int i = 0; i < AO_STEPS; i++) {
float h = 0.01 + 0.12 * float(i) / float(AO_STEPS - 1);
float d = map(pos + h * nor);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
// --- Ray March ---
float rayMarch(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + t * rd;
float d = map(p);
if (abs(d) < SURF_DIST * (1.0 + t * 0.1)) return t;
t += d;
if (t > MAX_DIST) break;
}
return -1.0;
}
// --- Camera ---
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
// --- Rendering ---
vec3 render(vec3 ro, vec3 rd) {
vec3 col = vec3(0.7, 0.7, 0.9) - max(rd.y, 0.0) * 0.3; // sky
float t = rayMarch(ro, rd);
if (t > 0.0) {
vec3 pos = ro + t * rd;
vec3 nor = calcNormal(pos);
// Material
vec3 mate = vec3(0.18);
if (pos.y < 0.001) {
float f = mod(floor(pos.x) + floor(pos.z), 2.0);
mate = vec3(0.1 + 0.05 * f);
} else {
mate = 0.2 + 0.2 * sin(vec3(0.0, 1.0, 2.0));
}
// Lighting
vec3 lightDir = normalize(vec3(-0.5, 0.4, -0.6));
float occ = calcAO(pos, nor);
float dif = clamp(dot(nor, lightDir), 0.0, 1.0);
dif *= calcSoftShadow(pos + nor * 0.01, lightDir, 0.02, 2.5);
vec3 hal = normalize(lightDir - rd);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), SHININESS) * dif;
float sky = sqrt(clamp(0.5 + 0.5 * nor.y, 0.0, 1.0));
vec3 lin = vec3(0.0);
lin += dif * vec3(1.3, 1.0, 0.7) * 2.2;
lin += sky * vec3(0.4, 0.6, 1.15) * 0.6 * occ;
lin += vec3(0.25) * 0.55 * occ;
col = mate * lin;
col += spe * vec3(1.3, 1.0, 0.7) * 5.0;
col = mix(col, vec3(0.7, 0.7, 0.9), 1.0 - exp(-0.0001 * t * t * t)); // distance fog
}
return clamp(col, 0.0, 1.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
float time = 32.0 + iTime * 1.5;
vec2 mo = iMouse.xy / iResolution.xy;
vec3 ta = vec3(0.0, 0.5, 0.0);
vec3 ro = ta + vec3(4.0*cos(0.1*time+7.0*mo.x), 1.5, 4.0*sin(0.1*time+7.0*mo.x));
mat3 ca = setCamera(ro, ta, 0.0);
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
vec3 rd = ca * normalize(vec3(uv, FOCAL_LENGTH));
vec3 col = render(ro, rd);
col = pow(col, vec3(0.4545));
vec2 q = fragCoord / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * q.x * q.y * (1.0 - q.x) * (1.0 - q.y), 0.25);
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### 1. Volumetric Ray Marching
Advance at fixed step size, accumulating density/color per step. Used for fire, smoke, and clouds.
```glsl
#define VOL_STEPS 150
#define VOL_STEP_SIZE 0.05
float fbmDensity(vec3 p) {
float den = 0.2 - p.y;
vec3 q = p - vec3(0.0, 1.0, 0.0) * iTime;
float f = 0.5000 * noise(q); q = q * 2.02 - vec3(0.0, 1.0, 0.0) * iTime;
f += 0.2500 * noise(q); q = q * 2.03 - vec3(0.0, 1.0, 0.0) * iTime;
f += 0.1250 * noise(q); q = q * 2.01 - vec3(0.0, 1.0, 0.0) * iTime;
f += 0.0625 * noise(q);
return den + 4.0 * f;
}
vec3 volumetricMarch(vec3 ro, vec3 rd) {
vec4 sum = vec4(0.0);
float t = 0.05;
for (int i = 0; i < VOL_STEPS; i++) {
vec3 pos = ro + t * rd;
float den = fbmDensity(pos);
if (den > 0.0) {
den = min(den, 1.0);
vec3 col = mix(vec3(1.0,0.5,0.05), vec3(0.48,0.53,0.5), clamp(pos.y*0.5,0.0,1.0));
col *= den; col.a = den * 0.6; col.rgb *= col.a;
sum += col * (1.0 - sum.a);
if (sum.a > 0.99) break;
}
t += VOL_STEP_SIZE;
}
return clamp(sum.rgb, 0.0, 1.0);
}
```
### 2. CSG Scene Construction
```glsl
float sceneSDF(vec3 p) {
p = rotateY(iTime * 0.5) * p;
float sphere = sdSphere(p, 1.2);
float cube = sdBox(p, vec3(0.9));
float cyl = sdCylinder(p, vec2(0.4, 2.0));
float cylX = sdCylinder(p.yzx, vec2(0.4, 2.0));
float cylZ = sdCylinder(p.xzy, vec2(0.4, 2.0));
return opSubtraction(opIntersection(sphere, cube), opUnion(cyl, opUnion(cylX, cylZ)));
}
```
### 3. Physically-Based Volumetric Scattering
```glsl
void getParticipatingMedia(out float sigmaS, out float sigmaE, vec3 pos) {
float heightFog = 0.3 * clamp((7.0 - pos.y), 0.0, 1.0);
sigmaS = 0.02 + heightFog;
sigmaE = max(0.000001, sigmaS);
}
vec3 S = lightColor * sigmaS * phaseFunction() * volShadow;
vec3 Sint = (S - S * exp(-sigmaE * stepLen)) / sigmaE;
scatteredLight += transmittance * Sint;
transmittance *= exp(-sigmaE * stepLen);
```
### 4. Glow Accumulation
```glsl
vec2 rayMarchWithGlow(vec3 ro, vec3 rd) {
float t = 0.0, dMin = MAX_DIST;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 p = ro + t * rd;
float d = map(p);
if (d < dMin) dMin = d;
if (d < SURF_DIST) break;
t += d;
if (t > MAX_DIST) break;
}
return vec2(t, dMin);
}
float glow = 0.02 / max(dMin, 0.001);
col += glow * vec3(1.0, 0.8, 0.9);
```
### 5. Refraction and Bidirectional Marching
```glsl
float castRay(vec3 ro, vec3 rd) {
float sign = (map(ro) < 0.0) ? -1.0 : 1.0;
float t = 0.0;
for (int i = 0; i < 120; i++) {
float h = sign * map(ro + rd * t);
if (abs(h) < 0.0001 || t > 12.0) break;
t += h;
}
return t;
}
vec3 refDir = refract(rd, nor, IOR); // IOR: index of refraction, e.g. 0.9
float t2 = 2.0;
for (int i = 0; i < 50; i++) {
float h = map(hitPos + refDir * t2);
t2 -= h;
if (abs(h) > 3.0) break;
}
vec3 nor2 = calcNormal(hitPos + refDir * t2);
```
## Performance & Composition
**Performance tips:**
- Use tetrahedral trick for normals (4 SDF evaluations instead of 6)
- `min(iFrame,0)` as loop start value to prevent compiler unrolling
- AABB bounding box pre-test to skip empty regions
- Adaptive hit threshold: `SURF_DIST * (1.0 + t * 0.1)`
- Step clamping: `t += clamp(h, 0.01, 0.2)`
- Early exit for volumetric rendering when `sum.a > 0.99`
- Use cheap bounding SDF first, then compute precise SDF
**Composition directions:**
- + FBM noise: terrain/rock texture, cloud/smoke volumetric density fields
- + Domain transforms (twist/bend/repeat): infinite repeating corridors, surreal geometry
- + PBR materials (Cook-Torrance BRDF + Fresnel + environment mapping)
- + Multi-pass post-processing: depth of field, motion blur, tone mapping
- + Procedural animation: time-driven SDF parameters + smoothstep easing
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/ray-marching.md)

View File

@@ -0,0 +1,631 @@
# 2D SDF Rendering Skill
## Use Cases
- 2D shape rendering: circles, rectangles, triangles, ellipses, line segments, Bezier curves, etc.
- UI elements and icons: drawn with math functions, naturally resolution-independent
- Anti-aliased graphics, shape boolean operations, outlines and glow
- Motion graphics and animation, 2D soft shadows and lighting
## Core Principles
For each pixel, compute the signed distance `d` to the shape boundary: `d < 0` inside, `d = 0` boundary, `d > 0` outside.
Map to color via `smoothstep`/`clamp`:
- **Fill**: color when `d < 0`
- **Anti-aliasing**: `smoothstep(-aa, aa, d)`
- **Stroke**: apply smoothstep to `abs(d) - strokeWidth`
- **Boolean operations**: `min(d1, d2)` union, `max(d1, d2)` intersection, `max(-d1, d2)` subtraction
Key formulas:
```
Circle: d = length(p - center) - radius
Rectangle: d = length(max(abs(p) - halfSize, 0.0)) + min(max(abs(p).x - halfSize.x, abs(p).y - halfSize.y), 0.0)
Line segment: d = length(p - a - clamp(dot(p-a, b-a)/dot(b-a, b-a), 0, 1) * (b-a)) - width/2
Smooth union: d = mix(d2, d1, h) - k*h*(1-h), h = clamp(0.5 + 0.5*(d2-d1)/k, 0, 1)
```
## Implementation Steps
### Step 1: Coordinate Normalization
```glsl
// Origin at center, y range [-1, 1] (standard approach)
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// Pixel space (suitable for fixed pixel-size UI)
vec2 p = fragCoord.xy;
vec2 center = iResolution.xy * 0.5;
// [0, 1] range (requires manual aspect ratio handling)
vec2 uv = fragCoord.xy / iResolution.xy;
```
### Step 2: SDF Primitive Functions
```glsl
float sdCircle(vec2 p, float radius) {
return length(p) - radius;
}
// halfSize is half-width/half-height, radius is corner rounding
float sdBox(vec2 p, vec2 halfSize, float radius) {
halfSize -= vec2(radius);
vec2 d = abs(p) - halfSize;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - radius;
}
float sdLine(vec2 p, vec2 start, vec2 end, float width) {
vec2 dir = end - start;
float h = clamp(dot(p - start, dir) / dot(dir, dir), 0.0, 1.0);
return length(p - start - dir * h) - width * 0.5;
}
// Exact signed distance, requires only one sqrt
float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
vec2 e0 = p1 - p0, v0 = p - p0;
vec2 e1 = p2 - p1, v1 = p - p1;
vec2 e2 = p0 - p2, v2 = p - p2;
float d0 = dot(v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0),
v0 - e0 * clamp(dot(v0, e0) / dot(e0, e0), 0.0, 1.0));
float d1 = dot(v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0),
v1 - e1 * clamp(dot(v1, e1) / dot(e1, e1), 0.0, 1.0));
float d2 = dot(v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0),
v2 - e2 * clamp(dot(v2, e2) / dot(e2, e2), 0.0, 1.0));
float o = e0.x * e2.y - e0.y * e2.x;
vec2 d = min(min(vec2(d0, o * (v0.x * e0.y - v0.y * e0.x)),
vec2(d1, o * (v1.x * e1.y - v1.y * e1.x))),
vec2(d2, o * (v2.x * e2.y - v2.y * e2.x)));
return -sqrt(d.x) * sign(d.y);
}
// Approximate ellipse SDF
float sdEllipse(vec2 p, vec2 center, float a, float b) {
float a2 = a * a, b2 = b * b;
vec2 d = p - center;
return (b2 * d.x * d.x + a2 * d.y * d.y - a2 * b2) / (a2 * b2);
}
```
### Step 3: CSG Boolean Operations
```glsl
float opUnion(float d1, float d2) { return min(d1, d2); }
float opIntersect(float d1, float d2) { return max(d1, d2); }
float opSubtract(float d1, float d2) { return max(-d1, d2); }
float opXor(float d1, float d2) { return min(max(-d1, d2), max(-d2, d1)); }
// k controls transition width
float opSmoothUnion(float d1, float d2, float k) {
float h = clamp(0.5 + 0.5 * (d2 - d1) / k, 0.0, 1.0);
return mix(d2, d1, h) - k * h * (1.0 - h);
}
```
### Step 4: Coordinate Transforms
```glsl
vec2 translate(vec2 p, vec2 t) { return p - t; }
vec2 rotateCCW(vec2 p, float angle) {
mat2 m = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
return p * m;
}
// Usage: translate first, then rotate
float d = sdBox(rotateCCW(translate(p, vec2(0.5, 0.3)), iTime), vec2(0.2), 0.05);
```
### Step 5: Rendering and Anti-Aliasing
```glsl
// smoothstep anti-aliasing (recommended)
float px = 2.0 / iResolution.y;
float mask = smoothstep(px, -px, d); // 1.0 inside, 0.0 outside
vec3 col = mix(backgroundColor, shapeColor, mask);
// fwidth adaptive anti-aliasing (suitable for scaled scenes)
float anti = fwidth(d) * 1.0;
float mask = 1.0 - smoothstep(-anti, anti, d);
// Classic distance field debug visualization
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.65, 0.85, 1.0);
col *= 1.0 - exp(-12.0 * abs(d));
col *= 0.8 + 0.2 * cos(120.0 * d);
col = mix(col, vec3(1.0), smoothstep(1.5*px, 0.0, abs(d) - 0.002));
```
### Step 6: Stroke and Border
```glsl
// Fill + stroke rendering (fwidth adaptive)
vec4 renderShape(float d, vec3 color, float stroke) {
float anti = fwidth(d) * 1.0;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
if (stroke < 0.0001) return colorLayer;
return vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}
float fillMask(float d) { return clamp(-d, 0.0, 1.0); }
float innerBorderMask(float d, float width) {
return clamp(d + width, 0.0, 1.0) - clamp(d, 0.0, 1.0);
}
float outerBorderMask(float d, float width) {
return clamp(d, 0.0, 1.0) - clamp(d - width, 0.0, 1.0);
}
```
### Step 7: Multi-Layer Compositing
```glsl
vec3 bgColor = vec3(1.0, 0.8, 0.7 - 0.07 * p.y) * (1.0 - 0.25 * length(p));
float d1 = sdCircle(translate(p, pos1), 0.3);
vec4 layer1 = renderShape(d1, vec3(0.9, 0.3, 0.2), 0.02);
float d2 = sdBox(translate(p, pos2), vec2(0.2), 0.05);
vec4 layer2 = renderShape(d2, vec3(0.2, 0.5, 0.8), 0.0);
// Composite back to front
vec3 col = bgColor;
col = mix(col, layer1.rgb, layer1.a);
col = mix(col, layer2.rgb, layer2.a);
fragColor = vec4(col, 1.0);
```
## Full Code Template
```glsl
// ===== 2D SDF Full Template (runs directly in ShaderToy) =====
#define AA_WIDTH 1.0 // Anti-aliasing width factor
#define STROKE_WIDTH 0.015 // Stroke width
#define SMOOTH_K 0.05 // Smooth union transition width
#define CONTOUR_FREQ 80.0 // Contour line frequency (for debugging)
#define ANIM_SPEED 1.0 // Animation speed multiplier
// --- SDF Primitives ---
float sdCircle(vec2 p, float r) { return length(p) - r; }
float sdBox(vec2 p, vec2 b, float r) {
b -= vec2(r);
vec2 d = abs(p) - b;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}
float sdLine(vec2 p, vec2 a, vec2 b, float w) {
vec2 d = b - a;
float h = clamp(dot(p - a, d) / dot(d, d), 0.0, 1.0);
return length(p - a - d * h) - w * 0.5;
}
float sdTriangle(vec2 p, vec2 p0, vec2 p1, vec2 p2) {
vec2 e0 = p1 - p0, v0 = p - p0;
vec2 e1 = p2 - p1, v1 = p - p1;
vec2 e2 = p0 - p2, v2 = p - p2;
float d0 = dot(v0 - e0 * clamp(dot(v0,e0)/dot(e0,e0),0.0,1.0),
v0 - e0 * clamp(dot(v0,e0)/dot(e0,e0),0.0,1.0));
float d1 = dot(v1 - e1 * clamp(dot(v1,e1)/dot(e1,e1),0.0,1.0),
v1 - e1 * clamp(dot(v1,e1)/dot(e1,e1),0.0,1.0));
float d2 = dot(v2 - e2 * clamp(dot(v2,e2)/dot(e2,e2),0.0,1.0),
v2 - e2 * clamp(dot(v2,e2)/dot(e2,e2),0.0,1.0));
float o = e0.x*e2.y - e0.y*e2.x;
vec2 dd = min(min(vec2(d0, o*(v0.x*e0.y-v0.y*e0.x)),
vec2(d1, o*(v1.x*e1.y-v1.y*e1.x))),
vec2(d2, o*(v2.x*e2.y-v2.y*e2.x)));
return -sqrt(dd.x) * sign(dd.y);
}
// --- CSG ---
float opUnion(float a, float b) { return min(a, b); }
float opSubtract(float a, float b) { return max(-a, b); }
float opIntersect(float a, float b) { return max(a, b); }
float opSmoothUnion(float a, float b, float k) {
float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0);
return mix(b, a, h) - k*h*(1.0-h);
}
float opXor(float a, float b) { return min(max(-a, b), max(-b, a)); }
// --- Coordinate Transforms ---
vec2 translate(vec2 p, vec2 t) { return p - t; }
vec2 rotateCCW(vec2 p, float a) {
return mat2(cos(a), sin(a), -sin(a), cos(a)) * p;
}
// --- Rendering Utilities ---
vec4 render(float d, vec3 color, float stroke) {
float anti = fwidth(d) * AA_WIDTH;
vec4 strokeLayer = vec4(vec3(0.05), 1.0 - smoothstep(-anti, anti, d - stroke));
vec4 colorLayer = vec4(color, 1.0 - smoothstep(-anti, anti, d));
if (stroke < 0.0001) return colorLayer;
return vec4(mix(strokeLayer.rgb, colorLayer.rgb, colorLayer.a), strokeLayer.a);
}
float fillAA(float d, float px) { return smoothstep(px, -px, d); }
// --- Scene ---
float sceneDist(vec2 p) {
float t = iTime * ANIM_SPEED;
float c = sdCircle(translate(p, vec2(-0.6, 0.3)), 0.25);
float b = sdBox(translate(p, vec2(0.0, 0.3)), vec2(0.25, 0.18), 0.05);
vec2 tp = rotateCCW(translate(p, vec2(0.6, 0.3)), t * 0.5);
float tr = sdTriangle(tp, vec2(0.0, 0.25), vec2(-0.22, -0.12), vec2(0.22, -0.12));
float row1 = opUnion(c, opUnion(b, tr));
float c2 = sdCircle(translate(p, vec2(-0.5, -0.35)), 0.2);
float b2 = sdBox(translate(p, vec2(-0.3, -0.35)), vec2(0.15, 0.15), 0.0);
float smooth_demo = opSmoothUnion(c2, b2, SMOOTH_K);
float c3 = sdCircle(translate(p, vec2(0.15, -0.35)), 0.22);
float b3 = sdBox(translate(p, vec2(0.15, -0.35 + sin(t) * 0.15)), vec2(0.3, 0.08), 0.0);
float sub_demo = opSubtract(b3, c3);
float c4 = sdCircle(translate(p, vec2(0.65, -0.35)), 0.2);
float b4 = sdBox(translate(p, vec2(0.65, -0.35 + sin(t + 1.0) * 0.15)), vec2(0.3, 0.08), 0.0);
float xor_demo = opXor(b4, c4);
float row2 = opUnion(smooth_demo, opUnion(sub_demo, xor_demo));
return opUnion(row1, row2);
}
// --- Main Function ---
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float px = 2.0 / iResolution.y;
float d = sceneDist(p);
vec3 bgCol = vec3(0.15, 0.15, 0.18) + 0.05 * p.y;
bgCol *= 1.0 - 0.3 * length(p);
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.4, 0.7, 1.0);
col *= 1.0 - exp(-10.0 * abs(d));
col *= 0.8 + 0.2 * cos(CONTOUR_FREQ * d);
col = mix(col, vec3(1.0), smoothstep(1.5 * px, 0.0, abs(d) - 0.002));
col = mix(bgCol, col, 0.85);
// Uncomment to switch to solid rendering mode:
// vec3 shapeCol = vec3(0.2, 0.8, 0.6);
// float mask = fillAA(d, px);
// col = mix(bgCol, shapeCol, mask);
col = pow(col, vec3(1.0 / 2.2));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Solid Fill + Stroke Mode
```glsl
vec3 shapeColor = vec3(0.32, 0.56, 0.53);
float strokeW = 0.015;
vec4 shape = render(d, shapeColor, strokeW);
vec3 col = bgCol;
col = mix(col, shape.rgb, shape.a);
```
### Variant 2: Multi-Layer CSG Illustration
```glsl
float a = sdEllipse(p, vec2(0.0, 0.16), 0.25, 0.25);
float b = sdEllipse(p, vec2(0.0, -0.03), 0.8, 0.35);
float body = opIntersect(a, b);
vec4 layer1 = render(body, vec3(0.32, 0.56, 0.53), fwidth(body) * 2.0);
float handle = sdLine(p, vec2(0.0, 0.05), vec2(0.0, -0.42), 0.01);
float arc = sdCircle(translate(p, vec2(-0.04, -0.42)), 0.04);
float arcInner = sdCircle(translate(p, vec2(-0.04, -0.42)), 0.03);
handle = opUnion(handle, opSubtract(arcInner, arc));
vec4 layer0 = render(handle, vec3(0.4, 0.3, 0.28), STROKE_WIDTH);
vec3 col = bgCol;
col = mix(col, layer0.rgb, layer0.a);
col = mix(col, layer1.rgb, layer1.a);
```
### Variant 3: Hexagonal Grid Tiling
```glsl
vec4 hexagon(vec2 p) {
vec2 q = vec2(p.x * 2.0 * 0.5773503, p.y + p.x * 0.5773503);
vec2 pi = floor(q);
vec2 pf = fract(q);
float v = mod(pi.x + pi.y, 3.0);
float ca = step(1.0, v);
float cb = step(2.0, v);
vec2 ma = step(pf.xy, pf.yx);
float e = dot(ma, 1.0 - pf.yx + ca*(pf.x+pf.y-1.0) + cb*(pf.yx-2.0*pf.xy));
p = vec2(q.x + floor(0.5 + p.y / 1.5), 4.0 * p.y / 3.0) * 0.5 + 0.5;
float f = length((fract(p) - 0.5) * vec2(1.0, 0.85));
return vec4(pi + ca - cb * ma, e, f);
}
#define HEX_SCALE 8.0
vec4 h = hexagon(HEX_SCALE * p + 0.5 * iTime);
vec3 col = 0.15 + 0.15 * hash1(h.xy + 1.2);
col *= smoothstep(0.10, 0.11, h.z);
col *= smoothstep(0.10, 0.11, h.w);
```
### Variant 4: Organic Shapes (Polar SDF)
```glsl
// Heart SDF
p.y -= 0.25;
float a = atan(p.x, p.y) / 3.141593;
float r = length(p);
float h = abs(a);
float d = (13.0*h - 22.0*h*h + 10.0*h*h*h) / (6.0 - 5.0*h);
// Pulse animation
float tt = mod(iTime, 1.5) / 1.5;
float ss = pow(tt, 0.2) * 0.5 + 0.5;
ss = 1.0 + ss * 0.5 * sin(tt * 6.2831 * 3.0) * exp(-tt * 4.0);
vec3 col = mix(bgCol, heartCol, smoothstep(-0.01, 0.01, d - r));
```
### Variant 5: Bezier Curve SDF
```glsl
vec3 solveCubic(float a, float b, float c) {
float p = b - a*a/3.0, p3 = p*p*p;
float q = a*(2.0*a*a - 9.0*b)/27.0 + c;
float d = q*q + 4.0*p3/27.0;
float offset = -a/3.0;
if (d >= 0.0) {
float z = sqrt(d);
vec2 x = (vec2(z,-z) - q) / 2.0;
vec2 uv = sign(x) * pow(abs(x), vec2(1.0/3.0));
return vec3(offset + uv.x + uv.y);
}
float v = acos(-sqrt(-27.0/p3)*q/2.0) / 3.0;
float m = cos(v), n = sin(v) * 1.732050808;
return vec3(m+m, -n-m, n-m) * sqrt(-p/3.0) + offset;
}
float sdBezier(vec2 A, vec2 B, vec2 C, vec2 p) {
B = mix(B + vec2(1e-4), B, step(1e-6, abs(B*2.0-A-C)));
vec2 a = B-A, b = A-B*2.0+C, c = a*2.0, d = A-p;
vec3 k = vec3(3.*dot(a,b), 2.*dot(a,a)+dot(d,b), dot(d,a)) / dot(b,b);
vec3 t = clamp(solveCubic(k.x, k.y, k.z), 0.0, 1.0);
vec2 pos = A+(c+b*t.x)*t.x; float dis = length(pos-p);
pos = A+(c+b*t.y)*t.y; dis = min(dis, length(pos-p));
pos = A+(c+b*t.z)*t.z; dis = min(dis, length(pos-p));
return dis * signBezier(A, B, C, p);
}
```
## Extended 2D SDF Library
```glsl
// === Extended 2D SDF Library ===
// Rounded Box with independent corner radii (vec4 r = top-right, bottom-right, top-left, bottom-left)
float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
r.xy = (p.x > 0.0) ? r.xy : r.zw;
r.x = (p.y > 0.0) ? r.x : r.y;
vec2 q = abs(p) - b + r.x;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x;
}
// Oriented Box (from point a to point b with thickness th)
float sdOrientedBox(vec2 p, vec2 a, vec2 b, float th) {
float l = length(b - a);
vec2 d = (b - a) / l;
vec2 q = (p - (a + b) * 0.5);
q = mat2(d.x, -d.y, d.y, d.x) * q;
q = abs(q) - vec2(l, th) * 0.5;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
}
// Arc (sc = vec2(sin,cos) of aperture angle, ra = radius, rb = thickness)
float sdArc(vec2 p, vec2 sc, float ra, float rb) {
p.x = abs(p.x);
return ((sc.y * p.x > sc.x * p.y) ? length(p - sc * ra) : abs(length(p) - ra)) - rb;
}
// Pie / Sector (c = vec2(sin,cos) of aperture angle)
float sdPie(vec2 p, vec2 c, float r) {
p.x = abs(p.x);
float l = length(p) - r;
float m = length(p - c * clamp(dot(p, c), 0.0, r));
return max(l, m * sign(c.y * p.x - c.x * p.y));
}
// Ring (n = vec2(sin,cos) of aperture, r = radius, th = thickness)
float sdRing(vec2 p, vec2 n, float r, float th) {
p.x = abs(p.x);
float d = length(p);
// If within aperture angle
if (n.y * p.x > n.x * p.y) {
return abs(d - r) - th;
}
// Cap endpoints
return min(length(p - n * r), length(p + n * r)) - th;
}
// Moon shape
float sdMoon(vec2 p, float d, float ra, float rb) {
p.y = abs(p.y);
float a = (ra * ra - rb * rb + d * d) / (2.0 * d);
float b2 = ra * ra - a * a;
if (d * (p.x * rb * rb - p.y * a * rb * rb - a * b2) > 0.0)
return length(p - vec2(a, sqrt(max(b2, 0.0))));
return max(length(p) - ra, -(length(p - vec2(d, 0.0)) - rb));
}
// Heart (approximate)
float sdHeart(vec2 p) {
p.x = abs(p.x);
if (p.y + p.x > 1.0)
return sqrt(dot(p - vec2(0.25, 0.75), p - vec2(0.25, 0.75))) - sqrt(2.0) / 4.0;
return sqrt(min(dot(p - vec2(0.0, 1.0), p - vec2(0.0, 1.0)),
dot(p - 0.5 * max(p.x + p.y, 0.0), p - 0.5 * max(p.x + p.y, 0.0)))) *
sign(p.x - p.y);
}
// Vesica (lens shape)
float sdVesica(vec2 p, float w, float h) {
p = abs(p);
float b = sqrt(h * h + w * w * 0.25) / w;
return ((p.y - h) * b * w > p.x * b * h)
? length(p - vec2(0.0, h))
: length(p - vec2(-w * 0.5, 0.0)) - b;
}
// Egg shape
float sdEgg(vec2 p, float he, float ra, float rb) {
p.x = abs(p.x);
float r = (p.y < 0.0) ? ra : rb;
return length(vec2(p.x, p.y - clamp(p.y, -he, he))) - r;
}
// Equilateral Triangle
float sdEquilateralTriangle(vec2 p, float r) {
const float k = sqrt(3.0);
p.x = abs(p.x) - r;
p.y = p.y + r / k;
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
p.x -= clamp(p.x, -2.0 * r, 0.0);
return -length(p) * sign(p.y);
}
// Pentagon
float sdPentagon(vec2 p, float r) {
const vec3 k = vec3(0.809016994, 0.587785252, 0.726542528);
p.x = abs(p.x);
p -= 2.0 * min(dot(vec2(-k.x, k.y), p), 0.0) * vec2(-k.x, k.y);
p -= 2.0 * min(dot(vec2(k.x, k.y), p), 0.0) * vec2(k.x, k.y);
p -= vec2(clamp(p.x, -r * k.z, r * k.z), r);
return length(p) * sign(p.y);
}
// Hexagon
float sdHexagon(vec2 p, float r) {
const vec3 k = vec3(-0.866025404, 0.5, 0.577350269);
p = abs(p);
p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
return length(p) * sign(p.y);
}
// Octagon
float sdOctagon(vec2 p, float r) {
const vec3 k = vec3(-0.9238795325, 0.3826834323, 0.4142135623);
p = abs(p);
p -= 2.0 * min(dot(vec2(k.x, k.y), p), 0.0) * vec2(k.x, k.y);
p -= 2.0 * min(dot(vec2(-k.x, k.y), p), 0.0) * vec2(-k.x, k.y);
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
return length(p) * sign(p.y);
}
// Star (n-pointed, m = inner radius ratio)
float sdStar(vec2 p, float r, int n, float m) {
float an = 3.141593 / float(n);
float en = 3.141593 / m;
vec2 acs = vec2(cos(an), sin(an));
vec2 ecs = vec2(cos(en), sin(en));
float bn = mod(atan(p.x, p.y), 2.0 * an) - an;
p = length(p) * vec2(cos(bn), abs(sin(bn)));
p -= r * acs;
p += ecs * clamp(-dot(p, ecs), 0.0, r * acs.y / ecs.y);
return length(p) * sign(p.x);
}
// Quadratic Bezier curve SDF
float sdBezier(vec2 pos, vec2 A, vec2 B, vec2 C) {
vec2 a = B - A;
vec2 b = A - 2.0 * B + C;
vec2 c = a * 2.0;
vec2 d = A - pos;
float kk = 1.0 / dot(b, b);
float kx = kk * dot(a, b);
float ky = kk * (2.0 * dot(a, a) + dot(d, b)) / 3.0;
float kz = kk * dot(d, a);
float res = 0.0;
float p2 = ky - kx * kx;
float q = kx * (2.0 * kx * kx - 3.0 * ky) + kz;
float h = q * q + 4.0 * p2 * p2 * p2;
if (h >= 0.0) {
h = sqrt(h);
vec2 x = (vec2(h, -h) - q) / 2.0;
vec2 uv2 = sign(x) * pow(abs(x), vec2(1.0 / 3.0));
float t = clamp(uv2.x + uv2.y - kx, 0.0, 1.0);
res = dot(d + (c + b * t) * t, d + (c + b * t) * t);
} else {
float z = sqrt(-p2);
float v = acos(q / (p2 * z * 2.0)) / 3.0;
float m2 = cos(v);
float n2 = sin(v) * 1.732050808;
vec3 t = clamp(vec3(m2 + m2, -n2 - m2, n2 - m2) * z - kx, 0.0, 1.0);
res = min(dot(d + (c + b * t.x) * t.x, d + (c + b * t.x) * t.x),
dot(d + (c + b * t.y) * t.y, d + (c + b * t.y) * t.y));
}
return sqrt(res);
}
// Parabola
float sdParabola(vec2 pos, float k) {
pos.x = abs(pos.x);
float ik = 1.0 / k;
float p2 = ik * (pos.y - 0.5 * ik) / 3.0;
float q = 0.25 * ik * ik * pos.x;
float h = q * q - p2 * p2 * p2;
float r = sqrt(abs(h));
float x = (h > 0.0) ?
pow(q + r, 1.0 / 3.0) + pow(abs(q - r), 1.0 / 3.0) * sign(p2) :
2.0 * cos(atan(r, q) / 3.0) * sqrt(p2);
return length(pos - vec2(x, k * x * x)) * sign(pos.x - x);
}
// Cross shape
float sdCross(vec2 p, vec2 b, float r) {
p = abs(p); p = (p.y > p.x) ? p.yx : p.xy;
vec2 q = p - b;
float k = max(q.y, q.x);
vec2 w = (k > 0.0) ? q : vec2(b.y - p.x, -k);
return sign(k) * length(max(w, 0.0)) + r;
}
```
## 2D SDF Modifiers
```glsl
// === 2D SDF Modifiers ===
// Round any 2D SDF
float opRound2D(float d, float r) { return d - r; }
// Create annular (ring) version of any 2D SDF
float opAnnular2D(float d, float r) { return abs(d) - r; }
// Repeat a 2D SDF in a grid
vec2 opRepeat2D(vec2 p, float s) { return mod(p + s * 0.5, s) - s * 0.5; }
// Mirror across arbitrary 2D direction
vec2 opMirror2D(vec2 p, vec2 dir) {
return p - 2.0 * dir * max(dot(p, dir), 0.0);
}
```
## Performance & Composition Tips
**Performance:**
- In polygon SDFs, compare squared distances first; use a single `sqrt` at the end
- For simple scenes, use fixed `px = 2.0/iResolution.y` instead of `fwidth(d)`; use `fwidth` when coordinate scaling is involved
- For many primitives, spatially partition and skip distant ones early
- Supersampling (2x2/3x3) only for offline rendering; for real-time, single-pixel AA with `smoothstep`/`fwidth` is sufficient
- For 2D soft shadow marching, use adaptive step size `dt += max(1.0, abs(sd))`
**Composition:**
- **SDF + Noise**: `d += noise(p * 10.0 + iTime) * 0.05` to create organic edges
- **SDF + 2D Lighting**: cone marching for soft shadows, query occlusion via `sceneDist()`
- **SDF + Normal Mapping**: finite differences for normals + Blinn-Phong lighting to simulate bump effects
- **SDF + Domain Repetition**: `fract`/`mod` for infinite repetition, `floor` for cell ID
- **SDF + Animation**: parameters driven by `sin/cos` periodic motion, `exp` decay, `mod` looping
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/sdf-2d.md)

View File

@@ -0,0 +1,589 @@
# 3D Signed Distance Fields (3D SDF) Skill
## Use Cases
- Real-time rendering of 3D geometry in ShaderToy / fragment shaders (no traditional meshes needed)
- Complex scenes composed from basic primitives (sphere, box, cylinder, torus, etc.)
- Smooth organic blending (character modeling, fluid blobs, biological forms)
- Infinitely repeating architectural/pattern structures (corridors, gear arrays, grids)
- Precise boolean operations (drilling holes, cutting, intersection) for sculpting geometry
## Core Principles
An SDF returns the **signed distance** from any point in space to the nearest surface: positive = outside, negative = inside, zero = surface.
**Sphere Tracing**: advance along a ray, stepping by the current SDF value (the safe marching distance) at each step. The SDF guarantees no surface exists within that radius. A hit is registered when the distance falls below epsilon.
Key math:
- Sphere: `f(p) = |p| - r`
- Box: `f(p) = |max(|p|-b, 0)| + min(max(|p-b|), 0)`
- Union: `min(d1, d2)` / Subtraction: `max(d1, -d2)`
- Smooth union: `min(d1,d2) - h^2/4k`, `h = max(k-|d1-d2|, 0)`
- Normal = SDF gradient: `n = normalize(gradient of f(p))` (finite difference approximation)
## Rendering Pipeline Overview
1. **SDF Primitive Library** -- `sdSphere`, `sdBox`, `sdEllipsoid`, `sdTorus`, `sdCapsule`, `sdCylinder`
2. **Boolean Operations** -- `opUnion`/`opSubtraction`/`opIntersection` + smooth variants `smin`/`smax`
3. **Scene Definition** -- `map(p)` returns `vec2(distance, materialID)`, combining all primitives
4. **Ray Marching** -- `raycast(ro, rd)` sphere tracing loop (128 steps, adaptive threshold `SURF_DIST * t`)
5. **Normal Calculation** -- tetrahedral differencing (4 map calls, ZERO macro to prevent inlining)
6. **Soft Shadows** -- quadratic stepping with `k*h/t` to estimate occlusion softness, Hermite smoothing
7. **Ambient Occlusion** -- 5-layer sampling along the normal, comparing SDF values with expected distances
8. **Camera + Rendering** -- look-at matrix, multiple lights (sun + sky + SSS), gamma correction, fog
## Full Code Template
Runs directly in ShaderToy. Includes multi-primitive scene, smooth blending, soft shadows, AO, and material system.
**IMPORTANT:** When using the `vec2(distance, materialID)` material system, `smin` needs to handle `vec2` types. The template includes a `vec2 smin(vec2 a, vec2 b, float k)` overload that ensures the material ID is correctly passed through during smooth blending (taking the material of the closer distance).
```glsl
// 3D SDF Full Rendering Pipeline Template - Runs in ShaderToy
#define AA 1 // Anti-aliasing (1=off, 2=4xAA, 3=9xAA)
#define MAX_STEPS 128
#define MAX_DIST 40.0
#define SURF_DIST 0.0001
#define SHADOW_STEPS 24
#define SHADOW_SOFTNESS 8.0
#define SMOOTH_K 0.3
#define ZERO (min(iFrame, 0))
// === SDF Primitives ===
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float sdEllipsoid(vec3 p, vec3 r) {
float k0 = length(p / r); float k1 = length(p / (r * r));
return k0 * (k0 - 1.0) / k1;
}
float sdTorus(vec3 p, vec2 t) {
return length(vec2(length(p.xz) - t.x, p.y)) - t.y;
}
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
vec3 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h) - r;
}
float sdCylinder(vec3 p, vec2 h) {
vec2 d = abs(vec2(length(p.xz), p.y)) - h;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
// === Extended SDF Primitives ===
float sdRoundBox(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b + r;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float sdBoxFrame(vec3 p, vec3 b, float e) {
p = abs(p) - b;
vec3 q = abs(p + e) - e;
return min(min(
length(max(vec3(p.x, q.y, q.z), 0.0)) + min(max(p.x, max(q.y, q.z)), 0.0),
length(max(vec3(q.x, p.y, q.z), 0.0)) + min(max(q.x, max(p.y, q.z)), 0.0)),
length(max(vec3(q.x, q.y, p.z), 0.0)) + min(max(q.x, max(q.y, p.z)), 0.0));
}
float sdCone(vec3 p, vec2 c, float h) {
vec2 q = h * vec2(c.x / c.y, -1.0);
vec2 w = vec2(length(p.xz), p.y);
vec2 a = w - q * clamp(dot(w, q) / dot(q, q), 0.0, 1.0);
vec2 b = w - q * vec2(clamp(w.x / q.x, 0.0, 1.0), 1.0);
float k = sign(q.y);
float d = min(dot(a, a), dot(b, b));
float s = max(k * (w.x * q.y - w.y * q.x), k * (w.y - q.y));
return sqrt(d) * sign(s);
}
float sdCappedCone(vec3 p, float h, float r1, float r2) {
vec2 q = vec2(length(p.xz), p.y);
vec2 k1 = vec2(r2, h);
vec2 k2 = vec2(r2 - r1, 2.0 * h);
vec2 ca = vec2(q.x - min(q.x, (q.y < 0.0) ? r1 : r2), abs(q.y) - h);
vec2 cb = q - k1 + k2 * clamp(dot(k1 - q, k2) / dot(k2, k2), 0.0, 1.0);
float s = (cb.x < 0.0 && ca.y < 0.0) ? -1.0 : 1.0;
return s * sqrt(min(dot(ca, ca), dot(cb, cb)));
}
float sdRoundCone(vec3 p, float r1, float r2, float h) {
float b = (r1 - r2) / h;
float a = sqrt(1.0 - b * b);
vec2 q = vec2(length(p.xz), p.y);
float k = dot(q, vec2(-b, a));
if (k < 0.0) return length(q) - r1;
if (k > a * h) return length(q - vec2(0.0, h)) - r2;
return dot(q, vec2(a, b)) - r1;
}
float sdSolidAngle(vec3 p, vec2 c, float ra) {
vec2 q = vec2(length(p.xz), p.y);
float l = length(q) - ra;
float m = length(q - c * clamp(dot(q, c), 0.0, ra));
return max(l, m * sign(c.y * q.x - c.x * q.y));
}
float sdOctahedron(vec3 p, float s) {
p = abs(p);
float m = p.x + p.y + p.z - s;
vec3 q;
if (3.0 * p.x < m) q = p.xyz;
else if (3.0 * p.y < m) q = p.yzx;
else if (3.0 * p.z < m) q = p.zxy;
else return m * 0.57735027;
float k = clamp(0.5 * (q.z - q.y + s), 0.0, s);
return length(vec3(q.x, q.y - s + k, q.z - k));
}
float sdPyramid(vec3 p, float h) {
float m2 = h * h + 0.25;
p.xz = abs(p.xz);
p.xz = (p.z > p.x) ? p.zx : p.xz;
p.xz -= 0.5;
vec3 q = vec3(p.z, h * p.y - 0.5 * p.x, h * p.x + 0.5 * p.y);
float s = max(-q.x, 0.0);
float t = clamp((q.y - 0.5 * p.z) / (m2 + 0.25), 0.0, 1.0);
float a = m2 * (q.x + s) * (q.x + s) + q.y * q.y;
float b = m2 * (q.x + 0.5 * t) * (q.x + 0.5 * t) + (q.y - m2 * t) * (q.y - m2 * t);
float d2 = min(q.y, -q.x * m2 - q.y * 0.5) > 0.0 ? 0.0 : min(a, b);
return sqrt((d2 + q.z * q.z) / m2) * sign(max(q.z, -p.y));
}
float sdHexPrism(vec3 p, vec2 h) {
const vec3 k = vec3(-0.8660254, 0.5, 0.57735);
p = abs(p);
p.xy -= 2.0 * min(dot(k.xy, p.xy), 0.0) * k.xy;
vec2 d = vec2(length(p.xy - vec2(clamp(p.x, -k.z * h.x, k.z * h.x), h.x)) * sign(p.y - h.x), p.z - h.y);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float sdCutSphere(vec3 p, float r, float h) {
float w = sqrt(r * r - h * h);
vec2 q = vec2(length(p.xz), p.y);
float s = max((h - r) * q.x * q.x + w * w * (h + r - 2.0 * q.y), h * q.x - w * q.y);
return (s < 0.0) ? length(q) - r : (q.x < w) ? h - q.y : length(q - vec2(w, h));
}
float sdCappedTorus(vec3 p, vec2 sc, float ra, float rb) {
p.x = abs(p.x);
float k = (sc.y * p.x > sc.x * p.y) ? dot(p.xy, sc) : length(p.xy);
return sqrt(dot(p, p) + ra * ra - 2.0 * ra * k) - rb;
}
float sdLink(vec3 p, float le, float r1, float r2) {
vec3 q = vec3(p.x, max(abs(p.y) - le, 0.0), p.z);
return length(vec2(length(q.xy) - r1, q.z)) - r2;
}
float sdPlane(vec3 p, vec3 n, float h) {
return dot(p, n) + h;
}
float sdRhombus(vec3 p, float la, float lb, float h, float ra) {
p = abs(p);
vec2 b = vec2(la, lb);
float f = clamp((dot(b, b - 2.0 * p.xz)) / dot(b, b), -1.0, 1.0);
vec2 q = vec2(length(p.xz - 0.5 * b * vec2(1.0 - f, 1.0 + f)) * sign(p.x * b.y + p.z * b.x - b.x * b.y) - ra, p.y - h);
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0));
}
// Unsigned distance (exact)
float udTriangle(vec3 p, vec3 a, vec3 b, vec3 c) {
vec3 ba = b - a; vec3 pa = p - a;
vec3 cb = c - b; vec3 pb = p - b;
vec3 ac = a - c; vec3 pc = p - c;
vec3 nor = cross(ba, ac);
return sqrt(
(sign(dot(cross(ba, nor), pa)) +
sign(dot(cross(cb, nor), pb)) +
sign(dot(cross(ac, nor), pc)) < 2.0)
? min(min(
dot(ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa,
ba * clamp(dot(ba, pa) / dot(ba, ba), 0.0, 1.0) - pa),
dot(cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb,
cb * clamp(dot(cb, pb) / dot(cb, cb), 0.0, 1.0) - pb)),
dot(ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc,
ac * clamp(dot(ac, pc) / dot(ac, ac), 0.0, 1.0) - pc))
: dot(nor, pa) * dot(nor, pa) / dot(nor, nor));
}
// === Boolean Operations ===
vec2 opU(vec2 d1, vec2 d2) { return (d1.x < d2.x) ? d1 : d2; }
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / k;
}
vec2 smin(vec2 a, vec2 b, float k) {
// vec2 smin: x=distance (smooth blend), y=materialID (take material of closer distance)
float h = max(k - abs(a.x - b.x), 0.0);
float d = min(a.x, b.x) - h * h * 0.25 / k;
float m = (a.x < b.x) ? a.y : b.y;
return vec2(d, m);
}
float smax(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0);
return max(a, b) + h * h * 0.25 / k;
}
// === Deformation Operators ===
// Round: soften edges of any SDF
// Usage: sdRound(sdBox(p, vec3(1.0)), 0.1)
float opRound(float d, float r) { return d - r; }
// Onion: hollow out any SDF into a shell
// Usage: opOnion(sdSphere(p, 1.0), 0.1) — sphere shell of thickness 0.1
float opOnion(float d, float t) { return abs(d) - t; }
// Elongate: stretch a shape along axes
// Usage: elongate a sphere into a capsule-like shape
float opElongate(in vec3 p, in vec3 h, in vec3 center, in vec3 size) {
// Generic elongation: subtract h from abs(p), clamp to 0
vec3 q = abs(p) - h;
// Then evaluate original SDF with max(q, 0.0)
// Return: sdOriginal(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0)
return sdBox(max(q, 0.0), size) + min(max(q.x, max(q.y, q.z)), 0.0); // example with box
}
// Twist: rotate around Y axis based on height
vec3 opTwist(vec3 p, float k) {
float c = cos(k * p.y);
float s = sin(k * p.y);
mat2 m = mat2(c, -s, s, c);
return vec3(m * p.xz, p.y);
}
// Cheap Bend: bend along X axis based on X position
vec3 opCheapBend(vec3 p, float k) {
float c = cos(k * p.x);
float s = sin(k * p.x);
mat2 m = mat2(c, -s, s, c);
vec2 q = m * p.xy;
return vec3(q, p.z);
}
// Displacement: add procedural detail to surface
float opDisplace(float d, vec3 p) {
float displacement = sin(20.0 * p.x) * sin(20.0 * p.y) * sin(20.0 * p.z);
return d + displacement * 0.02;
}
// === 2D-to-3D Constructors ===
// Revolution: rotate a 2D SDF around the Y axis to create a 3D solid of revolution
// sdf2d: any 2D SDF function, o: offset from axis
float opRevolution(vec3 p, float sdf2d_result, float o) {
vec2 q = vec2(length(p.xz) - o, p.y);
// Example: revolve a 2D circle to make a torus
// float d2d = length(q) - 0.3; // 2D circle as cross-section
// return d2d;
return sdf2d_result; // pass pre-computed 2D SDF of vec2(length(p.xz)-o, p.y)
}
// Extrusion: extend a 2D SDF along the Z axis with finite height
float opExtrusion(vec3 p, float d2d, float h) {
vec2 w = vec2(d2d, abs(p.z) - h);
return min(max(w.x, w.y), 0.0) + length(max(w, 0.0));
}
// Usage example: extruded 2D star
// float d2d = sdStar2D(p.xy, 0.5, 5, 2.0); // any 2D SDF
// float d3d = opExtrusion(p, d2d, 0.2); // extrude 0.2 units
// === Symmetry Operators ===
// Mirror across X axis (most common — bilateral symmetry)
// Place this at the beginning of map() to model only one half
vec3 opSymX(vec3 p) { p.x = abs(p.x); return p; }
// Mirror across X and Z (four-fold symmetry)
vec3 opSymXZ(vec3 p) { p.xz = abs(p.xz); return p; }
// Mirror across arbitrary direction
vec3 opMirror(vec3 p, vec3 dir) {
return p - 2.0 * dir * max(dot(p, dir), 0.0);
}
// === Scene ===
vec2 map(vec3 pos) {
vec2 res = vec2(pos.y, 0.0);
// Animated blob cluster
float dBlob = 2.0;
for (int i = 0; i < 8; i++) {
float fi = float(i);
float t = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
vec3 offset = sin(t + fi * vec3(52.5126, 64.627, 632.25)) * vec3(2.0, 2.0, 0.8);
float radius = mix(0.3, 0.6, fract(fi * 412.531 + 0.5124));
dBlob = smin(dBlob, sdSphere(pos + offset, radius), SMOOTH_K);
}
res = opU(res, vec2(dBlob, 1.0));
float dBox = sdBox(pos - vec3(3.0, 0.4, 0.0), vec3(0.3, 0.4, 0.3));
res = opU(res, vec2(dBox, 2.0));
float dTorus = sdTorus((pos - vec3(-3.0, 0.5, 0.0)).xzy, vec2(0.4, 0.1));
res = opU(res, vec2(dTorus, 3.0));
// CSG subtraction: sphere minus box
float dCSG = sdSphere(pos - vec3(0.0, 0.5, 3.0), 0.5);
dCSG = max(dCSG, -sdBox(pos - vec3(0.0, 0.5, 3.0), vec3(0.3)));
res = opU(res, vec2(dCSG, 4.0));
return res;
}
// === Normals ===
vec3 calcNormal(vec3 pos) {
vec3 n = vec3(0.0);
for (int i = ZERO; i < 4; i++) {
vec3 e = 0.5773 * (2.0 * vec3((((i+3)>>1)&1), ((i>>1)&1), (i&1)) - 1.0);
n += e * map(pos + 0.0005 * e).x;
}
return normalize(n);
}
// === Shadows ===
float calcSoftshadow(vec3 ro, vec3 rd, float mint, float tmax) {
float res = 1.0, t = mint;
for (int i = ZERO; i < SHADOW_STEPS; i++) {
float h = map(ro + rd * t).x;
float s = clamp(SHADOW_SOFTNESS * h / t, 0.0, 1.0);
res = min(res, s);
t += clamp(h, 0.01, 0.2);
if (res < 0.004 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
// === AO ===
float calcAO(vec3 pos, vec3 nor) {
float occ = 0.0, sca = 1.0;
for (int i = ZERO; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(pos + h * nor).x;
occ += (h - d) * sca;
sca *= 0.95;
if (occ > 0.35) break;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0) * (0.5 + 0.5 * nor.y);
}
// === Ray Marching ===
vec2 raycast(vec3 ro, vec3 rd) {
vec2 res = vec2(-1.0);
float t = 0.01;
for (int i = 0; i < MAX_STEPS && t < MAX_DIST; i++) {
vec2 h = map(ro + rd * t);
if (abs(h.x) < SURF_DIST * t) { res = vec2(t, h.y); break; }
t += h.x;
}
return res;
}
// === Camera ===
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
// === Rendering ===
vec3 render(vec3 ro, vec3 rd) {
vec3 col = vec3(0.7, 0.7, 0.9) - max(rd.y, 0.0) * 0.3;
vec2 res = raycast(ro, rd);
float t = res.x, m = res.y;
if (m > -0.5) {
vec3 pos = ro + t * rd;
vec3 nor = (m < 0.5) ? vec3(0.0, 1.0, 0.0) : calcNormal(pos);
vec3 ref = reflect(rd, nor);
vec3 mate = 0.2 + 0.2 * sin(m * 2.0 + vec3(0.0, 1.0, 2.0));
if (m < 0.5) mate = vec3(0.15);
float occ = calcAO(pos, nor);
vec3 lin = vec3(0.0);
// Key light
{
vec3 lig = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(lig - rd);
float dif = clamp(dot(nor, lig), 0.0, 1.0);
dif *= calcSoftshadow(pos, lig, 0.02, 2.5);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif * (0.04 + 0.96 * pow(clamp(1.0 - dot(hal, lig), 0.0, 1.0), 5.0));
lin += mate * 2.20 * dif * vec3(1.30, 1.00, 0.70);
lin += 5.00 * spe * vec3(1.30, 1.00, 0.70);
}
// Sky light
{
float dif = sqrt(clamp(0.5 + 0.5 * nor.y, 0.0, 1.0)) * occ;
lin += mate * 0.60 * dif * vec3(0.40, 0.60, 1.15);
}
// Subsurface scattering approximation
{
float dif = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 2.0) * occ;
lin += mate * 0.25 * dif;
}
col = lin;
col = mix(col, vec3(0.7, 0.7, 0.9), 1.0 - exp(-0.0001 * t * t * t));
}
return clamp(col, 0.0, 1.0);
}
// === Main Function ===
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 mo = iMouse.xy / iResolution.xy;
float time = 32.0 + iTime * 1.5;
vec3 ta = vec3(0.0, 0.0, 0.0);
vec3 ro = ta + vec3(4.5 * cos(0.1 * time + 7.0 * mo.x), 2.2,
4.5 * sin(0.1 * time + 7.0 * mo.x));
mat3 ca = setCamera(ro, ta, 0.0);
vec3 tot = vec3(0.0);
#if AA > 1
for (int m = ZERO; m < AA; m++)
for (int n = ZERO; n < AA; n++) {
vec2 o = vec2(float(m), float(n)) / float(AA) - 0.5;
vec2 p = (2.0 * (fragCoord + o) - iResolution.xy) / iResolution.y;
#else
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
#endif
vec3 rd = ca * normalize(vec3(p, 2.5));
vec3 col = render(ro, rd);
col = pow(col, vec3(0.4545));
tot += col;
#if AA > 1
}
tot /= float(AA * AA);
#endif
fragColor = vec4(tot, 1.0);
}
```
## Common Variants
### Variant 1: Dynamic Organic Body (Smooth Blob Animation)
```glsl
vec2 map(vec3 p) {
float d = 2.0;
for (int i = 0; i < 16; i++) {
float fi = float(i);
float t = iTime * (fract(fi * 412.531 + 0.513) - 0.5) * 2.0;
d = smin(sdSphere(p + sin(t + fi * vec3(52.5126, 64.627, 632.25)) * vec3(2.0, 2.0, 0.8),
mix(0.5, 1.0, fract(fi * 412.531 + 0.5124))), d, 0.4);
}
return vec2(d, 1.0);
}
```
### Variant 2: Infinite Repeating Corridor (Domain Repetition)
```glsl
float repeat(float v, float c) { return mod(v, c) - c * 0.5; }
float amod(inout vec2 p, float count) {
float an = 6.283185 / count;
float a = atan(p.y, p.x) + an * 0.5;
float c = floor(a / an);
a = mod(a, an) - an * 0.5;
p = vec2(cos(a), sin(a)) * length(p);
return c;
}
vec2 map(vec3 p) {
p.z = repeat(p.z, 4.0);
p.x += 2.0 * sin(p.z * 0.1);
float d = -sdBox(p, vec3(2.0, 2.0, 20.0));
d = max(d, -sdBox(p, vec3(1.8, 1.8, 1.9)));
d = min(d, sdCylinder(p - vec3(1.5, -2.0, 0.0), vec2(0.1, 2.0)));
return vec2(d, 1.0);
}
```
### Variant 3: Character/Creature Modeling
```glsl
vec2 sdStick(vec3 p, vec3 a, vec3 b, float r1, float r2) {
vec3 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return vec2(length(pa - ba * h) - mix(r1, r2, h * h * (3.0 - 2.0 * h)), h);
}
vec2 map(vec3 pos) {
float d = sdEllipsoid(pos, vec3(0.25, 0.3, 0.25)); // body
d = smin(d, sdEllipsoid(pos - vec3(0.0, 0.35, 0.02),
vec3(0.12, 0.15, 0.13)), 0.1); // head
vec2 arm = sdStick(abs(pos.x) > 0.0 ? vec3(abs(pos.x), pos.yz) : pos,
vec3(0.18, 0.2, -0.05), vec3(0.35, -0.1, -0.15), 0.03, 0.05);
d = smin(d, arm.x, 0.04); // arms
d = smax(d, -sdEllipsoid(pos - vec3(0.0, 0.3, 0.15),
vec3(0.08, 0.03, 0.1)), 0.03); // mouth carving
return vec2(d, 1.0);
}
```
### Variant 4: Symmetry Optimization
```glsl
vec2 rot45(vec2 v) { return vec2(v.x - v.y, v.y + v.x) * 0.707107; }
vec2 map(vec3 p) {
float d = sdSphere(p, 0.12);
// Octahedral symmetry: 18-gear evaluations reduced to 4
vec3 qx = vec3(rot45(p.zy), p.x);
if (abs(qx.x) > abs(qx.y)) qx = qx.zxy;
vec3 qy = vec3(rot45(p.xz), p.y);
if (abs(qy.x) > abs(qy.y)) qy = qy.zxy;
vec3 qz = vec3(rot45(p.yx), p.z);
if (abs(qz.x) > abs(qz.y)) qz = qz.zxy;
vec3 qa = abs(p);
qa = (qa.x > qa.y && qa.x > qa.z) ? p.zxy : (qa.z > qa.y) ? p.yzx : p.xyz;
d = min(d, min(min(gear(qa, 0.0), gear(qx, 1.0)), min(gear(qy, 1.0), gear(qz, 1.0))));
return vec2(d, 1.0);
}
```
### Variant 5: PBR Material Rendering
```glsl
float D_GGX(float NoH, float roughness) {
float a = roughness * roughness; float a2 = a * a;
float d = NoH * NoH * (a2 - 1.0) + 1.0;
return a2 / (3.14159 * d * d);
}
vec3 F_Schlick(float VoH, vec3 f0) {
return f0 + (1.0 - f0) * pow(1.0 - VoH, 5.0);
}
vec3 pbrLighting(vec3 pos, vec3 nor, vec3 rd, vec3 albedo, float roughness, float metallic) {
vec3 lig = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(lig - rd);
vec3 f0 = mix(vec3(0.04), albedo, metallic);
float NoL = max(dot(nor, lig), 0.0);
float NoH = max(dot(nor, hal), 0.0);
float VoH = max(dot(-rd, hal), 0.0);
vec3 spec = D_GGX(NoH, roughness) * F_Schlick(VoH, f0) * 0.25;
vec3 diff = albedo * (1.0 - metallic) / 3.14159;
float shadow = calcSoftshadow(pos, lig, 0.02, 2.5);
return (diff + spec) * NoL * shadow * vec3(1.3, 1.0, 0.7) * 3.0;
}
```
## Performance & Composition
### Performance Optimization Tips
- **Bounding volume acceleration**: test ray against AABB first to narrow `tmin/tmax`, avoiding wasted steps in empty regions
- **Sub-scene bounding**: in `map()`, use a cheap `sdBox` to check proximity before computing the precise SDF
- **Adaptive step size**: `abs(h.x) < SURF_DIST * t` -- looser tolerance at distance, stricter up close
- **Prevent compiler inlining**: `#define ZERO (min(iFrame, 0))` + loop prevents `calcNormal` from inlining map 4 times
- **Exploit symmetry**: fold into the fundamental domain, reducing 18 evaluations to 4
### Common Composition Techniques
- **Noise displacement**: `d += 0.05 * sin(p.x*10.)*sin(p.y*10.)*sin(p.z*10.)` adds organic detail; breaks the Lipschitz condition, so step size should be multiplied by 0.5~0.7
- **Bump mapping**: perturb only during normal calculation, leaving ray marching unaffected for better performance
- **Domain transforms**: warp coordinates before entering map (bending, polar coordinate transforms, etc.)
- **Procedural animation**: bone angles driven by time to position primitives, `smin` ensures smooth joints
- **Motion blur**: multi-frame temporal sampling averaged
## Further Reading
Full step-by-step tutorials, mathematical derivations, and advanced usage in [reference](../reference/sdf-3d.md)

View File

@@ -0,0 +1,100 @@
# SDF Advanced Tricks & Optimization
## Use Cases
- Optimizing complex SDF scenes for real-time performance
- Adding fine detail to SDF surfaces without increasing geometric complexity
- Creating special effects with SDF manipulation (hollowing, layered edges, interior structures)
- Debugging and visualizing SDF fields
## Core Techniques
### Hollowing (Shell Creation)
Convert any solid SDF into a thin shell:
```glsl
float hollowed = abs(sdf) - thickness;
// Example: hollow sphere with 0.02 wall thickness
float d = abs(sdSphere(p, 1.0)) - 0.02;
```
### Layered Edges (Concentric Contour Lines)
Create equidistant contour rings from any SDF:
```glsl
float spacing = 0.2;
float thickness = 0.02;
float layered = abs(mod(d + spacing * 0.5, spacing) - spacing * 0.5) - thickness;
```
Useful for: topographic map effects, neon outlines, energy shields, wireframe-like rendering.
### FBM Detail on SDF (Distance-Based LOD)
Add procedural noise detail only where it's visible — near the camera:
```glsl
float map(vec3 p) {
float d = sdBasicShape(p);
// Only add expensive FBM detail when close to surface
if (d < 1.0) {
d += 0.02 * fbm(p * 8.0) * smoothstep(1.0, 0.0, d);
}
return d;
}
```
**Critical**: The `smoothstep` fade prevents the FBM from disrupting the SDF's Lipschitz continuity far from the surface, which would cause ray marching to overshoot.
### SDF Bounding Volumes (Performance Optimization)
Skip expensive SDF evaluation when the point is far from the object:
```glsl
float map(vec3 p) {
// Cheap bounding sphere test first
float bound = sdSphere(p - objectCenter, boundingRadius);
if (bound > 0.1) return bound; // far away — return bounding distance
// Expensive detailed SDF only when close
return complexSDF(p);
}
```
For scenes with multiple distant objects, this can provide 5-10x speedup.
### Binary Search Refinement
After ray marching finds an approximate hit, refine with binary search for sub-pixel precision:
```glsl
// After ray march loop finds t where map(ro+rd*t) < epsilon:
for (int i = 0; i < 6; i++) {
float mid = map(ro + rd * t);
t += mid * 0.5; // or use proper bisection:
// float dt = step * 0.5^i;
// t += (map(ro+rd*t) > 0.0) ? dt : -dt;
}
```
Especially useful for: sharp edge rendering, precise shadow termination, accurate reflection points.
### XOR Boolean Operation
Create interesting geometric patterns by combining SDFs with XOR:
```glsl
float opXor(float d1, float d2) {
return max(min(d1, d2), -max(d1, d2));
}
// Creates a "difference of unions" — geometry exists where exactly one shape is present
```
### Interior SDF Structures
Use the sign of the SDF to create interior geometry:
```glsl
float interiorPattern(vec3 p) {
float outer = sdSphere(p, 1.0);
float inner = sdBox(fract(p * 4.0) - 0.5, vec3(0.1)); // repeating inner pattern
return (outer < 0.0) ? max(outer, inner) : outer; // inner visible only inside
}
```
## SDF Debugging Visualization
```glsl
// Visualize SDF distance as color bands
vec3 debugSDF(float d) {
vec3 col = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.4, 0.7, 0.85); // outside/inside
col *= 1.0 - exp(-6.0 * abs(d)); // darken near surface
col *= 0.8 + 0.2 * cos(150.0 * d); // distance bands
col = mix(col, vec3(1.0), 1.0 - smoothstep(0.0, 0.01, abs(d))); // white at surface
return col;
}
```
→ For deeper details, see [reference/sdf-tricks.md](../reference/sdf-tricks.md)

View File

@@ -0,0 +1,776 @@
# SDF Soft Shadow Techniques
## Core Principles
March from the surface point toward the light source, using the **ratio of nearest distance to marching distance** to estimate penumbra width.
### Key Formulas
Classic formula: `shadow = min(shadow, k * h / t)`
- `h` = SDF value at current position, `t` = distance traveled, `k` = penumbra hardness
Improved formula (geometric triangulation) — eliminates sharp edge banding artifacts:
```
y = h² / (2 * ph) // ph = SDF value from previous step
d = sqrt(h² - y²) // true closest distance perpendicular to the ray
shadow = min(shadow, d / (w * max(0, t - y)))
```
Negative extension — allows `res` to drop to -1, remapped with a C1 continuous function to eliminate hard creases:
```
res = max(res, -1.0)
shadow = 0.25 * (1 + res)² * (2 - res)
```
This is equivalent to `smoothstep` over [-1, 1] instead of [0, 1]. The step size is clamped with `clamp(h, 0.005, 0.50)` to ensure the ray penetrates slightly into geometry, capturing both outer and inner penumbra. This produces results close to ground truth for varying light sizes.
## Implementation Steps
### Step 1: Scene SDF
```glsl
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdPlane(vec3 p) { return p.y; }
float sdRoundBox(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float map(vec3 p) {
float d = sdPlane(p);
d = min(d, sdSphere(p - vec3(0.0, 0.5, 0.0), 0.5));
d = min(d, sdRoundBox(p - vec3(-1.2, 0.3, 0.5), vec3(0.3), 0.05));
return d;
}
```
### Step 2: Classic Soft Shadow
```glsl
// Classic SDF soft shadow
float calcSoftShadow(vec3 ro, vec3 rd, float mint, float tmax) {
float res = 1.0;
float t = mint;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
float s = clamp(SHADOW_K * h / t, 0.0, 1.0);
res = min(res, s);
t += clamp(h, MIN_STEP, MAX_STEP);
if (res < 0.004 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res); // smoothstep smoothing
}
```
### Step 3: Improved Soft Shadow (Geometric Triangulation)
```glsl
// Improved version - geometric triangulation using adjacent SDF values
float calcSoftShadowImproved(vec3 ro, vec3 rd, float mint, float tmax, float w) {
float res = 1.0;
float t = mint;
float ph = 1e10;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
float y = h * h / (2.0 * ph);
float d = sqrt(h * h - y * y);
res = min(res, d / (w * max(0.0, t - y)));
ph = h;
t += h;
if (res < 0.0001 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
```
### Step 4: Negative Extension (Smoothest Penumbra)
```glsl
// Negative extension - allows res to go negative for C1 continuous penumbra
float calcSoftShadowSmooth(vec3 ro, vec3 rd, float mint, float tmax, float w) {
float res = 1.0;
float t = mint;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
res = min(res, h / (w * t));
t += clamp(h, MIN_STEP, MAX_STEP);
if (res < -1.0 || t > tmax) break;
}
res = max(res, -1.0);
return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
}
```
### Step 5: Bounding Volume Optimization
```glsl
// plane clipping -- clip the ray to the scene's upper bound
float tp = (SCENE_Y_MAX - ro.y) / rd.y;
if (tp > 0.0) tmax = min(tmax, tp);
// AABB bounding box clipping
vec2 iBox(vec3 ro, vec3 rd, vec3 rad) {
vec3 m = 1.0 / rd;
vec3 n = m * ro;
vec3 k = abs(m) * rad;
vec3 t1 = -n - k;
vec3 t2 = -n + k;
float tN = max(max(t1.x, t1.y), t1.z);
float tF = min(min(t2.x, t2.y), t2.z);
if (tN > tF || tF < 0.0) return vec2(-1.0);
return vec2(tN, tF);
}
// usage: return 1.0 immediately if the ray misses the bounding box entirely
vec2 dis = iBox(ro, rd, BOUND_SIZE);
if (dis.y < 0.0) return 1.0;
tmin = max(tmin, dis.x);
tmax = min(tmax, dis.y);
```
### Step 6: Shadow Color Rendering
```glsl
// Classic colored shadow
vec3 shadowColor = vec3(sha, sha * sha * 0.5 + 0.5 * sha, sha * sha);
// per-channel power (penumbra region shifts warm)
vec3 shadowColor = pow(vec3(sha), vec3(1.0, 1.2, 1.5));
```
### Step 7: Integration with Lighting Model
```glsl
vec3 sunDir = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(sunDir - rd);
float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
if (dif > 0.0001)
dif *= calcSoftShadow(pos + nor * 0.01, sunDir, 0.02, 8.0);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif;
vec3 col = vec3(0.0);
col += albedo * 2.0 * dif * vec3(1.0, 0.9, 0.8);
col += 5.0 * spe * vec3(1.0, 0.9, 0.8);
col += albedo * 0.5 * clamp(0.5 + 0.5 * nor.y, 0.0, 1.0) * vec3(0.4, 0.6, 1.0);
```
## Complete Code Template
Runs directly in ShaderToy, with A/B comparison of three soft shadow techniques.
```glsl
#define ZERO (min(iFrame, 0))
// ---- Adjustable Parameters ----
#define MAX_MARCH_STEPS 128
#define MAX_SHADOW_STEPS 64 // 16~128
#define SHADOW_K 8.0 // 4~64, higher = harder
#define SHADOW_MINT 0.02 // 0.01~0.05
#define SHADOW_TMAX 8.0
#define SHADOW_MIN_STEP 0.01
#define SHADOW_MAX_STEP 0.20
#define SHADOW_W 0.10 // improved version penumbra width
// 0=classic, 1=improved(Aaltonen), 2=negative extension
#define SHADOW_TECHNIQUE 0
// ---- SDF Primitives ----
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdPlane(vec3 p) { return p.y; }
float sdRoundBox(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
// ---- Scene SDF ----
float map(vec3 p) {
float d = sdPlane(p);
d = min(d, sdSphere(p - vec3(0.0, 0.5, 0.0), 0.5));
d = min(d, sdRoundBox(p - vec3(-1.2, 0.30, 0.5), vec3(0.25), 0.05));
d = min(d, sdTorus(p - vec3(1.2, 0.25, -0.3), vec2(0.40, 0.08)));
return d;
}
// ---- Normal ----
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.0005, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)));
}
// ---- Raymarching ----
float castRay(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = ZERO; i < MAX_MARCH_STEPS; i++) {
float h = map(ro + rd * t);
if (h < 0.0002) return t;
t += h;
if (t > 20.0) break;
}
return -1.0;
}
// ---- Bounding Volume Clipping ----
float clipTmax(vec3 ro, vec3 rd, float tmax, float yMax) {
float tp = (yMax - ro.y) / rd.y;
if (tp > 0.0) tmax = min(tmax, tp);
return tmax;
}
// ---- Shadow: Classic ----
float softShadowClassic(vec3 ro, vec3 rd, float mint, float tmax) {
tmax = clipTmax(ro, rd, tmax, 1.5);
float res = 1.0, t = mint;
for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
float s = clamp(SHADOW_K * h / t, 0.0, 1.0);
res = min(res, s);
t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
if (res < 0.004 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
// ---- Shadow: Improved ----
float softShadowImproved(vec3 ro, vec3 rd, float mint, float tmax, float w) {
tmax = clipTmax(ro, rd, tmax, 1.5);
float res = 1.0, t = mint, ph = 1e10;
for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
float y = h * h / (2.0 * ph);
float d = sqrt(h * h - y * y);
res = min(res, d / (w * max(0.0, t - y)));
ph = h;
t += h;
if (res < 0.0001 || t > tmax) break;
}
res = clamp(res, 0.0, 1.0);
return res * res * (3.0 - 2.0 * res);
}
// ---- Shadow: Negative Extension ----
float softShadowSmooth(vec3 ro, vec3 rd, float mint, float tmax, float w) {
tmax = clipTmax(ro, rd, tmax, 1.5);
float res = 1.0, t = mint;
for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
res = min(res, h / (w * t));
t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
if (res < -1.0 || t > tmax) break;
}
res = max(res, -1.0);
return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
}
// ---- Unified Interface ----
float calcSoftShadow(vec3 ro, vec3 rd, float mint, float tmax) {
#if SHADOW_TECHNIQUE == 0
return softShadowClassic(ro, rd, mint, tmax);
#elif SHADOW_TECHNIQUE == 1
return softShadowImproved(ro, rd, mint, tmax, SHADOW_W);
#else
return softShadowSmooth(ro, rd, mint, tmax, SHADOW_W);
#endif
}
// ---- AO ----
float calcAO(vec3 p, vec3 n) {
float occ = 0.0, sca = 1.0;
for (int i = ZERO; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(p + h * n);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
// ---- Checkerboard ----
float checkerboard(vec2 p) {
vec2 q = floor(p);
return mix(0.3, 1.0, mod(q.x + q.y, 2.0));
}
// ---- Render ----
vec3 render(vec3 ro, vec3 rd) {
vec3 col = vec3(0.7, 0.75, 0.85) - 0.3 * rd.y;
float t = castRay(ro, rd);
if (t < 0.0) return col;
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
vec3 albedo = vec3(0.18);
if (pos.y < 0.001)
albedo = vec3(0.08 + 0.15 * checkerboard(pos.xz * 2.0));
vec3 sunDir = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(sunDir - rd);
float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
if (dif > 0.0001)
dif *= calcSoftShadow(pos + nor * 0.001, sunDir, SHADOW_MINT, SHADOW_TMAX);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif;
float fre = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0);
spe *= 0.04 + 0.96 * fre;
float sky = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0);
float occ = calcAO(pos, nor);
vec3 lin = vec3(0.0);
lin += 2.5 * dif * vec3(1.30, 1.00, 0.70);
lin += 8.0 * spe * vec3(1.30, 1.00, 0.70);
lin += 0.5 * sky * vec3(0.40, 0.60, 1.00) * occ;
lin += 0.25 * occ * vec3(0.40, 0.50, 0.60);
col = albedo * lin;
col = pow(col, vec3(0.4545));
return col;
}
// ---- Camera ----
mat3 setCamera(vec3 ro, vec3 ta) {
vec3 cw = normalize(ta - ro);
vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0)));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float an = 0.3 * iTime;
vec3 ro = vec3(3.5 * sin(an), 1.8, 3.5 * cos(an));
vec3 ta = vec3(0.0, 0.3, 0.0);
mat3 ca = setCamera(ro, ta);
vec3 rd = ca * normalize(vec3(p, 1.8));
vec3 col = render(ro, rd);
fragColor = vec4(col, 1.0);
}
```
## Standalone HTML + WebGL2 Template
When generating standalone HTML files, use the following complete template. Key points:
- Must use `canvas.getContext('webgl2')`
- Shaders use `#version 300 es`
- Entry function is `void main()`, not `void mainImage()`
- Use `gl_FragCoord.xy` to get pixel coordinates (available in WebGL2)
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Soft Shadows - SDF Raymarching</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
document.body.innerHTML = '<p style="color:#fff;">WebGL2 not supported</p>';
throw new Error('WebGL2 not supported');
}
// Vertex shader: fullscreen quad
const vsSource = `#version 300 es
in vec4 aPosition;
void main() {
gl_Position = aPosition;
}
`;
// Fragment shader: SDF soft shadows
const fsSource = `#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform vec4 iMouse;
out vec4 fragColor;
#define ZERO (min(int(iTime), 0))
#define MAX_MARCH_STEPS 128
#define MAX_SHADOW_STEPS 64
#define SHADOW_MINT 0.02
#define SHADOW_TMAX 10.0
#define SHADOW_MIN_STEP 0.01
#define SHADOW_MAX_STEP 0.25
#define SHADOW_W 0.08
#define SHADOW_K 16.0
// SDF primitives
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdPlane(vec3 p) { return p.y; }
float sdRoundBox(vec3 p, vec3 b, float r) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
// Scene SDF
float map(vec3 p) {
float d = sdPlane(p);
d = min(d, sdSphere(p - vec3(0.0, 0.6, 0.0), 0.6));
d = min(d, sdRoundBox(p - vec3(-1.5, 0.4, 0.8), vec3(0.35), 0.08));
d = min(d, sdTorus(p - vec3(1.6, 0.35, -0.5), vec2(0.45, 0.12)));
return d;
}
// Normal
vec3 calcNormal(vec3 p) {
vec2 e = vec2(0.0005, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)));
}
// Raymarching
float castRay(vec3 ro, vec3 rd) {
float t = 0.0;
for (int i = ZERO; i < MAX_MARCH_STEPS; i++) {
float h = map(ro + rd * t);
if (h < 0.0002) return t;
t += h;
if (t > 25.0) break;
}
return -1.0;
}
// Plane clipping
float clipTmax(vec3 ro, vec3 rd, float tmax, float yMax) {
float tp = (yMax - ro.y) / rd.y;
if (tp > 0.0) tmax = min(tmax, tp);
return tmax;
}
// Soft shadow (negative extension)
float softShadow(vec3 ro, vec3 rd, float mint, float tmax, float w) {
tmax = clipTmax(ro, rd, tmax, 2.0);
float res = 1.0;
float t = mint;
for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
float h = map(ro + rd * t);
res = min(res, h / (w * t));
t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
if (res < -1.0 || t > tmax) break;
}
res = max(res, -1.0);
return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
}
// Soft shadow call
float calcSoftShadow(vec3 ro, vec3 rd) {
return softShadow(ro, rd, SHADOW_MINT, SHADOW_TMAX, SHADOW_W);
}
// AO
float calcAO(vec3 p, vec3 n) {
float occ = 0.0, sca = 1.0;
for (int i = ZERO; i < 5; i++) {
float h = 0.01 + 0.12 * float(i) / 4.0;
float d = map(p + h * n);
occ += (h - d) * sca;
sca *= 0.95;
}
return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}
// Checkerboard
float checkerboard(vec2 p) {
vec2 q = floor(p);
return mix(0.25, 0.35, mod(q.x + q.y, 2.0));
}
// Render
vec3 render(vec3 ro, vec3 rd) {
// sky
vec3 col = vec3(0.65, 0.72, 0.85) - 0.4 * rd.y;
col = mix(col, vec3(0.3, 0.35, 0.45), exp(-0.8 * max(rd.y, 0.0)));
float t = castRay(ro, rd);
if (t < 0.0) return col;
vec3 pos = ro + rd * t;
vec3 nor = calcNormal(pos);
// material color
vec3 albedo = vec3(0.18);
if (pos.y < 0.01) {
albedo = vec3(0.12 + 0.12 * checkerboard(pos.xz * 1.5));
} else if (pos.y > 0.5 && length(pos.xz) < 0.7) {
albedo = vec3(0.85, 0.25, 0.2);
} else if (pos.x < -1.0) {
albedo = vec3(0.2, 0.4, 0.85);
} else if (pos.x > 1.0) {
albedo = vec3(0.25, 0.75, 0.35);
} else {
albedo = vec3(0.9, 0.6, 0.2);
}
// lighting
vec3 sunDir = normalize(vec3(-0.6, 0.45, -0.65));
vec3 hal = normalize(sunDir - rd);
float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
if (dif > 0.0001) {
dif *= calcSoftShadow(pos + nor * 0.01, sunDir);
}
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 32.0);
spe *= dif;
float fre = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0);
spe *= 0.04 + 0.96 * fre;
float sky = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0);
float occ = calcAO(pos, nor);
vec3 lin = vec3(0.0);
lin += 2.2 * dif * vec3(1.35, 1.05, 0.75);
lin += 6.0 * spe * vec3(1.35, 1.05, 0.75);
lin += 0.4 * sky * vec3(0.45, 0.6, 0.9) * occ;
lin += 0.25 * occ * vec3(0.5, 0.55, 0.6);
col = albedo * lin;
col = pow(col, vec3(0.4545));
// vignette
vec2 uv = gl_FragCoord.xy / iResolution.xy;
col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.2);
return col;
}
// Camera
mat3 setCamera(vec3 ro, vec3 ta) {
vec3 cw = normalize(ta - ro);
vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0)));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
void main() {
vec2 fragCoord = gl_FragCoord.xy;
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
// slowly rotating camera
float an = 0.15 * iTime;
float dist = 5.5;
vec3 ro = vec3(dist * sin(an), 2.2, dist * cos(an));
vec3 ta = vec3(0.0, 0.3, 0.0);
mat3 ca = setCamera(ro, ta);
vec3 rd = ca * normalize(vec3(p, 2.0));
vec3 col = render(ro, rd);
fragColor = vec4(col, 1.0);
}
`;
// Compile shader
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Create program
function createProgram(gl, vs, fs) {
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
return null;
}
return program;
}
const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = createProgram(gl, vs, fs);
// Fullscreen quad
const positions = new Float32Array([
-1, -1, 1, -1, -1, 1, 1, 1
]);
const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const posLoc = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
// Uniforms
const uTime = gl.getUniformLocation(program, 'iTime');
const uResolution = gl.getUniformLocation(program, 'iResolution');
const uMouse = gl.getUniformLocation(program, 'iMouse');
// Mouse tracking
let mouseX = 0, mouseY = 0;
canvas.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = canvas.height - e.clientY;
});
// Window resize
function resize() {
const dpr = Math.min(window.devicePixelRatio, 2);
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
// Render loop
function render(time) {
time *= 0.001;
gl.useProgram(program);
gl.uniform1f(uTime, time);
gl.uniform2f(uResolution, canvas.width, canvas.height);
gl.uniform4f(uMouse, mouseX, mouseY, mouseX, mouseY);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>
```
## Common Variants
### Analytic Sphere Shadow
```glsl
vec2 sphDistances(vec3 ro, vec3 rd, vec4 sph) {
vec3 oc = ro - sph.xyz;
float b = dot(oc, rd);
float c = dot(oc, oc) - sph.w * sph.w;
float h = b * b - c;
float d = sqrt(max(0.0, sph.w * sph.w - h)) - sph.w;
return vec2(d, -b - sqrt(max(h, 0.0)));
}
float sphSoftShadow(vec3 ro, vec3 rd, vec4 sph, float k) {
vec2 r = sphDistances(ro, rd, sph);
if (r.y > 0.0)
return clamp(k * max(r.x, 0.0) / r.y, 0.0, 1.0);
return 1.0;
}
```
### Terrain Heightfield Shadow
```glsl
float terrainShadow(vec3 ro, vec3 rd, float dis) {
float minStep = clamp(dis * 0.01, 0.5, 50.0);
float res = 1.0, t = 0.01;
for (int i = 0; i < 80; i++) {
vec3 p = ro + t * rd;
float h = p.y - terrainMap(p.xz);
res = min(res, 16.0 * h / t);
t += max(minStep, h);
if (res < 0.001 || p.y > MAX_TERRAIN_HEIGHT) break;
}
return clamp(res, 0.0, 1.0);
}
```
### Per-Material Soft/Hard Blending
```glsl
float hsha = 1.0; // global variable, set per material in map()
float mapWithShadowHardness(vec3 p) {
float d = sdPlane(p); hsha = 1.0;
float dChar = sdCharacter(p);
if (dChar < d) { d = dChar; hsha = 0.0; }
return d;
}
// in shadow loop: res = min(res, mix(1.0, SHADOW_K * h / t, hsha));
```
### Multi-Layer Shadow Compositing
```glsl
float sha_terrain = terrainShadow(pos, sunDir, 0.02);
float sha_trees = treesShadow(pos, sunDir);
float sha_clouds = cloudShadow(pos, sunDir);
float sha = sha_terrain * sha_trees;
sha *= smoothstep(-0.3, -0.1, sha_clouds);
dif *= sha;
```
### Volumetric Light / God Rays
```glsl
float godRays(vec3 ro, vec3 rd, float tmax, vec3 sunDir) {
float v = 0.0, dt = 0.15;
float t = dt * fract(texelFetch(iChannel0, ivec2(fragCoord) & 255, 0).x);
for (int i = 0; i < 32; i++) {
if (t > tmax) break;
vec3 p = ro + rd * t;
float sha = calcSoftShadow(p, sunDir, 0.02, 8.0);
v += sha * exp(-0.2 * t);
t += dt;
}
v /= 32.0;
return v * v;
}
// col += intensity * godRays(...) * vec3(1.0, 0.75, 0.4);
```
## Performance & Composition
**Performance optimization:**
- Bounding volume clipping (plane/AABB) can reduce 30-70% of wasted iterations
- Step clamping `clamp(h, minStep, maxStep)` prevents stalling / skipping thin objects
- Early exit: `res < 0.004` (classic) or `res < -1.0` (negative extension)
- Simplified `map()` omitting material calculations, returning distance only
- Only compute shadow when `dif > 0.0001`; skip for backlit faces
- Iteration count: simple scenes 16~32, complex FBM 64~128, terrain ~80
- `#define ZERO (min(iFrame,0))` prevents compiler loop unrolling
**Composition tips:**
- AO: shadows control direct light, AO controls indirect light, `col = diffuse * sha + ambient * ao`
- SSS: `sss *= 0.25 + 0.75 * sha` -- SSS weakens but does not vanish in shadow
- Fog: complete lit+shadowed shading first, then `mix(col, fogColor, 1.0 - exp(-0.001*t*t))`
- Normal mapping: perturbed normals for lighting, geometric normals for shadow determination
- Reflection: `refSha = calcSoftShadow(pos + nor*0.01, reflect(rd, nor), 0.02, 8.0)`
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/shadow-techniques.md)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,490 @@
**IMPORTANT - GLSL ES 3.00 Critical Rules**:
1. **Type strictness**: `int` and `float` cannot be mixed directly; array indices must be of `int` type
2. **Reserved words**: `sample` is a reserved word in GLSL ES 3.00; it cannot be used as a variable name
3. **Constant arrays**: Must explicitly specify size when declaring, e.g., `const float ARR[4] = float[4](1.,2.,3.,4.);`
4. **Integer division**: In GLSL ES 3.00, `1/2` evaluates to 0 (integer division); must use `1.0/2.0` or `float(1)/float(2)`
# Sound Synthesis (Procedural Audio)
## Use Cases
- Generate procedural audio using `mainSound()` in ShaderToy
- Synthesize melodies, chords, rhythm patterns, and complete music
- Synthesize instrument timbres: piano, bass, acid synth, percussion
- Implement audio effects: delay, reverb, distortion, filters
- Pure mathematical audio generation without external samples
## Core Principles
ShaderToy sound shader four-layer architecture:
1. **Oscillator layer**: `sin(2π·f·t)`, layering harmonics or FM modulation to build timbre
2. **Envelope layer**: `exp(-rate·t)` + `smoothstep` attack, simulating strike→decay
3. **Sequencer layer**: Macro definitions / array lookup / hash pseudo-random for arranging melodies
4. **Effects layer**: Reverb, delay, distortion, filters, and other post-processing
Key formulas:
- MIDI → frequency: `f = 440.0 × 2^((n - 69) / 12)`
- Sine oscillator: `y = sin(2π × freq × time)`
- Exponential decay: `env = exp(-decay_rate × time)`
- FM modulation: `y = sin(2π × f_c × t + depth × sin(2π × f_m × t))`
## Implementation Steps
### Step 1: mainSound Entry Framework
```glsl
#define TAU 6.28318530718
#define BPM 120.0
#define SPB (60.0 / BPM)
vec2 mainSound(int samp, float time) {
vec2 audio = vec2(0.0);
// Layer each instrument/track
audio *= 0.5 * smoothstep(0.0, 0.5, time); // Master volume + pop prevention
return clamp(audio, -1.0, 1.0);
}
```
### Step 2: MIDI Note to Frequency
```glsl
float noteFreq(float note) {
return 440.0 * pow(2.0, (note - 69.0) / 12.0);
}
```
### Step 3: Basic Oscillators
```glsl
float osc_sin(float t) { return sin(TAU * t); }
float osc_saw(float t) { return fract(t) * 2.0 - 1.0; }
float osc_sqr(float t) { return step(fract(t), 0.5) * 2.0 - 1.0; }
float osc_tri(float t) { return abs(fract(t) - 0.5) * 4.0 - 1.0; }
```
### Step 4: Additive Synthesis Instrument
```glsl
// Layer harmonics to build timbre; higher harmonics decay faster
float instrument_additive(float freq, float t) {
float y = 0.0;
y += 0.50 * sin(TAU * 1.00 * freq * t) * exp(-0.0015 * 1.0 * freq * t);
y += 0.30 * sin(TAU * 2.01 * freq * t) * exp(-0.0015 * 2.0 * freq * t);
y += 0.20 * sin(TAU * 4.01 * freq * t) * exp(-0.0015 * 4.0 * freq * t);
y += 0.1 * y * y * y; // Nonlinear waveshaping
y *= 0.9 + 0.1 * cos(40.0 * t); // Tremolo
y *= smoothstep(0.0, 0.01, t); // Smooth attack
return y;
}
```
### Step 5: FM Synthesis Instrument
```glsl
// FM electric piano (stereo)
vec2 fm_epiano(float freq, float t) {
vec2 f0 = vec2(freq * 0.998, freq * 1.002); // Stereo micro-detuning
// "Glass" layer - high-frequency FM, metallic attack
vec2 glass = sin(TAU * (f0 + 3.0) * t
+ sin(TAU * 14.0 * f0 * t) * exp(-30.0 * t)
) * exp(-4.0 * t);
glass = sin(glass);
// "Body" layer - low-frequency FM, warm sustained tone
vec2 body = sin(TAU * f0 * t
+ sin(TAU * f0 * t) * exp(-0.5 * t) * pow(440.0 / f0.x, 0.5)
) * exp(-t);
return (glass + body) * smoothstep(0.0, 0.001, t) * 0.1;
}
// FM generic instrument (struct parameterized)
struct Instr {
float att, fo, vibe, vphas, phas, dtun;
};
float fm_instrument(float freq, float t, float beatTime, Instr ins) {
float f = freq - beatTime * ins.dtun;
float phase = f * t * TAU;
float vibrato = cos(beatTime * ins.vibe * 3.14159 / 8.0 + ins.vphas * 1.5708);
float fm = sin(phase + vibrato * sin(phase * ins.phas));
float env = exp(-beatTime * ins.fo) * (1.0 - exp(-beatTime * ins.att));
return fm * env * (1.0 - beatTime * 0.125);
}
```
### Step 6: Percussion Synthesis
```glsl
float hash(float p) {
p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p);
}
// 909 kick drum: frequency sweep + noise click
float kick(float t) {
float phase = TAU * (60.0 * t - 512.0 * 0.01 * exp(-t / 0.01));
float body = sin(phase) * smoothstep(0.3, 0.0, t) * 1.5;
float click = sin(TAU * 8000.0 * fract(t)) * hash(t * 2000.0)
* smoothstep(0.007, 0.0, t);
return body + click;
}
// Hi-hat: noise + exponential decay. decay: 5.0=open, 15.0=closed
float hihat(float t, float decay) {
float noise = hash(floor(t * 44100.0)) * 2.0 - 1.0;
return noise * exp(-decay * t) * smoothstep(0.0, 0.02, t);
}
// Clap/snare
float clap(float t) {
float noise = hash(floor(t * 44100.0)) * 2.0 - 1.0;
return noise * smoothstep(0.1, 0.0, t);
}
```
### Step 7: Note Sequence Arrangement
```glsl
// === Method A: D() macro accumulation (good for handwritten melodies) ===
#define D(duration, note) b += float(duration); if(t > b) { x = b; n = float(note); }
float melody_macro(float time) {
float t = time / 0.18;
float n = 0.0, b = 0.0, x = 0.0;
D(10,71) D(2,76) D(3,79) D(1,78) D(2,76) D(4,83) D(2,81) D(6,78)
float freq = noteFreq(n);
float noteTime = 0.18 * (t - x);
return instrument_additive(freq, noteTime);
}
// === Method B: Array lookup (good for complex arrangements) ===
// NOTE: Array indices must be int type in GLSL ES 3.00
const float NOTES[16] = float[16](
60., 62., 64., 65., 67., 69., 71., 72.,
60., 64., 67., 72., 65., 69., 64., 60.
);
float melody_array(float time, float bpm) {
float beat = time * bpm / 60.0;
int idx = int(mod(beat, 16.0)); // IMPORTANT: Must use int() conversion
float noteTime = fract(beat);
float freq = noteFreq(NOTES[idx]);
return instrument_additive(freq, noteTime * 60.0 / bpm);
}
// === Method C: Hash pseudo-random (good for algorithmic composition) ===
float nse(float x) { return fract(sin(x * 110.082) * 19871.8972); }
float scale_filter(float note) {
float n2 = mod(note, 12.0);
if (n2==1.||n2==3.||n2==6.||n2==8.||n2==10.) return -100.0;
return note;
}
float melody_random(float time, float bpm) {
float beat = time * bpm / 60.0;
float note = 48.0 + floor(nse(floor(beat)) * 24.0);
note = scale_filter(note);
return instrument_additive(noteFreq(note), fract(beat) * 60.0 / bpm);
}
```
### Step 8: Chord Construction
```glsl
vec2 chord(float time, float root, float isMinor) {
vec2 result = vec2(0.0);
float bass = root - 24.0;
result += fm_epiano(noteFreq(bass), time, 2.0);
result += fm_epiano(noteFreq(root), time - SPB * 0.5, 1.25);
result += fm_epiano(noteFreq(root + 4.0 - isMinor), time - SPB, 1.5); // Third
result += fm_epiano(noteFreq(root + 7.0), time - SPB * 0.5, 1.25); // Fifth
result += fm_epiano(noteFreq(root + 11.0 - isMinor), time - SPB, 1.5); // Seventh
result += fm_epiano(noteFreq(root + 14.0), time - SPB, 1.5); // Ninth
return result;
}
```
### Step 9: Delay and Reverb
```glsl
// Multi-tap echo
// NOTE: "sample" is a reserved word in GLSL ES 3.00; use "samp" instead
vec2 echo_reverb(float time) {
vec2 tot = vec2(0.0);
float hh = 1.0;
for (int i = 0; i < 6; i++) {
float h = float(i) / 5.0;
float samp = get_instrument_sample(time - 0.7 * h);
tot += samp * vec2(0.5 + 0.1 * h, 0.5 - 0.1 * h) * hh;
hh *= 0.5;
}
return tot;
}
// Ping-pong stereo delay
vec2 pingpong_delay(float time) {
vec2 mx = get_stereo_sample(time) * 0.5;
float ec = 0.4, fb = 0.6, dt = 0.222;
float et = dt;
mx += get_stereo_sample(time - et) * ec * vec2(1.0, 0.5); ec *= fb; et += dt;
mx += get_stereo_sample(time - et) * ec * vec2(0.5, 1.0); ec *= fb; et += dt;
mx += get_stereo_sample(time - et) * ec * vec2(1.0, 0.5); ec *= fb; et += dt;
mx += get_stereo_sample(time - et) * ec * vec2(0.5, 1.0); ec *= fb; et += dt;
return mx;
}
```
### Step 10: Beat and Arrangement Structure
```glsl
vec2 mainSound(int samp, float time) {
vec2 audio = vec2(0.0);
float beat = time * BPM / 60.0;
float bar = beat / 4.0;
// Kick (every beat) + hi-hat (every half beat) + melody
float kickTime = mod(time, SPB);
audio += vec2(kick(kickTime) * 0.5);
float hatTime = mod(time, SPB * 0.5);
audio += vec2(hihat(hatTime, 15.0) * 0.15);
audio += vec2(melody_array(time, BPM)) * 0.3;
// Arrangement: smoothstep controls intro/outro
audio *= smoothstep(0.0, 4.0, bar); // Fade in over first 4 bars
audio *= 0.35 * smoothstep(0.0, 0.5, time);
// IMPORTANT: Array indices must be int type
// float idx = mod(beat, 16.0); // WRONG: float cannot be used as index
int idx = int(mod(beat, 16.0)); // CORRECT: int(mod(...)) conversion
return clamp(audio, -1.0, 1.0);
}
```
## Complete Code Template
Can be pasted directly into the ShaderToy Sound tab to run. Includes FM piano melody, kick drum rhythm, and ping-pong delay.
```glsl
// === Sound Synthesis Complete Template ===
#define TAU 6.28318530718
#define BPM 130.0
#define SPB (60.0 / BPM)
#define NUM_HARMONICS 4
#define ECHO_TAPS 4
#define ECHO_DELAY 0.18
#define ECHO_DECAY 0.45
float noteFreq(float note) {
return 440.0 * pow(2.0, (note - 69.0) / 12.0);
}
float hash11(float p) {
p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p);
}
float osc_tri(float t) { return abs(fract(t) - 0.5) * 4.0 - 1.0; }
float instrument(float freq, float t) {
float y = 0.0;
for (int i = 1; i <= NUM_HARMONICS; i++) {
float h = float(i);
float amp = 0.6 / h;
float decay = 0.002 * h * freq;
y += amp * sin(TAU * h * 1.003 * freq * t) * exp(-decay * t);
}
y += 0.15 * y * y * y;
y *= 0.9 + 0.1 * cos(35.0 * t);
y *= smoothstep(0.0, 0.008, t);
return y;
}
vec2 epiano(float freq, float t) {
vec2 f0 = vec2(freq * 0.998, freq * 1.002);
vec2 glass = sin(TAU * (f0 + 3.0) * t
+ sin(TAU * 14.0 * f0 * t) * exp(-30.0 * t)
) * exp(-4.0 * t);
glass = sin(glass);
vec2 body = sin(TAU * f0 * t
+ sin(TAU * f0 * t) * exp(-0.5 * t) * pow(440.0 / max(f0.x, 1.0), 0.5)
) * exp(-t);
return (glass + body) * smoothstep(0.0, 0.001, t) * 0.12;
}
float kick(float t) {
float df = 512.0, dftime = 0.01, freq = 60.0;
float phase = TAU * (freq * t - df * dftime * exp(-t / dftime));
float body = sin(phase) * smoothstep(0.3, 0.0, t) * 1.5;
float click = sin(TAU * 8000.0 * fract(t)) * hash11(t * 2048.0)
* smoothstep(0.007, 0.0, t);
return body + click;
}
float hihat(float t) {
float noise = hash11(floor(t * 44100.0)) * 2.0 - 1.0;
return noise * exp(-15.0 * t) * smoothstep(0.0, 0.002, t);
}
const float MELODY[16] = float[16](
67., 67., 72., 71., 69., 67., 64., 64.,
65., 65., 69., 67., 67., 65., 64., 62.
);
const float BASS[4] = float[4](43., 48., 45., 41.);
vec2 mainSound(int samp, float time) {
time = mod(time, 32.0 * SPB * 4.0);
vec2 audio = vec2(0.0);
float beat = time / SPB;
float bar = beat / 4.0;
// Melody
{ int idx = int(mod(beat, 16.0));
float noteTime = fract(beat) * SPB;
audio += vec2(instrument(noteFreq(MELODY[idx]), noteTime) * 0.25); }
// Bass
{ int idx = int(mod(bar, 4.0));
float noteTime = fract(bar) * SPB * 4.0;
float freq = noteFreq(BASS[idx]);
audio += vec2(osc_tri(freq * noteTime) * exp(-1.5 * noteTime)
* smoothstep(0.0, 0.01, noteTime) * 0.3); }
// Kick (every beat) + sidechain compression
{ float kt = mod(time, SPB);
float k = kick(kt) * 0.4;
audio *= min(1.0, kt * 6.0 / SPB);
audio += vec2(k); }
// Hi-hat (every half beat, panned right)
{ float ht = mod(time, SPB * 0.5);
audio += vec2(0.4, 0.6) * hihat(ht) * 0.12; }
// Ping-pong delay (melody)
{ float ec = 0.3;
for (int i = 1; i <= ECHO_TAPS; i++) {
float dt = ECHO_DELAY * float(i);
int idx = int(mod((time - dt) / SPB, 16.0));
float nt = fract((time - dt) / SPB) * SPB;
float echoed = instrument(noteFreq(MELODY[idx]), nt) * 0.25 * ec;
if (i % 2 == 0) audio += vec2(0.3, 1.0) * echoed;
else audio += vec2(1.0, 0.3) * echoed;
ec *= ECHO_DECAY;
} }
audio *= 0.4 * smoothstep(0.0, 2.0, time);
return clamp(audio, -1.0, 1.0);
}
```
## Common Variants
### Variant 1: Subtractive Synthesis / TB-303 Acid Synth
Sawtooth wave through resonant low-pass filter, cutoff frequency modulated by envelope to produce the "wow" sound.
```glsl
#define NSPC 128
float lpf_response(float h, float cutoff, float reso) {
cutoff -= 20.0;
float df = max(h - cutoff, 0.0);
float df2 = abs(h - cutoff);
return exp(-0.005 * df * df) * 0.5 + exp(df2 * df2 * -0.1) * reso;
}
vec2 acid_synth(float freq, float noteTime) {
vec2 v = vec2(0.0);
float cutoff = exp(noteTime * -1.5) * 50.0 + 10.0;
float sqr = step(0.5, fract(noteTime * 4.5));
for (int i = 0; i < NSPC; i++) {
float h = float(i + 1);
float inten = 1.0 / h;
inten = mix(inten, inten * mod(h, 2.0), sqr);
inten *= lpf_response(h, cutoff, 2.2);
v.x += inten * sin((TAU + 0.01) * noteTime * freq * h);
v.y += inten * sin(TAU * noteTime * freq * h);
}
float amp = smoothstep(0.05, 0.0, abs(noteTime - 0.31) - 0.26) * exp(noteTime * -1.0);
return clamp(v * amp * 2.0, -1.0, 1.0);
}
```
### Variant 2: IIR Biquad Filter
Time-domain IIR filter based on the Audio EQ Cookbook, supporting 7 types including low-pass/high-pass/band-pass.
```glsl
float waveSaw(float freq, int samp) {
return fract(freq * float(samp) / iSampleRate) * 2.0 - 1.0;
}
vec2 widerSaw(float freq, int samp) {
int offset = int(freq) * 64;
return vec2(waveSaw(freq, samp - offset), waveSaw(freq, samp + offset));
}
void biquadLPF(float freq, float Q, float sr,
out float b0, out float b1, out float b2,
out float a0, out float a1, out float a2) {
float omega = TAU * freq / sr;
float sn = sin(omega), cs = cos(omega);
float alpha = sn / (2.0 * Q);
b0 = (1.0 - cs) * 0.5; b1 = 1.0 - cs; b2 = (1.0 - cs) * 0.5;
a0 = 1.0 + alpha; a1 = -2.0 * cs; a2 = 1.0 - alpha;
}
```
### Variant 3: Vocal / Formant Synthesis
Vocal tract model simulating human voice by synthesizing vowels through formant frequencies and bandwidths.
```glsl
float tract(float x, float formantFreq, float bandwidth) {
return sin(TAU * formantFreq * x) * exp(-bandwidth * 3.14159 * x);
}
float vowel_aah(float t, float pitch) {
float x = mod(t, 1.0 / pitch);
float aud = tract(x, 710.0, 70.0) * 0.5 // F1
+ tract(x, 1000.0, 90.0) * 0.6 // F2
+ tract(x, 2450.0, 140.0) * 0.4; // F3
return aud;
}
float fricative(float t, float formantFreq) {
return (hash11(floor(formantFreq * t) * 20.0) - 0.5) * 3.0;
}
```
### Variant 4: Algorithmic Composition
Hash pseudo-random melody + scale quantization, multi-layer rhythmic subdivision producing fractal music structures.
```glsl
vec2 noteRing(float n) {
float r = 0.5 + 0.5 * fract(sin(mod(floor(n), 32.123) * 32.123) * 41.123);
n = mod(n, 8.0);
float note = n<1.?0. : n<2.?5. : n<3.?-2. : n<4.?4. : n<5.?7. : n<6.?4. : n<7.?2. : 0.;
return vec2(note, r);
}
vec2 generativeNote(float beat) {
float b2 = floor(beat * 0.25);
return noteRing(b2 * 0.0625) + noteRing(b2 * 0.25) + noteRing(b2);
}
```
### Variant 5: Circle of Fifths Chord Progressions
Automatically generates harmony based on the circle of fifths, advancing +7 semitones every 4 beats, alternating major/minor chords.
```glsl
vec2 mainSound(int samp, float time) {
float id = floor(time / SPB / 4.0);
float offset = id * 7.0;
float minor = mod(id, 4.0) >= 3.0 ? 1.0 : 0.0;
float t = mod(time, SPB * 4.0);
float root = 57.0 + mod(offset, 12.0);
vec2 result = chord(t, root, minor);
result += vec2(0.5, 0.2) * chord(t - SPB * 0.5, root, minor);
result += vec2(0.05, 0.1) * chord(t - SPB, root, minor);
return result;
}
```
## Performance & Composition
**Performance Tips:**
- Harmonic count (`NUM_HARMONICS` / `NSPC`) is the biggest bottleneck; start with 4-8, stop when sufficient
- IIR filters require looping through sample history per output sample; prefer frequency-domain methods
- Each delay tap requires recomputing the full signal chain; 4 taps = 5x computation
- `fract(x)` is faster than `mod(x, 1.0)`; hoist constants out of loops
- Use Common Pass to share constants; avoid redundant computation between Sound and Image
**Composition Tips:**
- **Audio visualization**: Sound output is read via `iChannel0` in the Image shader for spectrum display
- **Raymarching sync**: Common Pass defines shared timeline; Sound/Image reference it synchronously
- **Particle systems**: Use kick triggers to drive particle emission; share BPM/SPB for beat position calculation
- **Post-processing linkage**: Sidechain compression coefficients drive bloom/chromatic aberration/dithering via Common Pass
- **Text overlay**: `message()` in Image shader renders parameter display or interaction instructions
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/sound-synthesis.md)

View File

@@ -0,0 +1,408 @@
# Heightfield Ray Marching Terrain Rendering
## Use Cases
- Procedural generation of natural landscapes (mountains, canyons, dunes, etc.) in ShaderToy / Fragment Shaders
- Complete 3D terrain flythrough scenes in a single pixel shader, without geometry
- Cinematic aerial perspective, soft shadows, and layered material effects
## Core Principles
Rendering pipeline: height field definition → ray marching intersection → normals & materials → lighting → atmospheric effects
- **FBM**: `f(p) = Σ (aⁿ × noise(2ⁿ × R × p))`, a=0.5, R=rotation matrix, 2ⁿ=frequency doubling
- **Derivative erosion**: `f(p) = Σ (aⁿ × noise(p) / (1 + dot(d,d)))`, d=accumulated gradient, suppresses detail on steep slopes
- **Adaptive step size**: `step = factor × (ray.y - terrain_height)`
## Implementation Steps
1. **Noise & hash** — sin-free hash + Value Noise with analytic derivatives (`noised` returns value + partial derivatives)
2. **FBM terrain** — derivative erosion FBM, `mat2(0.8,-0.6,0.6,0.8)` per-layer rotation to eliminate banding; LOD tiers (L=3/M=9/H=16 octaves)
3. **Ray marching** — upper bound clipping + adaptive step `STEP_FACTOR * h` + distance-adaptive precision `abs(h) < 0.0015*t`
4. **Normals** — finite differences, epsilon increases with distance to avoid distant aliasing, using high-precision `terrainH`
5. **Soft shadows** — march toward sun, track `min(k*h/t)` to estimate penumbra
6. **Materials** — blend rock/grass/snow/sand by height + slope + noise
7. **Lighting** — Lambert diffuse + hemisphere ambient + backlight + Fresnel rim light + Blinn-Phong specular
8. **Atmospheric fog** — wavelength-dependent attenuation `exp(-t*k*vec3(1,1.5,4))` + sun scatter fog color
9. **Sky** — zenith-to-horizon gradient + sun disk/halo
10. **Camera** — Look-At matrix + path-following flight, height tracks terrain
## Complete Code Template
```glsl
// =====================================================
// Heightfield Terrain Rendering - Complete Template
// =====================================================
#define TERRAIN_OCTAVES 9 // FBM octave count (3~16)
#define TERRAIN_SCALE 0.003 // Terrain spatial frequency
#define TERRAIN_HEIGHT 120.0 // Terrain elevation scale
#define MAX_STEPS 300 // Ray march step count (80~400)
#define MAX_DIST 5000.0 // Maximum render distance
#define STEP_FACTOR 0.4 // March conservative factor (0.3~0.8)
#define SHADOW_STEPS 80 // Shadow step count (32~128)
#define SHADOW_K 16.0 // Penumbra softness (8~64)
#define FOG_DENSITY 0.00025 // Fog density
#define SNOW_HEIGHT 80.0 // Snow line height
#define CAM_ALTITUDE 20.0 // Camera height above ground
#define SUN_DIR normalize(vec3(0.8, 0.4, -0.6))
#define SUN_COL vec3(8.0, 5.0, 3.0)
#define SKY_COL vec3(0.5, 0.7, 1.0)
// ---- Hash & Noise ----
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 19.19);
return fract((p3.x + p3.y) * p3.z);
}
vec3 noised(in vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
vec2 du = 6.0 * f * (1.0 - f);
float a = hash(i + vec2(0.0, 0.0));
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
float v = a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y;
vec2 g = du * (vec2(b - a, c - a) + (a - b - c + d) * u.yx);
return vec3(v, g);
}
float noise(in vec2 p) { return noised(p).x; }
// ---- FBM Terrain (derivative erosion) + LOD ----
const mat2 m2 = mat2(0.8, -0.6, 0.6, 0.8);
float terrainFBM(in vec2 p, int octaves) {
p *= TERRAIN_SCALE;
float a = 0.0, b = 1.0;
vec2 d = vec2(0.0);
for (int i = 0; i < 16; i++) {
if (i >= octaves) break;
vec3 n = noised(p);
d += n.yz;
a += b * n.x / (1.0 + dot(d, d));
b *= 0.5;
p = m2 * p * 2.0;
}
return a * TERRAIN_HEIGHT;
}
float terrainL(vec2 p) { return terrainFBM(p, 3); }
float terrainM(vec2 p) { return terrainFBM(p, TERRAIN_OCTAVES); }
float terrainH(vec2 p) { return terrainFBM(p, 16); }
// ---- Ray Marching ----
float raymarch(in vec3 ro, in vec3 rd) {
float t = 0.0;
if (ro.y > TERRAIN_HEIGHT && rd.y >= 0.0) return -1.0;
if (ro.y > TERRAIN_HEIGHT) t = (ro.y - TERRAIN_HEIGHT) / (-rd.y);
for (int i = 0; i < MAX_STEPS; i++) {
vec3 pos = ro + t * rd;
float h = pos.y - terrainM(pos.xz);
if (abs(h) < 0.0015 * t) break;
if (t > MAX_DIST) return -1.0;
t += STEP_FACTOR * h;
}
return t;
}
// ---- Normals ----
vec3 calcNormal(in vec3 pos, float t) {
float eps = 0.02 + 0.00005 * t * t;
float hC = terrainH(pos.xz);
float hR = terrainH(pos.xz + vec2(eps, 0.0));
float hU = terrainH(pos.xz + vec2(0.0, eps));
return normalize(vec3(hC - hR, eps, hC - hU));
}
// ---- Soft Shadows ----
float calcShadow(in vec3 pos, in vec3 sunDir) {
float res = 1.0, t = 1.0;
for (int i = 0; i < SHADOW_STEPS; i++) {
vec3 p = pos + t * sunDir;
float h = p.y - terrainM(p.xz);
if (h < 0.001) return 0.0;
res = min(res, SHADOW_K * h / t);
t += clamp(h, 2.0, 100.0);
}
return clamp(res, 0.0, 1.0);
}
// ---- Materials ----
vec3 getMaterial(in vec3 pos, in vec3 nor) {
float slope = nor.y, h = pos.y;
float nz = noise(pos.xz * 0.04) * noise(pos.xz * 0.005);
vec3 rock = vec3(0.10, 0.09, 0.08);
vec3 grass = mix(vec3(0.10, 0.08, 0.04), vec3(0.05, 0.09, 0.02), nz);
vec3 snow = vec3(0.62, 0.65, 0.70);
vec3 sand = vec3(0.50, 0.45, 0.35);
vec3 col = rock;
col = mix(col, grass, smoothstep(0.5, 0.8, slope));
float snowMask = smoothstep(SNOW_HEIGHT - 20.0 * nz, SNOW_HEIGHT + 10.0, h)
* smoothstep(0.3, 0.7, slope);
col = mix(col, snow, snowMask);
float beachMask = smoothstep(2.5, 0.0, h) * smoothstep(0.5, 0.9, slope);
col = mix(col, sand, beachMask);
return col;
}
// ---- Lighting ----
vec3 calcLighting(in vec3 pos, in vec3 nor, in vec3 rd, float shadow) {
float dif = clamp(dot(nor, SUN_DIR), 0.0, 1.0);
float amb = 0.5 + 0.5 * nor.y;
vec3 backDir = normalize(vec3(-SUN_DIR.x, 0.0, -SUN_DIR.z));
float bac = clamp(0.2 + 0.8 * dot(nor, backDir), 0.0, 1.0);
float fre = pow(clamp(1.0 + dot(rd, nor), 0.0, 1.0), 2.0);
vec3 hal = normalize(SUN_DIR - rd);
float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0)
* (0.04 + 0.96 * pow(1.0 + dot(hal, rd), 5.0));
vec3 lin = vec3(0.0);
lin += dif * shadow * SUN_COL * 0.1;
lin += amb * SKY_COL * 0.2;
lin += bac * vec3(0.15, 0.05, 0.04);
lin += fre * SKY_COL * 0.3;
lin += spe * shadow * SUN_COL * 0.05;
return lin;
}
// ---- Atmosphere ----
vec3 applyFog(in vec3 col, float t, in vec3 rd) {
vec3 ext = exp(-t * FOG_DENSITY * vec3(1.0, 1.5, 4.0));
float sundot = clamp(dot(rd, SUN_DIR), 0.0, 1.0);
vec3 fogCol = mix(vec3(0.55, 0.55, 0.58), vec3(1.0, 0.7, 0.3), 0.3 * pow(sundot, 8.0));
return col * ext + fogCol * (1.0 - ext);
}
// ---- Sky ----
vec3 getSky(in vec3 rd) {
vec3 col = vec3(0.3, 0.5, 0.85) - rd.y * vec3(0.2, 0.15, 0.0);
float horizon = pow(1.0 - max(rd.y, 0.0), 4.0);
col = mix(col, vec3(0.8, 0.75, 0.7), 0.5 * horizon);
float sundot = clamp(dot(rd, SUN_DIR), 0.0, 1.0);
col += vec3(1.0, 0.7, 0.3) * 0.3 * pow(sundot, 8.0);
col += vec3(1.0, 0.9, 0.7) * 0.5 * pow(sundot, 64.0);
col += vec3(1.0, 1.0, 0.9) * min(pow(sundot, 1150.0), 0.3);
return col;
}
// ---- Camera ----
vec3 cameraPath(float t) {
return vec3(100.0 * sin(0.2 * t), 0.0, -100.0 * t);
}
mat3 setCamera(in vec3 ro, in vec3 ta) {
vec3 cw = normalize(ta - ro);
vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0)));
vec3 cv = cross(cu, cw);
return mat3(cu, cv, cw);
}
// ======== Main Function ========
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
float time = iTime * 0.5;
vec3 ro = cameraPath(time);
ro.y = terrainL(ro.xz) + CAM_ALTITUDE;
vec3 ta = cameraPath(time + 2.0);
ta.y = terrainL(ta.xz) + CAM_ALTITUDE * 0.5;
mat3 cam = setCamera(ro, ta);
vec3 rd = cam * normalize(vec3(uv, 1.5));
float t = raymarch(ro, rd);
vec3 col;
if (t > 0.0) {
vec3 pos = ro + t * rd;
vec3 nor = calcNormal(pos, t);
vec3 mate = getMaterial(pos, nor);
float sha = calcShadow(pos + nor * 0.5, SUN_DIR);
vec3 lin = calcLighting(pos, nor, rd, sha);
col = mate * lin;
col = applyFog(col, t, rd);
} else {
col = getSky(rd);
}
col = 1.0 - exp(-col * 2.0);
col = pow(col, vec3(1.0 / 2.2));
fragColor = vec4(col, 1.0);
}
```
### Binary Refinement (optional, called after raymarch)
```glsl
float bisect(in vec3 ro, in vec3 rd, float tNear, float tFar) {
for (int i = 0; i < 5; i++) {
float tMid = 0.5 * (tNear + tFar);
vec3 pos = ro + tMid * rd;
float h = pos.y - terrainM(pos.xz);
if (h > 0.0) tNear = tMid; else tFar = tMid;
}
return 0.5 * (tNear + tFar);
}
```
## Common Variants
### Relaxation Marching
Automatically increases step size at far distances, covering greater range in 90 steps.
```glsl
float raymarchRelax(in vec3 ro, in vec3 rd) {
float t = 0.0;
float d = (ro + rd * t).y - terrainM((ro + rd * t).xz);
for (int i = 0; i < 90; i++) {
if (abs(d) < t * 0.0001 || t > 400.0) break;
float rl = max(t * 0.02, 1.0);
t += d * rl;
vec3 pos = ro + t * rd;
d = (pos.y - terrainM(pos.xz)) * 0.7;
}
return t;
}
```
### Sign-Alternating FBM
Amplitude flips sign each layer, producing rugged alternating ridge/valley patterns.
```glsl
float terrainSignFlip(in vec2 p) {
p *= TERRAIN_SCALE;
float a = 0.0, w = 1.0;
for (int i = 0; i < TERRAIN_OCTAVES; i++) {
a += w * noise(p);
w = -w * 0.4;
p = m2 * p * 2.0;
}
return a * TERRAIN_HEIGHT;
}
```
### Canyon Style (Texture-Driven + 3D Displacement)
Texture sampling + 3D FBM displacement, supporting cliffs/caves and other non-heightfield formations.
```glsl
float noise3D(in vec3 x) {
vec3 p = floor(x); vec3 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
vec2 uv = (p.xy + vec2(37.0, 17.0) * p.z) + f.xy;
vec2 rg = textureLod(iChannel0, (uv + 0.5) / 256.0, 0.0).yx;
return mix(rg.x, rg.y, f.z);
}
const mat3 m3 = mat3(0.00, 0.80, 0.60, -0.80, 0.36,-0.48, -0.60,-0.48, 0.64);
float displacement(vec3 p) {
float f = 0.5 * noise3D(p); p = m3 * p * 2.02;
f += 0.25 * noise3D(p); p = m3 * p * 2.03;
f += 0.125 * noise3D(p); p = m3 * p * 2.01;
f += 0.0625 * noise3D(p);
return f;
}
float mapCanyon(vec3 p) {
float h = terrainM(p.xz);
float dis = displacement(0.25 * p * vec3(1.0, 4.0, 1.0)) * 3.0;
return (dis + p.y - h) * 0.25;
}
```
### Directional Erosion Noise
Slope direction drives Gabor noise projection, producing realistic dendritic drainage patterns.
```glsl
#define EROSION_BRANCH 1.5
vec3 erosionNoise(vec2 p, vec2 dir) {
vec2 ip = floor(p); vec2 fp = fract(p) - 0.5;
float va = 0.0, wt = 0.0; vec2 dva = vec2(0.0);
for (int i = -2; i <= 1; i++)
for (int j = -2; j <= 1; j++) {
vec2 o = vec2(float(i), float(j));
vec2 h = hash2(ip - o) * 0.5;
vec2 pp = fp + o + h;
float d = dot(pp, pp);
float w = exp(-d * 2.0);
float mag = dot(pp, dir);
va += cos(mag * 6.283) * w;
dva += -sin(mag * 6.283) * dir * w;
wt += w;
}
return vec3(va, dva) / wt;
}
float terrainErosion(vec2 p, vec2 baseSlope) {
float e = 0.0, a = 0.5;
vec2 dir = normalize(baseSlope + vec2(0.001));
for (int i = 0; i < 5; i++) {
vec3 n = erosionNoise(p * 4.0, dir);
e += a * n.x;
dir = normalize(dir + n.zy * vec2(1.0, -1.0) * EROSION_BRANCH);
a *= 0.5; p *= 2.0;
}
return e;
}
```
### Volumetric Clouds + God Rays
Front-to-back alpha compositing of cloud slabs, accumulating god ray factor.
```glsl
#define CLOUD_BASE 200.0
#define CLOUD_TOP 300.0
vec4 raymarchClouds(vec3 ro, vec3 rd) {
float tmin = (CLOUD_BASE - ro.y) / rd.y;
float tmax = (CLOUD_TOP - ro.y) / rd.y;
if (tmin > tmax) { float tmp = tmin; tmin = tmp; tmax = tmp; }
if (tmin < 0.0) tmin = 0.0;
float t = tmin;
vec4 sum = vec4(0.0); float rays = 0.0;
for (int i = 0; i < 64; i++) {
if (sum.a > 0.99 || t > tmax) break;
vec3 pos = ro + t * rd;
float hFrac = (pos.y - CLOUD_BASE) / (CLOUD_TOP - CLOUD_BASE);
float shape = 1.0 - 2.0 * abs(hFrac - 0.5);
float den = shape - 1.6 * (1.0 - noise(pos.xz * 0.01));
if (den > 0.0) {
float shadowDen = shape - 1.6 * (1.0 - noise((pos.xz + SUN_DIR.xz * 30.0) * 0.01));
float shadow = clamp(1.0 - shadowDen * 2.0, 0.0, 1.0);
vec3 cloudCol = mix(vec3(0.4, 0.4, 0.45), vec3(1.0, 0.95, 0.8), shadow);
float alpha = clamp(den * 0.4, 0.0, 1.0);
rays += 0.02 * shadow * (1.0 - sum.a);
cloudCol *= alpha;
sum += vec4(cloudCol, alpha) * (1.0 - sum.a);
}
t += max(0.5, 0.05 * t);
}
sum.rgb += pow(rays, 3.0) * 0.4 * vec3(1.0, 0.8, 0.7);
return sum;
}
```
## Performance & Composition
**Performance:**
- LOD tiers: low octaves for marching (3-9), high octaves for normals (16), lowest for camera (3)
- Upper bound clipping: intersect ray with terrain max height plane before marching
- Adaptive precision: hit threshold `abs(h) < k * t`, tolerates larger error at distance
- Texture instead of noise: `textureLod` sampling of pre-baked noise, 2-3x speed
- Early exit: `t > MAX_DIST`, `alpha > 0.99`, shadow `h < 0`
- Dithered start: `t += hash(fragCoord) * step_size` to eliminate banding artifacts
**Composition:**
- Terrain + water: water at a fixed y-plane, multi-frequency noise perturbing normals, Fresnel controlling reflection/refraction
- Terrain + volumetric clouds: render terrain first, then march cloud slab, front-to-back alpha compositing
- Terrain + volumetric fog: additionally sample 3D FBM density field along ray, decay with distance
- Terrain + SDF objects: `floor(p.xz/gridSize)` grid placement, `hash(cell)` randomization
- Terrain + TAA: inter-frame reprojection blending, ~10% new frame + 90% history frame
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/terrain-rendering.md)

View File

@@ -0,0 +1,121 @@
# Advanced Texture Mapping Techniques
## Use Cases
- Texturing 3D surfaces without UV seams (triplanar/biplanar mapping)
- Eliminating visible tiling repetition on large surfaces
- Proper texture filtering in ray-marched scenes (mip-level selection)
- Combining procedural and sampled textures
## Techniques
### 1. Biplanar Mapping (Optimized Triplanar)
Uses only 2 texture fetches instead of 3, selecting the two most relevant projection axes:
```glsl
vec4 biplanar(sampler2D sam, vec3 p, vec3 n, float k) {
vec3 dpdx = dFdx(p);
vec3 dpdy = dFdy(p);
n = abs(n);
// Determine major, minor, median axes
ivec3 ma = (n.x > n.y && n.x > n.z) ? ivec3(0,1,2) :
(n.y > n.z) ? ivec3(1,2,0) : ivec3(2,0,1);
ivec3 mi = (n.x < n.y && n.x < n.z) ? ivec3(0,1,2) :
(n.y < n.z) ? ivec3(1,2,0) : ivec3(2,0,1);
ivec3 me = ivec3(3) - mi - ma;
// Two texture fetches (major and median projections)
vec4 x = textureGrad(sam, vec2(p[ma.y], p[ma.z]),
vec2(dpdx[ma.y], dpdx[ma.z]),
vec2(dpdy[ma.y], dpdy[ma.z]));
vec4 y = textureGrad(sam, vec2(p[me.y], p[me.z]),
vec2(dpdx[me.y], dpdx[me.z]),
vec2(dpdy[me.y], dpdy[me.z]));
// Blend weights with local support
vec2 w = vec2(n[ma.x], n[me.x]);
w = clamp((w - 0.5773) / (1.0 - 0.5773), 0.0, 1.0); // 0.5773 = 1/sqrt(3)
w = pow(w, vec2(k / 8.0));
return (x * w.x + y * w.y) / (w.x + w.y);
}
// Usage: vec4 col = biplanar(tex, worldPos * scale, worldNormal, 8.0);
```
**Why biplanar over triplanar**: Saves one texture fetch (bandwidth-bound advantage), with k=8 visually equivalent to triplanar. The `dFdx/dFdy` gradient propagation prevents mipmap seams at axis-switching boundaries.
### 2. Texture Repetition Avoidance
Three approaches to eliminate visible tiling patterns:
#### Method A: Per-Tile Random Offset (4 fetches)
```glsl
vec4 textureNoTile(sampler2D sam, vec2 uv) {
vec2 iuv = floor(uv);
vec2 fuv = fract(uv);
// Generate 4 random offsets for the 4 surrounding tiles
vec4 ofa = hash42(iuv + vec2(0, 0));
vec4 ofb = hash42(iuv + vec2(1, 0));
vec4 ofc = hash42(iuv + vec2(0, 1));
vec4 ofd = hash42(iuv + vec2(1, 1));
// Transform UVs per tile
vec2 uva = uv + ofa.xy;
vec2 uvb = uv + ofb.xy;
vec2 uvc = uv + ofc.xy;
vec2 uvd = uv + ofd.xy;
// Blend near borders with smooth weights
vec2 b = smoothstep(0.25, 0.75, fuv);
return mix(mix(texture(sam, uva), texture(sam, uvb), b.x),
mix(texture(sam, uvc), texture(sam, uvd), b.x), b.y);
}
```
#### Method B: Virtual Pattern (2 fetches, cheapest)
```glsl
vec4 textureNoTileCheap(sampler2D sam, vec2 uv) {
float k = texture(iChannel1, 0.005 * uv).x; // low-freq variation index
float index = k * 8.0;
float i = floor(index);
float f = fract(index);
// Two offset lookups based on index
vec2 offa = sin(vec2(3.0, 7.0) * (i + 0.0));
vec2 offb = sin(vec2(3.0, 7.0) * (i + 1.0));
return mix(texture(sam, uv + offa), texture(sam, uv + offb), smoothstep(0.2, 0.8, f));
}
```
### 3. Ray Differential Texture Filtering
For ray-marched scenes, compute proper mip levels using ray differentials:
```glsl
// After finding hit point pos with normal nor:
// 1. Compute neighbor pixel ray directions
vec3 rdx = normalize(rd + dFdx(rd)); // x-neighbor ray
vec3 rdy = normalize(rd + dFdy(rd)); // y-neighbor ray
// 2. Intersect neighbors with tangent plane at hit point
float dt_dx = -dot(pos - ro, nor) / dot(rdx, nor);
float dt_dy = -dot(pos - ro, nor) / dot(rdy, nor);
vec3 posDx = ro + rdx * dt_dx;
vec3 posDy = ro + rdy * dt_dy;
// 3. World-space position derivatives = pixel footprint
vec3 dposdx = posDx - pos;
vec3 dposdy = posDy - pos;
// 4. Transform to texture derivatives and use textureGrad
// For simple planar mapping (e.g. ground plane):
vec2 duvdx = dposdx.xz * textureScale;
vec2 duvdy = dposdy.xz * textureScale;
vec4 color = textureGrad(tex, pos.xz * textureScale, duvdx, duvdy);
```
This provides correct mip-level selection for procedural and sampled textures on ray-marched surfaces, eliminating shimmer and aliasing at distance.
→ For deeper details, see [reference/texture-mapping-advanced.md](../reference/texture-mapping-advanced.md)

View File

@@ -0,0 +1,382 @@
**IMPORTANT - GLSL Type Strictness**:
- GLSL is a strongly-typed language and does not support the `string` type (you cannot define `string var`)
- `vec2`/`vec3`/`vec4` are vector types and cannot be directly assigned a float (e.g., `vec2 a = 1.0` must be `vec2 a = vec2(1.0)`)
- Array indices must be integer constants or uniform variables; runtime-computed floats cannot be used
- Avoid uninitialized variables — GLSL default values are undefined
# Texture Sampling
## Use Cases
- **Post-processing effects**: Blur, bloom, dispersion, chromatic aberration
- **Procedural noise**: FBM layering from noise textures to generate terrain, clouds, fire
- **PBR/IBL**: Cubemap environment lighting, BRDF LUT lookup
- **Simulation/feedback systems**: Reaction-diffusion, fluid simulation multi-buffer feedback
- **Data storage**: Textures used as structured data (game state, keyboard input)
- **Temporal accumulation**: TAA, motion blur, previous frame reading
## Core Principles
| Function | Coordinate Type | Filtering | Typical Use |
|----------|----------------|-----------|-------------|
| `texture(sampler, uv)` | Float UV `[0,1]` | Hardware bilinear | General texture reading |
| `textureLod(sampler, uv, lod)` | Float UV + LOD | Specified mip level | Control blur level / avoid auto mip |
| `texelFetch(sampler, ivec2, lod)` | Integer pixel coordinates | No filtering | Exact pixel data reading |
Key mathematics:
1. **Hardware bilinear interpolation**: `texture()` automatically linearly blends between 4 adjacent texels
2. **Quintic Hermite smoothing**: `u = f^3(6f^2 - 15f + 10)`, C2 continuous (eliminates hardware linear interpolation seams)
3. **LOD control**: `textureLod` third parameter selects mipmap level, `lod=0` is original resolution, each +1 halves resolution
4. **Coordinate wrapping**: `fract(uv)` implements torus boundary, equivalent to `GL_REPEAT`
## Implementation Steps
### Step 1: Basic Sampling and UV Normalization
```glsl
vec2 uv = fragCoord / iResolution.xy;
vec4 col = texture(iChannel0, uv);
```
### Step 2: textureLod for Mipmap Control
```glsl
// In ray marching: force LOD 0 to avoid artifacts
vec3 groundCol = textureLod(iChannel2, groundUv * 0.05, 0.0).rgb;
// Depth of field blur: LOD varies with distance
float focus = mix(maxBlur - coverage, minBlur, smoothstep(.1, .2, coverage));
vec3 col = textureLod(iChannel0, uv + normal, focus).rgb;
// Bloom: sample high mip levels
#define BLOOM_LOD_A 4.0 // adjustable: bloom first mip level
#define BLOOM_LOD_B 5.0
#define BLOOM_LOD_C 6.0
vec3 bloom = vec3(0.0);
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_A), BLOOM_LOD_A).rgb;
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_B), BLOOM_LOD_B).rgb;
bloom += textureLod(iChannel0, uv + off * exp2(BLOOM_LOD_C), BLOOM_LOD_C).rgb;
bloom /= 3.0;
```
### Step 3: texelFetch for Exact Pixel Reading
```glsl
// Data storage addresses
const ivec2 txBallPosVel = ivec2(0, 0);
const ivec2 txPaddlePos = ivec2(1, 0);
const ivec2 txPoints = ivec2(2, 0);
const ivec2 txState = ivec2(3, 0);
vec4 loadValue(in ivec2 addr) {
return texelFetch(iChannel0, addr, 0);
}
void storeValue(in ivec2 addr, in vec4 val, inout vec4 fragColor, in ivec2 fragPos) {
fragColor = (fragPos == addr) ? val : fragColor;
}
// Keyboard input
float key = texelFetch(iChannel1, ivec2(KEY_SPACE, 0), 0).x;
```
### Step 4: Manual Bilinear + Quintic Hermite Smoothing
```glsl
float noise(vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); // C2 continuous
#define TEX_RES 1024.0 // adjustable: noise texture resolution
float a = texture(iChannel0, (p + vec2(0.0, 0.0)) / TEX_RES).x;
float b = texture(iChannel0, (p + vec2(1.0, 0.0)) / TEX_RES).x;
float c = texture(iChannel0, (p + vec2(0.0, 1.0)) / TEX_RES).x;
float d = texture(iChannel0, (p + vec2(1.0, 1.0)) / TEX_RES).x;
return a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y;
}
```
### Step 5: FBM Texture Noise
```glsl
#define FBM_OCTAVES 5 // adjustable: number of layers
#define FBM_PERSISTENCE 0.5 // adjustable: amplitude decay rate
float fbm(vec2 x) {
float v = 0.0;
float a = 0.5;
float totalWeight = 0.0;
for (int i = 0; i < FBM_OCTAVES; i++) {
v += a * noise(x);
totalWeight += a;
x *= 2.0;
a *= FBM_PERSISTENCE;
}
return v / totalWeight;
}
```
### Step 6: Separable Gaussian Blur
```glsl
#define BLUR_RADIUS 4 // adjustable: blur radius
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
vec2 d = vec2(1.0 / iResolution.x, 0.0); // horizontal pass; for vertical pass change to vec2(0, 1/iResolution.y)
float w[9] = float[9](0.05, 0.09, 0.12, 0.15, 0.16, 0.15, 0.12, 0.09, 0.05);
vec4 col = vec4(0.0);
for (int i = -4; i <= 4; i++) {
col += w[i + 4] * texture(iChannel0, fract(uv + float(i) * d));
}
col /= 0.98;
fragColor = col;
}
```
### Step 7: Dispersion Sampling
```glsl
#define DISP_SAMPLES 64 // adjustable: sample count
vec3 sampleWeights(float i) {
return vec3(i * i, 46.6666 * pow((1.0 - i) * i, 3.0), (1.0 - i) * (1.0 - i));
}
vec3 sampleDisp(sampler2D tex, vec2 uv, vec2 disp) {
vec3 col = vec3(0.0);
vec3 totalWeight = vec3(0.0);
for (int i = 0; i < DISP_SAMPLES; i++) {
float t = float(i) / float(DISP_SAMPLES);
vec3 w = sampleWeights(t);
col += w * texture(tex, fract(uv + disp * t)).rgb;
totalWeight += w;
}
return col / totalWeight;
}
```
### Step 8: IBL Environment Sampling
```glsl
#define MAX_LOD 7.0 // adjustable: cubemap max mip level
#define DIFFUSE_LOD 6.5 // adjustable: diffuse sampling LOD
vec3 getSpecularLightColor(vec3 N, float roughness) {
vec3 raw = textureLod(iChannel0, N, roughness * MAX_LOD).rgb;
return pow(raw, vec3(4.5)) * 6.5; // HDR approximation
}
vec3 getDiffuseLightColor(vec3 N) {
return textureLod(iChannel0, N, DIFFUSE_LOD).rgb;
}
// BRDF LUT lookup
vec2 brdf = texture(iChannel3, vec2(NdotV, roughness)).rg;
vec3 specular = envColor * (F * brdf.x + brdf.y);
```
## Complete Code Template
iChannel0 bound to a noise texture (e.g., "Gray Noise Medium"), with mipmap enabled.
```glsl
// === Texture Sampling Comprehensive Demo ===
// iChannel0: noise texture (requires mipmap enabled)
#define TEX_RES 256.0
#define FBM_OCTAVES 6
#define FBM_PERSISTENCE 0.5
#define CLOUD_LAYERS 4
#define CLOUD_SPEED 0.02
#define DOF_MAX_BLUR 5.0
#define DOF_FOCUS_DIST 0.5
#define BLOOM_STRENGTH 0.3
#define BLOOM_LOD 4.0
float noise(vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = textureLod(iChannel0, (p + vec2(0.0, 0.0)) / TEX_RES, 0.0).x;
float b = textureLod(iChannel0, (p + vec2(1.0, 0.0)) / TEX_RES, 0.0).x;
float c = textureLod(iChannel0, (p + vec2(0.0, 1.0)) / TEX_RES, 0.0).x;
float d = textureLod(iChannel0, (p + vec2(1.0, 1.0)) / TEX_RES, 0.0).x;
return a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y;
}
float fbm(vec2 x) {
float v = 0.0;
float a = 0.5;
float w = 0.0;
for (int i = 0; i < FBM_OCTAVES; i++) {
v += a * noise(x);
w += a;
x *= 2.0;
a *= FBM_PERSISTENCE;
}
return v / w;
}
float cloudLayer(vec2 uv, float height, float time) {
vec2 offset = vec2(time * CLOUD_SPEED * (1.0 + height), 0.0);
float n = fbm((uv + offset) * (2.0 + height * 3.0));
return smoothstep(0.4, 0.7, n);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
float aspect = iResolution.x / iResolution.y;
// 1. Procedural sky
vec3 sky = mix(vec3(0.1, 0.15, 0.4), vec3(0.5, 0.7, 1.0), uv.y);
// 2. FBM cloud layers
vec3 col = sky;
for (int i = 0; i < CLOUD_LAYERS; i++) {
float h = float(i) / float(CLOUD_LAYERS);
float density = cloudLayer(vec2(uv.x * aspect, uv.y), h, iTime);
vec3 cloudCol = mix(vec3(0.8, 0.85, 0.9), vec3(1.0), h);
col = mix(col, cloudCol, density * (0.3 + 0.7 * h));
}
// 3. textureLod depth of field blur
float dist = abs(uv.y - DOF_FOCUS_DIST);
float lod = dist * DOF_MAX_BLUR;
vec3 blurred = textureLod(iChannel0, uv, lod).rgb;
col = mix(col, blurred * 0.5 + col * 0.5, 0.3);
// 4. Bloom
vec3 bloom = textureLod(iChannel0, uv, BLOOM_LOD).rgb;
bloom += textureLod(iChannel0, uv, BLOOM_LOD + 1.0).rgb;
bloom += textureLod(iChannel0, uv, BLOOM_LOD + 2.0).rgb;
bloom /= 3.0;
col += bloom * BLOOM_STRENGTH;
// 5. Post-processing
col = (col * (6.2 * col + 0.5)) / (col * (6.2 * col + 1.7) + 0.06);
col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.2);
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Anisotropic Flow-Field Blur
```glsl
#define BLUR_ITERATIONS 32 // adjustable: number of samples along flow field
#define BLUR_STEP 0.008 // adjustable: UV offset per step
vec3 flowBlur(vec2 uv) {
vec3 col = vec3(0.0);
float acc = 0.0;
for (int i = 0; i < BLUR_ITERATIONS; i++) {
float h = float(i) / float(BLUR_ITERATIONS);
float w = 4.0 * h * (1.0 - h);
col += w * texture(iChannel0, uv).rgb;
acc += w;
vec2 dir = texture(iChannel1, uv).xy * 2.0 - 1.0;
uv += BLUR_STEP * dir;
}
return col / acc;
}
```
### Variant 2: Buffer-as-Data Storage
```glsl
const ivec2 txPosition = ivec2(0, 0);
const ivec2 txVelocity = ivec2(1, 0);
const ivec2 txState = ivec2(2, 0);
vec4 load(ivec2 addr) { return texelFetch(iChannel0, addr, 0); }
void store(ivec2 addr, vec4 val, inout vec4 fragColor, ivec2 fragPos) {
fragColor = (fragPos == addr) ? val : fragColor;
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
ivec2 p = ivec2(fragCoord);
fragColor = texelFetch(iChannel0, p, 0);
vec4 pos = load(txPosition);
vec4 vel = load(txVelocity);
// ... update logic ...
store(txPosition, pos + vel * 0.016, fragColor, p);
store(txVelocity, vel, fragColor, p);
}
```
### Variant 3: Dispersion Effect
```glsl
#define DISP_SAMPLES 64 // adjustable: sample count
#define DISP_STRENGTH 0.05 // adjustable: dispersion strength
vec3 dispersion(vec2 uv, vec2 displacement) {
vec3 col = vec3(0.0);
vec3 w_total = vec3(0.0);
for (int i = 0; i < DISP_SAMPLES; i++) {
float t = float(i) / float(DISP_SAMPLES);
vec3 w = vec3(t * t, 46.666 * pow((1.0 - t) * t, 3.0), (1.0 - t) * (1.0 - t));
col += w * texture(iChannel0, fract(uv + displacement * t * DISP_STRENGTH)).rgb;
w_total += w;
}
return col / w_total;
}
```
### Variant 4: Triplanar Texture Mapping
```glsl
#define TRIPLANAR_SHARPNESS 2.0 // adjustable: blend sharpness
vec3 triplanarSample(sampler2D tex, vec3 pos, vec3 normal, float scale) {
vec3 w = pow(abs(normal), vec3(TRIPLANAR_SHARPNESS));
w /= (w.x + w.y + w.z);
vec3 xSample = texture(tex, pos.yz * scale).rgb;
vec3 ySample = texture(tex, pos.xz * scale).rgb;
vec3 zSample = texture(tex, pos.xy * scale).rgb;
return xSample * w.x + ySample * w.y + zSample * w.z;
}
```
### Variant 5: Temporal Reprojection (TAA)
```glsl
#define TAA_BLEND 0.9 // adjustable: history frame blend ratio
vec3 temporalBlend(vec2 currUv, vec2 prevUv, vec3 currColor) {
vec3 history = textureLod(iChannel0, prevUv, 0.0).rgb;
vec3 minCol = currColor - 0.1;
vec3 maxCol = currColor + 0.1;
history = clamp(history, minCol, maxCol);
return mix(currColor, history, TAA_BLEND);
}
```
## Performance & Composition
**Performance Tips**:
- Heavy sampling (e.g., 64 dispersion samples) is a bandwidth bottleneck — reduce sample count + use smart weight compensation; use `textureLod` with high LOD to reduce cache misses
- 2D Gaussian blur uses separable two-pass (O(N^2) -> O(2N)), leveraging hardware bilinear for (N+1)/2 samples to achieve N-tap
- Must use `textureLod(..., 0.0)` inside ray marching — the GPU cannot correctly estimate screen-space derivatives
- Manual Hermite interpolation is ~4x slower than hardware — only use for the first two FBM octaves, fall back to `texture()` for higher frequencies
- Each multi-buffer feedback adds one frame of latency — merge operations into the same pass; use `texelFetch` to avoid filtering overhead
**Composition Tips**:
- **+ SDF Ray Marching**: Noise textures for displacement maps/materials; use `textureLod(..., 0.0)` inside ray marching
- **+ Procedural Noise**: Hermite + FBM driving domain warping to generate terrain/clouds/fire; texture noise is faster than pure mathematical noise
- **+ Post-Processing Pipeline**: Multi-LOD bloom → separable DOF → dispersion → tone mapping, chaining a complete post-processing pipeline
- **+ PBR/IBL**: `textureLod` samples cubemap by roughness + BRDF LUT lookup = split-sum IBL
- **+ Simulation/Feedback**: Multi-buffer reaction-diffusion/fluid; Buffer A state, B/C separable blur diffusion, Image visualization; `fract()` torus boundary
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/texture-sampling.md)

View File

@@ -0,0 +1,375 @@
# Volumetric Rendering Skill
## Use Cases
- Rendering participating media: clouds, fog, smoke, fire, explosions, atmospheric scattering
- Visual effects of light passing through and scattering/absorbing within semi-transparent volumes
- Suitable for ShaderToy real-time fragment shaders, also portable to game engines
## Core Principles
Advance along each view ray at fixed or adaptive step sizes (Ray Marching), querying medium density at each sample point, accumulating color and opacity.
### Key Formulas
**Beer-Lambert transmittance**: `T = exp(-σe × d)`, where `σe = σs + σa`
**Front-to-back alpha compositing (premultiplied form)**:
```glsl
col.rgb *= col.a;
sum += col * (1.0 - sum.a);
```
**Henyey-Greenstein phase function**: `HG(cosθ, g) = (1 - g²) / (1 + g² - 2g·cosθ)^(3/2)`
- `g > 0` forward scattering, `g < 0` back scattering, `g = 0` isotropic
**Frostbite improved integration**: `Sint = (S - S × exp(-σe × dt)) / σe`
## Implementation Steps
### Step 1: Camera and Ray Construction
```glsl
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
vec3 ro = vec3(0.0, 1.0, -5.0); // Camera position
vec3 ta = vec3(0.0, 0.0, 0.0); // Look-at target
vec3 ww = normalize(ta - ro);
vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0)));
vec3 vv = cross(uu, ww);
float fl = 1.5; // Focal length
vec3 rd = normalize(uv.x * uu + uv.y * vv + fl * ww);
```
### Step 2: Volume Bounds Intersection
```glsl
// Method A: Horizontal plane bounds (cloud layers)
float tmin = (yBottom - ro.y) / rd.y;
float tmax = (yTop - ro.y) / rd.y;
if (tmin > tmax) { float tmp = tmin; tmin = tmax; tmax = tmp; }
// Method B: Sphere bounds (explosions, atmosphere)
vec2 intersectSphere(vec3 ro, vec3 rd, float r) {
float b = dot(ro, rd);
float c = dot(ro, ro) - r * r;
float d = b * b - c;
if (d < 0.0) return vec2(1e5, -1e5);
d = sqrt(d);
return vec2(-b - d, -b + d);
}
```
### Step 3: Density Field Definition
```glsl
// 3D Value Noise (texture-based)
float noise(vec3 x) {
vec3 p = floor(x);
vec3 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
vec2 uv = (p.xy + vec2(37.0, 239.0) * p.z) + f.xy;
vec2 rg = textureLod(iChannel0, (uv + 0.5) / 256.0, 0.0).yx;
return mix(rg.x, rg.y, f.z);
}
// fBM
float fbm(vec3 p) {
float f = 0.0;
f += 0.50000 * noise(p); p *= 2.02;
f += 0.25000 * noise(p); p *= 2.03;
f += 0.12500 * noise(p); p *= 2.01;
f += 0.06250 * noise(p); p *= 2.02;
f += 0.03125 * noise(p);
return f;
}
// Cloud density
float cloudDensity(vec3 p) {
vec3 q = p - vec3(0.0, 0.1, 1.0) * iTime;
float f = fbm(q);
return clamp(1.5 - p.y - 2.0 + 1.75 * f, 0.0, 1.0);
}
```
### Step 4: Ray Marching Main Loop
```glsl
#define NUM_STEPS 64
#define STEP_SIZE 0.05
vec4 raymarch(vec3 ro, vec3 rd, float tmin, float tmax, vec3 bgCol) {
vec4 sum = vec4(0.0);
// Dither start position to eliminate banding artifacts
float t = tmin + STEP_SIZE * fract(sin(dot(fragCoord, vec2(12.9898, 78.233))) * 43758.5453);
for (int i = 0; i < NUM_STEPS; i++) {
if (t > tmax || sum.a > 0.99) break;
vec3 pos = ro + t * rd;
float den = cloudDensity(pos);
if (den > 0.01) {
vec4 col = vec4(1.0, 0.95, 0.8, den);
col.a *= 0.4;
col.rgb *= col.a;
sum += col * (1.0 - sum.a);
}
t += STEP_SIZE;
}
return clamp(sum, 0.0, 1.0);
}
```
### Step 5: Lighting Calculation
```glsl
// Method A: Directional derivative lighting (1 extra sample)
vec3 sundir = normalize(vec3(1.0, 0.0, -1.0));
float dif = clamp((den - cloudDensity(pos + 0.3 * sundir)) / 0.6, 0.0, 1.0);
vec3 lin = vec3(1.0, 0.6, 0.3) * dif + vec3(0.91, 0.98, 1.05);
// Method B: Volumetric shadow (secondary ray march)
float volumetricShadow(vec3 from, vec3 lightDir) {
float shadow = 1.0, dt = 0.5, d = dt * 0.5;
for (int s = 0; s < 6; s++) {
shadow *= exp(-cloudDensity(from + lightDir * d) * dt);
dt *= 1.3; d += dt;
}
return shadow;
}
// Method C: HG phase function mixed scattering
float HenyeyGreenstein(float cosTheta, float g) {
float gg = g * g;
return (1.0 - gg) / pow(1.0 + gg - 2.0 * g * cosTheta, 1.5);
}
float scattering = mix(
HenyeyGreenstein(dot(rd, -sundir), 0.8),
HenyeyGreenstein(dot(rd, -sundir), -0.2),
0.5
);
```
### Step 6: Color Mapping
```glsl
// Method A: Density-interpolated coloring (clouds)
vec3 cloudColor = mix(vec3(1.0, 0.95, 0.8), vec3(0.25, 0.3, 0.35), den);
// Method B: Radial gradient coloring (explosions, fire)
vec3 computeColor(float density, float radius) {
vec3 result = mix(vec3(1.0, 0.9, 0.8), vec3(0.4, 0.15, 0.1), density);
result *= mix(7.0 * vec3(0.8, 1.0, 1.0), 1.5 * vec3(0.48, 0.53, 0.5), min(radius / 0.9, 1.15));
return result;
}
// Method C: Height-based ambient light gradient
vec3 ambientLight = mix(
vec3(39., 67., 87.) * (1.5 / 255.),
vec3(149., 167., 200.) * (1.5 / 255.),
normalizedHeight
);
```
### Step 7: Final Compositing and Post-Processing
```glsl
// Sky background
vec3 bgCol = vec3(0.6, 0.71, 0.75) - rd.y * 0.2 * vec3(1.0, 0.5, 1.0);
float sun = clamp(dot(sundir, rd), 0.0, 1.0);
bgCol += 0.2 * vec3(1.0, 0.6, 0.1) * pow(sun, 8.0);
// Compositing
vec4 vol = raymarch(ro, rd, tmin, tmax, bgCol);
vec3 col = bgCol * (1.0 - vol.a) + vol.rgb;
col += vec3(0.2, 0.08, 0.04) * pow(sun, 3.0); // Sun glare
col = smoothstep(0.15, 1.1, col); // Tone mapping
```
## Complete Code Template
Runnable volumetric cloud renderer for ShaderToy (iChannel0 = Gray Noise Small 256x256):
```glsl
// Volumetric Cloud Renderer — ShaderToy Template
#define NUM_STEPS 80
#define SUN_DIR normalize(vec3(-0.7, 0.0, -0.7))
#define CLOUD_BOTTOM -1.0
#define CLOUD_TOP 2.0
#define WIND_SPEED 0.1
#define DENSITY_SCALE 1.75
#define DENSITY_THRESHOLD 0.01
float noise(vec3 x) {
vec3 p = floor(x);
vec3 f = fract(x);
f = f * f * (3.0 - 2.0 * f);
vec2 uv = (p.xy + vec2(37.0, 239.0) * p.z) + f.xy;
vec2 rg = textureLod(iChannel0, (uv + 0.5) / 256.0, 0.0).yx;
return mix(rg.x, rg.y, f.z) * 2.0 - 1.0;
}
float map(vec3 p, int lod) {
vec3 q = p - vec3(0.0, WIND_SPEED, 1.0) * iTime;
float f;
f = 0.50000 * noise(q); q *= 2.02;
if (lod >= 2)
f += 0.25000 * noise(q); q *= 2.03;
if (lod >= 3)
f += 0.12500 * noise(q); q *= 2.01;
if (lod >= 4)
f += 0.06250 * noise(q); q *= 2.02;
if (lod >= 5)
f += 0.03125 * noise(q);
return clamp(1.5 - p.y - 2.0 + DENSITY_SCALE * f, 0.0, 1.0);
}
vec3 lightSample(vec3 pos, float den, int lod) {
float dif = clamp((den - map(pos + 0.3 * SUN_DIR, lod)) / 0.6, 0.0, 1.0);
vec3 lin = vec3(1.0, 0.6, 0.3) * dif + vec3(0.91, 0.98, 1.05);
vec3 col = mix(vec3(1.0, 0.95, 0.8), vec3(0.25, 0.3, 0.35), den);
return col * lin;
}
vec4 raymarch(vec3 ro, vec3 rd, vec3 bgcol, ivec2 px) {
float tmin = (CLOUD_BOTTOM - ro.y) / rd.y;
float tmax = (CLOUD_TOP - ro.y) / rd.y;
if (tmin > tmax) { float tmp = tmin; tmin = tmax; tmax = tmp; }
if (tmax < 0.0) return vec4(0.0);
tmin = max(tmin, 0.0);
tmax = min(tmax, 60.0);
float t = tmin + 0.1 * fract(sin(float(px.x * 73 + px.y * 311)) * 43758.5453);
vec4 sum = vec4(0.0);
for (int i = 0; i < NUM_STEPS; i++) {
float dt = max(0.05, 0.02 * t);
int lod = 5 - int(log2(1.0 + t * 0.5));
vec3 pos = ro + t * rd;
float den = map(pos, lod);
if (den > DENSITY_THRESHOLD) {
vec3 litCol = lightSample(pos, den, lod);
litCol = mix(litCol, bgcol, 1.0 - exp(-0.003 * t * t));
vec4 col = vec4(litCol, den);
col.a *= 0.4;
col.rgb *= col.a;
sum += col * (1.0 - sum.a);
}
t += dt;
if (t > tmax || sum.a > 0.99) break;
}
return clamp(sum, 0.0, 1.0);
}
mat3 setCamera(vec3 ro, vec3 ta, float cr) {
vec3 cw = normalize(ta - ro);
vec3 cp = vec3(sin(cr), cos(cr), 0.0);
vec3 cu = normalize(cross(cw, cp));
vec3 cv = normalize(cross(cu, cw));
return mat3(cu, cv, cw);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
vec2 m = iMouse.xy / iResolution.xy;
vec3 ro = 4.0 * normalize(vec3(sin(3.0 * m.x), 0.8 * m.y, cos(3.0 * m.x)));
ro.y += 0.5;
vec3 ta = vec3(0.0, -1.0, 0.0);
mat3 ca = setCamera(ro, ta, 0.07 * cos(0.25 * iTime));
vec3 rd = ca * normalize(vec3(p, 1.5));
float sun = clamp(dot(SUN_DIR, rd), 0.0, 1.0);
vec3 bgcol = vec3(0.6, 0.71, 0.75) - rd.y * 0.2 * vec3(1.0, 0.5, 1.0) + 0.075;
bgcol += 0.2 * vec3(1.0, 0.6, 0.1) * pow(sun, 8.0);
vec4 res = raymarch(ro, rd, bgcol, ivec2(fragCoord - 0.5));
vec3 col = bgcol * (1.0 - res.a) + res.rgb;
col += vec3(0.2, 0.08, 0.04) * pow(sun, 3.0);
col = smoothstep(0.15, 1.1, col);
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Self-Emissive Volume (Fire/Explosions)
```glsl
vec3 emissionColor(float density, float radius) {
vec3 result = mix(vec3(1.0, 0.9, 0.8), vec3(0.4, 0.15, 0.1), density);
vec3 colCenter = 7.0 * vec3(0.8, 1.0, 1.0);
vec3 colEdge = 1.5 * vec3(0.48, 0.53, 0.5);
result *= mix(colCenter, colEdge, min(radius / 0.9, 1.15));
return result;
}
// Bloom effect
sum.rgb += lightColor / exp(lDist * lDist * lDist * 0.08) / 30.0;
```
### Variant 2: Physical Scattering Atmosphere (Rayleigh + Mie)
```glsl
float density(vec3 p, float scaleHeight) {
return exp(-max(length(p) - R_INNER, 0.0) / scaleHeight);
}
float opticDepth(vec3 from, vec3 to, float scaleHeight) {
vec3 s = (to - from) / float(NUM_STEPS_LIGHT);
vec3 v = from + s * 0.5;
float sum = 0.0;
for (int i = 0; i < NUM_STEPS_LIGHT; i++) { sum += density(v, scaleHeight); v += s; }
return sum * length(s);
}
float phaseRayleigh(float cc) { return (3.0 / 16.0 / PI) * (1.0 + cc); }
vec3 scatter = sumRay * kRay * phaseRayleigh(cc) + sumMie * kMie * phaseMie(-0.78, c, cc);
```
### Variant 3: Frostbite Energy-Conserving Integration
```glsl
vec3 S = evaluateLight(p) * sigmaS * phaseFunction() * volumetricShadow(p, lightPos);
vec3 Sint = (S - S * exp(-sigmaE * dt)) / sigmaE;
scatteredLight += transmittance * Sint;
transmittance *= exp(-sigmaE * dt);
```
### Variant 4: Production-Grade Clouds (Horizon Zero Dawn Style)
```glsl
float m = cloudMapBase(pos, norY);
m *= cloudGradient(norY);
m -= cloudMapDetail(pos) * dstrength * 0.225;
m = smoothstep(0.0, 0.1, m + (COVERAGE - 1.0));
float scattering = mix(HenyeyGreenstein(sundotrd, 0.8), HenyeyGreenstein(sundotrd, -0.2), 0.5);
// Temporal reprojection
vec2 spos = reprojectPos(ro + rd * dist, iResolution.xy, iChannel1);
col = mix(texture(iChannel1, spos, 0.0), col, 0.05);
```
### Variant 5: Gradient Normal Surface Lighting (Fur Ball / Volume Surface)
```glsl
vec3 furNormal(vec3 pos, float density) {
float eps = 0.01;
vec3 n;
n.x = sampleDensity(pos + vec3(eps, 0, 0)) - density;
n.y = sampleDensity(pos + vec3(0, eps, 0)) - density;
n.z = sampleDensity(pos + vec3(0, 0, eps)) - density;
return normalize(n);
}
vec3 N = -furNormal(pos, density);
float diff = max(0.0, dot(N, L) * 0.5 + 0.5); // Half-Lambert
float spec = pow(max(0.0, dot(N, H)), 50.0); // Blinn-Phong
```
## Performance & Composition
### Performance Tips
- **Early exit**: break out of loop when `sum.a > 0.99`
- **LOD noise**: `int lod = 5 - int(log2(1.0 + t * 0.5));` reduce fBM octaves at distance
- **Adaptive step size**: `float dt = max(0.05, 0.02 * t);` fine near, coarse far
- **Dithering**: add pixel-dependent random offset to start position, eliminates banding artifacts
- **Bounds clipping**: only march within the ray-volume intersection interval
- **Density threshold skip**: only compute lighting when `den > 0.01`
- **Minimal shadow steps**: 6-16 steps with increasing step size
- **Temporal reprojection**: blend history frames (e.g., 5% new frame + 95% history frame)
### Composition Tips
- **SDF terrain + volumetric clouds**: mutual depth occlusion (Himalayas style)
- **Volumetric fog + scene lighting**: `color = color * transmittance + scatteredLight`
- **Multi-layer volumes**: different density functions at different heights, march independently then composite
- **Post-process light shafts (God Rays)**: radial blur or screen-space ray marching
- **Procedural sky + volumetric clouds**: distance fogging for natural transitions
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/volumetric-rendering.md)

View File

@@ -0,0 +1,458 @@
- **IMPORTANT:** All declared `uniform` variables must be used in the shader code, otherwise the compiler will optimize them away. After optimization, `gl.getUniformLocation()` returns `null`, and setting that uniform triggers a WebGL `INVALID_OPERATION` error, which may cause rendering failure. Ensure uniforms like `iTime` are actually used in `main()` (e.g., `float t = iTime * 1.0;`)
# Voronoi & Cellular Noise
## Use Cases
- Natural textures: cells, cracked soil, stone, skin pores
- Structured patterns: crystals, honeycombs, shattered glass, mosaics
- Effects: fire/nebula (fBm stacking), crack generation
- Procedural materials: cloud noise, terrain height maps, stylized partitioning
## Core Principles
Voronoi noise = **spatial partitioning**: scatter feature points, assign each pixel to the "cell" of its nearest feature point.
Algorithm flow:
1. `floor` divides into an integer grid; each cell contains a randomly offset feature point
2. Search the 3x3 (2D) or 3x3x3 (3D) neighborhood for all feature points
3. Record the nearest distance F1 (optionally second-nearest F2)
4. Map F1, F2, or F2-F1 to color/height/shape
Distance metrics:
- Euclidean: `dot(r,r)` (squared, fast) -> final `sqrt`
- Manhattan: `abs(r.x)+abs(r.y)`
- Chebyshev: `max(abs(r.x), abs(r.y))`
Exact border distance (two-pass algorithm): `dot(0.5*(mr+r), normalize(r-mr))`
Rounded borders (harmonic mean): `1/(1/(d2-d1) + 1/(d3-d1))`
## Implementation Steps
### Step 1: Hash Functions
```glsl
// sin-dot hash (suitable for most cases)
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453);
}
// 3D version
vec3 hash3(vec3 p) {
float n = sin(dot(p, vec3(7.0, 157.0, 113.0)));
return fract(vec3(2097152.0, 262144.0, 32768.0) * n);
}
// High-quality integer hash (ES 3.0+, more uniform)
vec3 hash3_uint(vec3 p) {
uvec3 q = uvec3(ivec3(p)) * uvec3(1597334673U, 3812015801U, 2798796415U);
q = (q.x ^ q.y ^ q.z) * uvec3(1597334673U, 3812015801U, 2798796415U);
return vec3(q) / float(0xffffffffU);
}
```
### Step 2: Basic F1 Voronoi
```glsl
// Returns (F1 distance, cell ID)
vec2 voronoi(vec2 x) {
vec2 n = floor(x);
vec2 f = fract(x);
vec3 m = vec3(8.0);
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++) {
vec2 g = vec2(float(i), float(j));
vec2 o = hash2(n + g);
vec2 r = g - f + o;
float d = dot(r, r);
if (d < m.x) {
m = vec3(d, o);
}
}
return vec2(sqrt(m.x), m.y + m.z);
}
```
### Step 3: F1 + F2 (Edge Detection)
```glsl
// Returns vec2(F1, F2), edge value = F2 - F1
vec2 voronoi_f1f2(vec2 x) {
vec2 p = floor(x);
vec2 f = fract(x);
vec2 res = vec2(8.0);
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++) {
vec2 b = vec2(i, j);
vec2 r = b - f + hash2(p + b);
float d = dot(r, r);
if (d < res.x) {
res.y = res.x;
res.x = d;
} else if (d < res.y) {
res.y = d;
}
}
return sqrt(res);
}
```
### Step 4: Exact Border Distance (Two-Pass Algorithm)
```glsl
// Returns vec3(border distance, nearest point offset)
vec3 voronoi_border(vec2 x) {
vec2 ip = floor(x);
vec2 fp = fract(x);
// First pass: find nearest feature point
vec2 mg, mr;
float md = 8.0;
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++) {
vec2 g = vec2(float(i), float(j));
vec2 o = hash2(ip + g);
vec2 r = g + o - fp;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
// Second pass: exact border distance (5x5 range)
md = 8.0;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++) {
vec2 g = mg + vec2(float(i), float(j));
vec2 o = hash2(ip + g);
vec2 r = g + o - fp;
if (dot(mr - r, mr - r) > 0.00001)
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
return vec3(md, mr);
}
```
### Step 5: Feature Point Animation
```glsl
// Replace static hash inside the neighborhood search loop:
vec2 o = hash2(n + g);
o = 0.5 + 0.5 * sin(iTime + 6.2831 * o); // different phase per point
vec2 r = g - f + o;
```
### Step 6: Coloring & Visualization
```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Must use iTime, otherwise the compiler will optimize away the uniform
float time = iTime * 1.0;
vec2 p = fragCoord.xy / iResolution.xy;
vec2 uv = p * SCALE;
vec2 c = voronoi(uv);
float dist = c.x;
float id = c.y;
// Cell coloring (ID-driven palette)
vec3 col = 0.5 + 0.5 * cos(id * 6.2831 + vec3(0.0, 1.0, 2.0));
// Distance falloff
col *= clamp(1.0 - 0.4 * dist * dist, 0.0, 1.0);
// Border lines
col -= (1.0 - smoothstep(0.08, 0.09, dist));
fragColor = vec4(col, 1.0);
}
```
## Complete Code Template
```glsl
// === Voronoi Cellular Noise — Complete ShaderToy Template ===
// Supports F1/F2/F2-F1 modes, multiple distance metrics, animation, exact borders
#define SCALE 8.0 // Cell density
#define ANIMATE 1 // 0=static, 1=animated
#define MODE 0 // 0=F1 fill, 1=F2-F1 edges, 2=exact borders
#define DIST_METRIC 0 // 0=Euclidean, 1=Manhattan, 2=Chebyshev
vec2 hash2(vec2 p) {
p = vec2(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)));
return fract(sin(p) * 43758.5453);
}
float distFunc(vec2 r) {
#if DIST_METRIC == 0
return dot(r, r);
#elif DIST_METRIC == 1
return abs(r.x) + abs(r.y);
#elif DIST_METRIC == 2
return max(abs(r.x), abs(r.y));
#endif
}
vec2 getPoint(vec2 cellId) {
vec2 o = hash2(cellId);
#if ANIMATE
o = 0.5 + 0.5 * sin(iTime + 6.2831 * o);
#endif
return o;
}
vec4 voronoi(vec2 x) {
vec2 n = floor(x);
vec2 f = fract(x);
float d1 = 8.0, d2 = 8.0;
vec2 nearestCell = vec2(0.0);
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++) {
vec2 g = vec2(float(i), float(j));
vec2 o = getPoint(n + g);
vec2 r = g - f + o;
float d = distFunc(r);
if (d < d1) {
d2 = d1; d1 = d;
nearestCell = n + g;
} else if (d < d2) {
d2 = d;
}
}
#if DIST_METRIC == 0
d1 = sqrt(d1); d2 = sqrt(d2);
#endif
return vec4(d1, d2, nearestCell);
}
vec3 voronoiBorder(vec2 x) {
vec2 ip = floor(x);
vec2 fp = fract(x);
vec2 mg, mr;
float md = 8.0;
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++) {
vec2 g = vec2(float(i), float(j));
vec2 o = getPoint(ip + g);
vec2 r = g + o - fp;
float d = dot(r, r);
if (d < md) { md = d; mr = r; mg = g; }
}
md = 8.0;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++) {
vec2 g = mg + vec2(float(i), float(j));
vec2 o = getPoint(ip + g);
vec2 r = g + o - fp;
if (dot(mr - r, mr - r) > 0.00001)
md = min(md, dot(0.5 * (mr + r), normalize(r - mr)));
}
return vec3(md, mr);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// Must use iTime, otherwise the compiler will optimize away the uniform (especially important when ANIMATE=1)
float time = iTime * 1.0;
vec2 p = fragCoord.xy / iResolution.xy;
p.x *= iResolution.x / iResolution.y;
vec2 uv = p * SCALE;
vec3 col = vec3(0.0);
#if MODE == 0
vec4 v = voronoi(uv);
float id = dot(v.zw, vec2(127.1, 311.7));
col = 0.5 + 0.5 * cos(id * 6.2831 + vec3(0.0, 1.0, 2.0));
col *= clamp(1.0 - 0.4 * v.x * v.x, 0.0, 1.0);
col -= (1.0 - smoothstep(0.08, 0.09, v.x));
#elif MODE == 1
vec4 v = voronoi(uv);
float edge = v.y - v.x;
col = vec3(1.0 - smoothstep(0.0, 0.15, edge));
col *= vec3(0.2, 0.6, 1.0);
#elif MODE == 2
vec3 c = voronoiBorder(uv);
col = c.x * (0.5 + 0.5 * sin(64.0 * c.x)) * vec3(1.0);
col = mix(vec3(1.0, 0.6, 0.0), col, smoothstep(0.04, 0.07, c.x));
float dd = length(c.yz);
col = mix(vec3(1.0, 0.6, 0.1), col, smoothstep(0.0, 0.12, dd));
#endif
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: 3D Voronoi + fBm Fire
```glsl
#define NUM_OCTAVES 5
vec3 hash3(vec3 p) {
float n = sin(dot(p, vec3(7.0, 157.0, 113.0)));
return fract(vec3(2097152.0, 262144.0, 32768.0) * n);
}
float voronoi3D(vec3 p) {
vec3 g = floor(p); p = fract(p);
float d = 1.0;
for (int j = -1; j <= 1; j++)
for (int i = -1; i <= 1; i++)
for (int k = -1; k <= 1; k++) {
vec3 b = vec3(i, j, k);
vec3 r = b - p + hash3(g + b);
d = min(d, dot(r, r));
}
return d;
}
float fbmVoronoi(vec3 p) {
vec3 t = vec3(0.0, 0.0, p.z + iTime * 1.5);
float tot = 0.0, sum = 0.0, amp = 1.0;
for (int i = 0; i < NUM_OCTAVES; i++) {
tot += voronoi3D(p + t) * amp;
p *= 2.0; t *= 1.5;
sum += amp; amp *= 0.5;
}
return tot / sum;
}
// Blackbody radiation palette
vec3 firePalette(float i) {
float T = 1400.0 + 1300.0 * i;
vec3 L = vec3(7.4, 5.6, 4.4);
L = pow(L, vec3(5.0)) * (exp(1.43876719683e5 / (T * L)) - 1.0);
return 1.0 - exp(-5e8 / L);
}
```
### Variant 2: Rounded Borders (3rd-Order Voronoi)
```glsl
float voronoiRounded(vec2 p) {
vec2 g = floor(p); p -= g;
vec3 d = vec3(1.0); // F1, F2, F3
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
vec2 o = vec2(x, y);
o += hash2(g + o) - p;
float r = dot(o, o);
d.z = max(d.x, max(d.y, min(d.z, r)));
d.y = max(d.x, min(d.y, r));
d.x = min(d.x, r);
}
d = sqrt(d);
return min(2.0 / (1.0 / max(d.y - d.x, 0.001)
+ 1.0 / max(d.z - d.x, 0.001)), 1.0);
}
```
### Variant 3: Voronoise (Unified Noise-Voronoi Framework)
```glsl
#define JITTER 1.0 // 0=regular grid, 1=fully random
#define SMOOTH 0.0 // 0=sharp Voronoi, 1=smooth noise
vec3 hash3(vec2 p) {
vec3 q = vec3(dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3)),
dot(p, vec2(419.2, 371.9)));
return fract(sin(q) * 43758.5453);
}
float voronoise(vec2 p, float u, float v) {
float k = 1.0 + 63.0 * pow(1.0 - v, 6.0);
vec2 i = floor(p); vec2 f = fract(p);
vec2 a = vec2(0.0);
for (int y = -2; y <= 2; y++)
for (int x = -2; x <= 2; x++) {
vec2 g = vec2(x, y);
vec3 o = hash3(i + g) * vec3(u, u, 1.0);
vec2 d = g - f + o.xy;
float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k);
a += vec2(o.z * w, w);
}
return a.x / a.y;
}
```
### Variant 4: Crack Texture (Multi-Layer Recursive Voronoi)
```glsl
#define CRACK_DEPTH 3.0
#define CRACK_WIDTH 0.0
#define CRACK_SLOPE 50.0
float ofs = 0.5;
#define disp(p) (-ofs + (1.0 + 2.0 * ofs) * hash2(p))
// Main loop
vec4 O = vec4(0.0);
vec2 U = uv;
for (float i = 0.0; i < CRACK_DEPTH; i++) {
vec2 D = fbm22(U) * 0.67;
vec3 H = voronoiBorder(U + D);
float d = H.x;
d = min(1.0, CRACK_SLOPE * pow(max(0.0, d - CRACK_WIDTH), 1.0));
O += vec4(1.0 - d) / exp2(i);
U *= 1.5 * rot(0.37);
}
```
### Variant 5: Tileable 3D Worley (Cloud Noise)
```glsl
#define TILE_FREQ 4.0
float worleyTileable(vec3 uv, float freq) {
vec3 id = floor(uv); vec3 p = fract(uv);
float minDist = 1e4;
for (float x = -1.0; x <= 1.0; x++)
for (float y = -1.0; y <= 1.0; y++)
for (float z = -1.0; z <= 1.0; z++) {
vec3 offset = vec3(x, y, z);
vec3 h = hash3_uint(mod(id + offset, vec3(freq))) * 0.5 + 0.5;
h += offset;
vec3 d = p - h;
minDist = min(minDist, dot(d, d));
}
return 1.0 - minDist;
}
float worleyFbm(vec3 p, float freq) {
return worleyTileable(p * freq, freq) * 0.625
+ worleyTileable(p * freq * 2.0, freq * 2.0) * 0.25
+ worleyTileable(p * freq * 4.0, freq * 4.0) * 0.125;
}
float remap(float x, float a, float b, float c, float d) {
return (((x - a) / (b - a)) * (d - c)) + c;
}
// cloud = remap(perlinNoise, worleyFbm - 1.0, 1.0, 0.0, 1.0);
```
## Performance & Composition
**Performance:**
- Use `dot(r,r)` instead of `length` during comparison; only `sqrt` for final output
- 3D loops can be manually unrolled along the z-axis to reduce nesting
- Search range: basic F1 uses 3x3; exact borders/Voronoise/extended jitter uses 5x5
- Hash choice: `sin(dot(...))` is fastest; integer hash is more uniform but requires ES 3.0+
- fBm layers: 3 is sufficient, 5 is the upper limit
**Combinations:**
- **+fBm distortion**: `uv + 0.5*fbm22(uv*2.0)` -> organic cell shapes
- **+Bump Mapping**: finite-difference normal computation -> pseudo-3D bumps
- **+Palette**: `0.5+0.5*cos(6.2831*(t+vec3(0,0.33,0.67)))` -> rich colors
- **+Raymarching**: Voronoi distance as part of the SDF -> cellular surfaces
- **+Multi-scale stacking**: Voronoi at different frequencies stacked -> primary structure + fine detail
## Further Reading
For complete step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/voronoi-cellular-noise.md)

View File

@@ -0,0 +1,985 @@
## WebGL2 Adaptation Requirements
The code templates in this document use ShaderToy GLSL style. When generating standalone HTML pages, you must adapt for WebGL2:
- Use `canvas.getContext("webgl2")` **(required! WebGL1 does not support in/out keywords)**
- Shader first line: `#version 300 es`, add `precision highp float;` to fragment shader
- **IMPORTANT: #version must be the very first line of the shader! No characters before it (including blank lines/comments/Unicode BOM)**
- Vertex shader: `attribute``in`, `varying``out`
- Fragment shader: `varying``in`, `gl_FragColor` → custom `out vec4 fragColor`, `texture2D()``texture()`
- ShaderToy's `void mainImage(out vec4 fragColor, in vec2 fragCoord)` needs to be adapted to the standard `void main()` entry point
### WebGL2 Full Adaptation Example
```glsl
// === Vertex Shader ===
const vertexShaderSource = `#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}`;
// === Fragment Shader ===
const fragmentShaderSource = `#version 300 es
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
// IMPORTANT: Important: WebGL2 must declare the output variable!
out vec4 fragColor;
// ... other functions ...
void main() {
// IMPORTANT: Use gl_FragCoord.xy instead of fragCoord
vec2 fragCoord = gl_FragCoord.xy;
vec3 col = vec3(0.0);
// ... rendering logic ...
// IMPORTANT: Write to fragColor, not gl_FragColor!
fragColor = vec4(col, 1.0);
}`;
```
**IMPORTANT: Common GLSL compile errors:**
- `in/out storage qualifier supported in GLSL ES 3.00 only` → Check that you are using `getContext("webgl2")` and `#version 300 es`
- `#version directive must occur on the first line` → Check that the shader string starts with #version, with no characters before it
- **IMPORTANT: GLSL reserved words**: `cast`, `class`, `template`, `namespace`, `union`, `enum`, `typedef`, `sizeof`, `input`, `output`, `filter`, `image`, `sampler`, `fixed`, `volatile`, `public`, `static`, `extern`, `external`, `interface`, `long`, `short`, `double`, `half`, `unsigned`, `superp`, `inline`, `noinline`, etc. are all GLSL reserved words and **must never be used as variable or function names**! Common pitfall: naming a function `cast` for ray casting → compile failure. **Use compound names like `castRay`, `castShadow`, `shootRay` instead**.
- **IMPORTANT: GLSL strict typing**: float/int cannot be mixed. `if (x > 0)` for int, `if (y < 0.0)` for float. Comparing ivec3 members to float requires explicit conversion: `float(c.y) < height`. When getVoxel returns int, compare with `> 0` not `> 0.0`. Function parameter types must match exactly.
- **IMPORTANT: Vector dimension mismatch (vec2 vs vec3)**: `p.xz` returns `vec2` and **must never** be added to `vec3` or passed to functions expecting `vec3` parameters (e.g., `fbm(vec3)`, `noise(vec3)`)! Common error: `fbm(p.xz * 0.08 + vec3(...))``vec2 + vec3` compile failure. **Fix**: either use a `vec2` version of noise/fbm, or construct a full vec3: `fbm(vec3(p.xz * 0.08, p.y * 0.05))`. Similarly, `vec2` only has `.x`/`.y`, cannot access `.z`/`.w`.
- **IMPORTANT: length() / floating-point precision**: `length(ivec2)` must first convert to `vec2`: `length(vec2(d))`. Exact floating-point equality comparison almost never works; use range comparison: `floor(p.y) == floor(height)`
# Voxel Rendering Skill
## Use Cases
- Rendering discrete volumetric data on regular 3D grids (Minecraft-style worlds, medical volume data, architectural voxel models)
- Pixel-accurate block/cube scenes
- "Block art", "3D pixel art", "low-poly voxel" visual styles
- Real-time voxel scenes in pure fragment shader environments like ShaderToy
- Advanced lighting effects including shadows, AO, and global illumination
## Core Principles
The core of voxel rendering is the **DDA (Digital Differential Analyzer) ray traversal algorithm**: cast a ray from the camera through each pixel, stepping through the 3D grid cell by cell along the ray direction until hitting an occupied voxel.
For ray `P(t) = rayPos + t * rayDir`, DDA maintains:
- **`mapPos`** = `floor(rayPos)`: current grid coordinate (integer)
- **`deltaDist`** = `abs(1.0 / rayDir)`: t cost to cross one cell
- **`sideDist`** = `(sign(rayDir) * (mapPos - rayPos) + sign(rayDir) * 0.5 + 0.5) * deltaDist`: t distance to the next boundary on each axis
Each step advances along the axis with the smallest `sideDist`, updating `sideDist += deltaDist` and `mapPos += rayStep`.
Normal on hit: `normal = -mask * rayStep`
Face UV is obtained by projecting the hit point onto the two tangent axes of the hit face.
## Implementation Steps
### Step 1: Camera Ray Construction
```glsl
vec2 screenPos = (fragCoord.xy / iResolution.xy) * 2.0 - 1.0;
vec3 cameraDir = vec3(0.0, 0.0, 0.8); // Focal length; larger = narrower FOV
vec3 cameraPlaneU = vec3(1.0, 0.0, 0.0);
vec3 cameraPlaneV = vec3(0.0, 1.0, 0.0) * iResolution.y / iResolution.x;
vec3 rayDir = cameraDir + screenPos.x * cameraPlaneU + screenPos.y * cameraPlaneV;
vec3 rayPos = vec3(0.0, 2.0, -12.0);
```
### Step 2: DDA Initialization
```glsl
ivec3 mapPos = ivec3(floor(rayPos));
vec3 rayStep = sign(rayDir);
vec3 deltaDist = abs(1.0 / rayDir); // When ray is normalized, equivalent to abs(1.0/rd), no length() needed
vec3 sideDist = (sign(rayDir) * (vec3(mapPos) - rayPos) + (sign(rayDir) * 0.5) + 0.5) * deltaDist;
```
### Step 3: DDA Traversal Loop (Branchless Version)
```glsl
#define MAX_RAY_STEPS 64
bvec3 mask;
for (int i = 0; i < MAX_RAY_STEPS; i++) {
if (getVoxel(mapPos)) break;
// Branchless axis selection
mask = lessThanEqual(sideDist.xyz, min(sideDist.yzx, sideDist.zxy));
sideDist += vec3(mask) * deltaDist;
mapPos += ivec3(vec3(mask)) * ivec3(rayStep);
}
```
Alternative form (step version):
```glsl
vec3 mask = step(sideDist.xyz, sideDist.yzx) * step(sideDist.xyz, sideDist.zxy);
sideDist += mask * deltaDist;
mapPos += mask * rayStep;
```
### Step 4: Voxel Occupancy Function
```glsl
// Basic version: solid block (most common; use this when user asks for "voxel cube")
// IMPORTANT: Important: getVoxel receives ivec3, but all internal calculations must use float!
bool getVoxel(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5); // ivec3 → vec3 conversion (required!)
float d = sdBox(p, vec3(6.0)); // Solid 12x12x12 cube
return d < 0.0;
}
// Advanced version: SDF boolean operations (sphere carved from box = only corners remain)
bool getVoxelCarved(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5);
float d = max(-sdSphere(p, 7.5), sdBox(p, vec3(6.0))); // box ∩ ¬sphere
return d < 0.0;
}
// Advanced version: height map terrain with material IDs
// IMPORTANT: Key: all comparisons must use float! c.y is int and must be converted to float for comparison
// IMPORTANT: Important: must use range comparison, not exact equality (floating-point precision issues)
int getVoxelMaterial(ivec3 c) {
vec3 p = vec3(c); // ivec3 → vec3 conversion (required!)
float groundHeight = getTerrainHeight(p.xz); // p.xz is vec2, passes float parameters
if (float(c.y) < groundHeight) return 1; // int → float comparison
if (float(c.y) < groundHeight + 4.0) return 7; // int → float comparison
return 0;
}
// Pure float version (simpler, recommended):
int getVoxelMaterial(vec3 c) {
float groundHeight = getTerrainHeight(c.xz);
// IMPORTANT: Use range comparison, never exact equality!
if (c.y >= groundHeight && c.y < groundHeight + 1.0) return 1; // Grass top layer
if (c.y >= groundHeight - 3.0 && c.y < groundHeight) return 2; // Dirt layer
if (c.y < groundHeight - 3.0) return 3; // Stone layer
return 0;
}
// Advanced version: mountain terrain (height-based coloring: grass green → rock gray → snow white)
// IMPORTANT: Key 1: color thresholds must be based on heightRatio (normalized height 0~1), not absolute height!
// IMPORTANT: Key 2: maxH must match the actual maximum return value of getMountainHeight!
// If getMountainHeight returns at most 15.0, maxH must be 15.0, not arbitrarily 20.0
// IMPORTANT: Key 3: threshold spacing must be large enough (at least 0.2), otherwise color bands are too narrow to see
// IMPORTANT: Key 4: grass area typically covers the largest terrain area (low elevation); set grass threshold high (0.4) to ensure green is clearly visible
float maxH = 15.0; // IMPORTANT: Must equal the actual max value of getMountainHeight!
int getMountainVoxel(vec3 c) {
float height = getMountainHeight(c.xz); // Returns 0 ~ maxH
if (c.y > height) return 0; // Air
float heightRatio = c.y / maxH; // Normalize to 0~1
// IMPORTANT: Thresholds from low to high: grass < 0.4, rock 0.4~0.7, snow > 0.7
if (heightRatio < 0.4) return 1; // Grass (green) — largest area
if (heightRatio < 0.7) return 2; // Rock (gray)
return 3; // Snow cap (white)
}
// IMPORTANT: Corresponding material colors must have sufficient saturation and clear contrast:
// mat==1: vec3(0.25, 0.55, 0.15) Grass green (saturated green, must not be grayish!)
// mat==2: vec3(0.5, 0.45, 0.4) Rock gray-brown
// mat==3: vec3(0.92, 0.93, 0.96) Snow white
// IMPORTANT: Lighting must not be too bright or it washes out colors! Sun intensity ≤ 2.0, sky light ≤ 1.0
// IMPORTANT: Gamma correction pow(col, vec3(0.4545)) brightens dark colors and reduces saturation;
// if colors look grayish-white, make grass green more saturated: vec3(0.2, 0.5, 0.1)
// IMPORTANT: Rotating objects: to rotate a voxel object, apply inverse rotation to the sample point in getVoxel!
// Do not rotate the camera to simulate object rotation (that only changes the viewpoint)
bool getVoxelRotating(ivec3 c) {
vec3 p = vec3(c) + vec3(0.5);
// Rotate around Y axis: apply inverse rotation to sample point
float angle = -iTime; // Negative sign = inverse transform
float s = sin(angle), co = cos(angle);
p.xz = vec2(p.x * co - p.z * s, p.x * s + p.z * co);
float d = sdBox(p, vec3(6.0)); // Rotated solid cube
return d < 0.0;
}
```
### Step 5: Face Shading (Normal + Base Color)
```glsl
vec3 normal = -vec3(mask) * rayStep;
vec3 color;
if (mask.x) color = vec3(0.5); // Side faces darkest
if (mask.y) color = vec3(1.0); // Top face brightest
if (mask.z) color = vec3(0.75); // Front/back faces medium
fragColor = vec4(color, 1.0);
```
### Step 6: Precise Hit Position and Face UV
```glsl
float t = dot(sideDist - deltaDist, vec3(mask));
vec3 hitPos = rayPos + rayDir * t;
vec3 uvw = hitPos - vec3(mapPos);
vec2 uv = vec2(dot(vec3(mask) * uvw.yzx, vec3(1.0)),
dot(vec3(mask) * uvw.zxy, vec3(1.0)));
```
### Step 7: Neighbor Voxel AO
```glsl
float vertexAo(vec2 side, float corner) {
return (side.x + side.y + max(corner, side.x * side.y)) / 3.0;
}
vec4 voxelAo(vec3 pos, vec3 d1, vec3 d2) {
vec4 side = vec4(
getVoxel(pos + d1), getVoxel(pos + d2),
getVoxel(pos - d1), getVoxel(pos - d2));
vec4 corner = vec4(
getVoxel(pos + d1 + d2), getVoxel(pos - d1 + d2),
getVoxel(pos - d1 - d2), getVoxel(pos + d1 - d2));
vec4 ao;
ao.x = vertexAo(side.xy, corner.x);
ao.y = vertexAo(side.yz, corner.y);
ao.z = vertexAo(side.zw, corner.z);
ao.w = vertexAo(side.wx, corner.w);
return 1.0 - ao;
}
// Bilinear interpolation
vec4 ambient = voxelAo(mapPos - rayStep * mask, mask.zxy, mask.yzx);
float ao = mix(mix(ambient.z, ambient.w, uv.x), mix(ambient.y, ambient.x, uv.x), uv.y);
ao = pow(ao, 1.0 / 3.0); // Gamma correction to control AO intensity
```
### Step 8: DDA Shadow Ray
```glsl
// IMPORTANT: Shadow steps must be capped at 16; total main ray + shadow ray steps should not exceed 80
#define MAX_SHADOW_STEPS 16
float castShadow(vec3 ro, vec3 rd) {
vec3 pos = floor(ro);
vec3 ri = 1.0 / rd;
vec3 rs = sign(rd);
vec3 dis = (pos - ro + 0.5 + rs * 0.5) * ri;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
if (getVoxel(ivec3(pos))) return 0.0;
vec3 mm = step(dis.xyz, dis.yzx) * step(dis.xyz, dis.zxy);
dis += mm * rs * ri;
pos += mm * rs;
}
return 1.0;
}
vec3 sundir = normalize(vec3(-0.5, 0.6, 0.7));
float shadow = castShadow(hitPos + normal * 0.01, sundir);
float diffuse = max(dot(normal, sundir), 0.0) * shadow;
```
## Complete Code Template
```glsl
// === Voxel Rendering - Complete ShaderToy Template ===
// Includes: DDA traversal, face shading, neighbor AO, hard shadows
// IMPORTANT: Performance critical: SwiftShader software renderer (headless browser evaluation environment) cannot handle too many loop iterations
// Default 64+16=80 steps, suitable for most scenes. Simple scenes (single cube) can increase to 96+24
// Multi-building/character/Minecraft scenes must keep 64+16 or lower!
#define MAX_RAY_STEPS 64
#define MAX_SHADOW_STEPS 16
#define GRID_SIZE 16.0
// ---- Math Utilities ----
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 d = abs(p) - b;
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
float hash31(vec3 n) { return fract(sin(dot(n, vec3(1.0, 113.0, 257.0))) * 43758.5453); }
vec2 rotate2d(vec2 v, float a) {
float s = sin(a), c = cos(a);
return vec2(v.x * c - v.y * s, v.y * c + v.x * s);
}
// ---- Voxel Scene Definition ----
// IMPORTANT: Default solid cube. Use sdBox for "voxel cube"; add SDF boolean ops for carved/sculpted shapes
int getVoxel(vec3 c) {
vec3 p = c + 0.5;
float d = sdBox(p, vec3(6.0)); // Solid 12x12x12 block
if (d < 0.0) {
if (p.y < -3.0) return 2;
return 1;
}
return 0;
}
// ---- Neighbor AO ----
float getOccupancy(vec3 c) { return float(getVoxel(c) > 0); }
float vertexAo(vec2 side, float corner) {
return (side.x + side.y + max(corner, side.x * side.y)) / 3.0;
}
vec4 voxelAo(vec3 pos, vec3 d1, vec3 d2) {
vec4 side = vec4(
getOccupancy(pos + d1), getOccupancy(pos + d2),
getOccupancy(pos - d1), getOccupancy(pos - d2));
vec4 corner = vec4(
getOccupancy(pos + d1 + d2), getOccupancy(pos - d1 + d2),
getOccupancy(pos - d1 - d2), getOccupancy(pos + d1 - d2));
vec4 ao;
ao.x = vertexAo(side.xy, corner.x);
ao.y = vertexAo(side.yz, corner.y);
ao.z = vertexAo(side.zw, corner.z);
ao.w = vertexAo(side.wx, corner.w);
return 1.0 - ao;
}
// ---- DDA Traversal Core ----
struct HitInfo {
bool hit;
float t;
vec3 pos;
vec3 normal;
vec3 mapPos;
vec2 uv;
int mat;
};
HitInfo castRay(vec3 ro, vec3 rd, int maxSteps) {
HitInfo info;
info.hit = false;
info.t = 0.0;
vec3 mapPos = floor(ro);
vec3 rayStep = sign(rd);
vec3 deltaDist = abs(1.0 / rd);
vec3 sideDist = (rayStep * (mapPos - ro) + rayStep * 0.5 + 0.5) * deltaDist;
vec3 mask = vec3(0.0);
for (int i = 0; i < maxSteps; i++) {
int vox = getVoxel(mapPos);
if (vox > 0) {
info.hit = true;
info.mat = vox;
info.normal = -mask * rayStep;
info.mapPos = mapPos;
info.t = dot(sideDist - deltaDist, mask);
info.pos = ro + rd * info.t;
vec3 uvw = info.pos - mapPos;
info.uv = vec2(dot(mask * uvw.yzx, vec3(1.0)),
dot(mask * uvw.zxy, vec3(1.0)));
return info;
}
mask = step(sideDist.xyz, sideDist.yzx) * step(sideDist.xyz, sideDist.zxy);
sideDist += mask * deltaDist;
mapPos += mask * rayStep;
}
return info;
}
// ---- Shadow Ray ----
// IMPORTANT: Shadow steps at 16 (combined with main ray 64 = 80, within SwiftShader safe range)
float castShadow(vec3 ro, vec3 rd) {
vec3 pos = floor(ro);
vec3 ri = 1.0 / rd;
vec3 rs = sign(rd);
vec3 dis = (pos - ro + 0.5 + rs * 0.5) * ri;
for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
// IMPORTANT: getVoxel returns int; comparison must use int constant (0), not float (0.0)
if (getVoxel(pos) > 0) return 0.0;
vec3 mm = step(dis.xyz, dis.yzx) * step(dis.xyz, dis.zxy);
dis += mm * rs * ri;
pos += mm * rs;
}
return 1.0;
}
// ---- Material Colors ----
// IMPORTANT: Texture coloring key: "low saturation" does not mean "near white/gray"!
// Low saturation = colorful but not vivid, must retain clear hue differences (e.g., brick red 0.55,0.35,0.3 not gray-white 0.8,0.8,0.8)
// Brick/stone textures: use UV periodic patterns (mortar lines = dark lines), never use solid colors!
vec3 getMaterialColor(int mat, vec2 uv) {
vec3 col = vec3(0.6);
if (mat == 1) col = vec3(0.7, 0.7, 0.75);
if (mat == 2) col = vec3(0.4, 0.55, 0.3);
float checker = mod(floor(uv.x * 4.0) + floor(uv.y * 4.0), 2.0);
col *= 0.85 + 0.15 * checker;
return col;
}
// ---- Brick/Stone Texture Coloring (use this to replace getMaterialColor when user requests "brick texture") ----
// IMPORTANT: Key: brick texture = UV periodic pattern (staggered rows + mortar dark lines), not solid color!
vec3 getBrickColor(vec2 uv, vec3 baseColor, vec3 mortarColor) {
vec2 brickUV = uv * vec2(4.0, 8.0);
float row = floor(brickUV.y);
brickUV.x += mod(row, 2.0) * 0.5; // Staggered row offset
vec2 f = fract(brickUV);
float mortar = step(f.x, 0.06) + step(f.y, 0.08); // Mortar joints
mortar = clamp(mortar, 0.0, 1.0);
float noise = fract(sin(dot(floor(brickUV), vec2(12.9898, 78.233))) * 43758.5453);
vec3 brickVariation = baseColor * (0.85 + 0.3 * noise); // Slight color variation per brick
return mix(brickVariation, mortarColor, mortar);
}
// Usage example (maze walls):
// if (mat == 1) col = getBrickColor(uv, vec3(0.55, 0.35, 0.3), vec3(0.4, 0.38, 0.35)); // Brick red + mortar
// if (mat == 2) col = getBrickColor(uv, vec3(0.5, 0.48, 0.42), vec3(0.35, 0.33, 0.3)); // Gray stone brick
// ---- Main Function ----
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 screenPos = (fragCoord.xy / iResolution.xy) * 2.0 - 1.0;
screenPos.x *= iResolution.x / iResolution.y;
vec3 ro = vec3(0.0, 2.0 * sin(iTime * 0.5), -12.0);
vec3 forward = vec3(0.0, 0.0, 0.8);
vec3 rd = normalize(forward + vec3(screenPos, 0.0));
ro.xz = rotate2d(ro.xz, iTime * 0.3);
rd.xz = rotate2d(rd.xz, iTime * 0.3);
vec3 sunDir = normalize(vec3(-0.5, 0.6, 0.7));
vec3 skyColor = vec3(0.6, 0.75, 0.9);
HitInfo hit = castRay(ro, rd, MAX_RAY_STEPS);
vec3 col;
if (hit.hit) {
vec3 matCol = getMaterialColor(hit.mat, hit.uv);
vec3 mask = abs(hit.normal);
vec4 ambient = voxelAo(hit.mapPos, mask.zxy, mask.yzx);
float ao = mix(
mix(ambient.z, ambient.w, hit.uv.x),
mix(ambient.y, ambient.x, hit.uv.x),
hit.uv.y);
ao = pow(ao, 0.5);
float shadow = castShadow(hit.pos + hit.normal * 0.01, sunDir);
float diff = max(dot(hit.normal, sunDir), 0.0);
float sky = 0.5 + 0.5 * hit.normal.y;
vec3 lighting = vec3(0.0);
// IMPORTANT: Mountain/terrain scenes: sun light ≤ 2.0, sky light ≤ 1.0; too bright washes out material color differences
lighting += 2.0 * diff * vec3(1.0, 0.95, 0.8) * shadow;
lighting += 1.0 * sky * skyColor;
lighting *= ao;
col = matCol * lighting;
// IMPORTANT: Fog: coefficient should not be too large, otherwise nearby objects get swallowed into pure sky color
// 0.0002 suits GRID_SIZE=16 scenes; use smaller coefficients for larger scenes
float fog = 1.0 - exp(-0.0002 * hit.t * hit.t);
col = mix(col, skyColor, clamp(fog, 0.0, 0.7)); // Clamp prevents objects from disappearing entirely
} else {
col = skyColor - rd.y * 0.2;
}
col = pow(clamp(col, 0.0, 1.0), vec3(0.4545));
fragColor = vec4(col, 1.0);
}
```
## Common Variants
### Variant 1: Glowing Voxels (Glow Accumulation)
Accumulate distance-based glow values during DDA traversal; produces semi-transparent glow even on miss.
```glsl
float glow = 0.0;
for (int i = 0; i < MAX_RAY_STEPS; i++) {
float d = sdSomeShape(vec3(mapPos));
glow += 0.015 / (0.01 + d * d);
if (d < 0.0) break;
// ... normal DDA stepping ...
}
vec3 col = baseColor + glow * vec3(0.4, 0.6, 1.0);
```
### Variant 2: Rounded Voxels (Intra-voxel SDF Refinement)
After DDA hit, perform SDF ray march inside the voxel to render rounded blocks.
```glsl
float id = hash31(mapPos);
float w = 0.05 + 0.35 * id;
float sdRoundedBox(vec3 p, float w) {
return length(max(abs(p) - 0.5 + w, 0.0)) - w;
}
vec3 localP = hitPos - mapPos - 0.5;
for (int j = 0; j < 6; j++) {
float h = sdRoundedBox(localP, w);
if (h < 0.025) break;
localP += rd * max(0.0, h);
}
```
### Variant 3: Hybrid SDF-Voxel Traversal
SDF sphere-tracing with large steps at distance, switching to precise DDA near the surface.
```glsl
#define VOXEL_SIZE 0.0625
#define SWITCH_DIST (VOXEL_SIZE * 1.732)
bool useVoxel = false;
for (int i = 0; i < MAX_STEPS; i++) {
vec3 pos = ro + rd * t;
float d = mapSDF(useVoxel ? voxelCenter : pos);
if (!useVoxel) {
t += d;
if (d < SWITCH_DIST) { useVoxel = true; voxelPos = getVoxelPos(pos); }
} else {
if (d < 0.0) break;
if (d > SWITCH_DIST) { useVoxel = false; t += d; continue; }
vec3 exitT = (voxelPos - ro * ird + ird * VOXEL_SIZE * 0.5);
// ... select minimum axis to advance ...
}
}
```
### Variant 4: Voxel Cone Tracing
Build multi-level mipmaps, cast cone-shaped rays from hit points for global illumination.
```glsl
vec4 traceCone(vec3 origin, vec3 dir, float coneRatio) {
vec4 light = vec4(0.0);
float t = 1.0;
for (int i = 0; i < 58; i++) {
vec3 sp = origin + dir * t;
float diameter = max(1.0, t * coneRatio);
float lod = log2(diameter);
vec4 sample = voxelFetch(sp, lod);
light += sample * (1.0 - light.w);
t += diameter;
}
return light;
}
```
### Variant 5: PBR Lighting + Multi-Bounce Reflection
GGX BRDF replacing Lambert, with metallic/roughness parameters; cast a second DDA ray for reflections.
```glsl
float ggxDiffuse(float NoL, float NoV, float LoH, float roughness) {
float FD90 = 0.5 + 2.0 * roughness * LoH * LoH;
float a = 1.0 + (FD90 - 1.0) * pow(1.0 - NoL, 5.0);
float b = 1.0 + (FD90 - 1.0) * pow(1.0 - NoV, 5.0);
return a * b / 3.14159;
}
vec3 rd2 = reflect(rd, normal);
HitInfo reflHit = castRay(hitPos + normal * 0.001, rd2, 64);
vec3 reflColor = reflHit.hit ? shade(reflHit) : skyColor;
float fresnel = 0.04 + 0.96 * pow(1.0 - max(dot(normal, -rd), 0.0), 5.0);
col += fresnel * reflColor;
```
### Variant 6: Voxel Water Scene (Water + Underwater Voxels)
Water surface ripple reflections, underwater refraction, sand and seaweed for a complete water scene.
```glsl
float waterY = 0.0;
// Underwater voxel scene definition (sand + seaweed)
// IMPORTANT: All coordinate operations must use correct vector dimensions!
// c.xz returns vec2, only has .x/.y components, cannot use .z!
int getVoxel(vec3 c) {
float sandHeight = -3.0 + 0.5 * sin(c.x * 0.3) * cos(c.z * 0.4);
if (c.y < sandHeight) return 1; // Sand interior
if (c.y < sandHeight + 1.0) return 2; // Sand surface
// Seaweed: only grows underwater, above sand
float grassHash = fract(sin(dot(floor(c.xz), vec2(12.9898, 78.233))) * 43758.5453);
// IMPORTANT: floor(c.xz) is vec2; the second argument to dot() must also be vec2
if (grassHash > 0.85 && c.y >= sandHeight + 1.0 && c.y < sandHeight + 1.0 + 3.0 * grassHash) {
return 3; // Seaweed
}
return 0;
}
// Handle water surface in main rendering
float tWater = (waterY - ro.y) / rd.y;
bool hitWater = tWater > 0.0 && (tWater < hit.t || !hit.hit);
if (hitWater) {
vec3 waterPos = ro + rd * tWater;
vec3 waterNormal = vec3(0.0, 1.0, 0.0);
// IMPORTANT: waterPos.xz is vec2; access with .x/.y (not .x/.z)
vec2 waveXZ = waterPos.xz; // vec2: waveXZ.x = worldX, waveXZ.y = worldZ
waterNormal.x += 0.05 * sin(waveXZ.x * 3.0 + iTime);
waterNormal.z += 0.05 * cos(waveXZ.y * 2.0 + iTime * 0.7);
waterNormal = normalize(waterNormal);
float fresnel = 0.04 + 0.96 * pow(1.0 - max(dot(waterNormal, -rd), 0.0), 5.0);
// Reflection
vec3 reflDir = reflect(rd, waterNormal);
HitInfo reflHit = castRay(waterPos + waterNormal * 0.01, reflDir, 64);
vec3 reflCol = reflHit.hit ? getMaterialColor(reflHit.mat, reflHit.uv) : skyColor;
// Refraction (underwater voxels: sand, seaweed)
vec3 refrDir = refract(rd, waterNormal, 1.0 / 1.33);
HitInfo refrHit = castRay(waterPos - waterNormal * 0.01, refrDir, 64);
vec3 refrCol;
if (refrHit.hit) {
vec3 matCol = getMaterialColor(refrHit.mat, refrHit.uv);
float underwaterDist = length(refrHit.pos - waterPos);
refrCol = mix(matCol, vec3(0.0, 0.15, 0.3), 1.0 - exp(-0.1 * underwaterDist));
} else {
refrCol = vec3(0.0, 0.1, 0.3);
}
col = mix(refrCol, reflCol, fresnel);
col = mix(col, vec3(0.0, 0.3, 0.5), 0.2);
}
```
### Variant 7: Rotating Voxel Objects
Rotate voxel objects as a whole. Core: apply inverse rotation to sample points in getVoxel.
```glsl
// IMPORTANT: Correct way to rotate objects: apply inverse rotation to sample coordinates in getVoxel
// Wrong approach: only rotate the camera (that just changes the viewpoint, not the object)
int getVoxel(vec3 c) {
vec3 p = c + 0.5;
// Rotate around Y axis
float angle = -iTime * 0.5;
float s = sin(angle), co = cos(angle);
p.xz = vec2(p.x * co - p.z * s, p.x * s + p.z * co);
// Can also rotate around multiple axes:
// p.yz = vec2(p.y * co2 - p.z * s2, p.y * s2 + p.z * co2); // X axis rotation
float d = sdBox(p, vec3(6.0));
if (d < 0.0) return 1;
return 0;
}
```
### Variant 8: Indoor/Cave/Enclosed Scenes (Point Lights + High Ambient Lighting)
Indoor, cave, underground, sci-fi base, and other enclosed or semi-enclosed scenes require point lights and high ambient lighting.
```glsl
// IMPORTANT: Key points for enclosed/semi-enclosed scenes (caves, interiors, sci-fi bases, mazes, etc.):
// 1. Camera must be placed inside the cavity (a position where getVoxel returns 0)
// 2. Must use point lights, not just directional light (directional light blocked by walls/ceiling = total darkness!)
// 3. Ambient light must be high enough (at least 0.2-0.3) to prevent scene from being too dark to see details
// 4. Can use multiple point lights + emissive voxels to simulate torches/fluorescence/holographic displays
// 5. Sci-fi scene metallic walls need bright enough light sources to show reflections
// 6. Emissive elements (holographic screens, indicator lights, magic circles) use emissive materials: add emissive color directly to lighting
// Cave scene: cavity = area where getVoxel returns 0
// IMPORTANT: Cave/terrain noise functions must respect vector dimensions!
// p.xz is vec2; if noise/fbm function takes vec3, construct a full vec3:
// Correct: fbm(vec3(p.xz, p.y * 0.5)) or use vec2 version of noise
// Wrong: fbm(p.xz + vec3(...)) ← vec2 + vec3 compile failure!
int getVoxel(vec3 c) {
float cave = sdSphere(c + 0.5, 12.0);
// IMPORTANT: For noise-carved detail, use c's components directly (all float)
cave += 2.0 * sin(c.x * 0.3) * sin(c.y * 0.4) * sin(c.z * 0.35);
if (cave > 0.0) return 1; // Rock wall
return 0; // Cavity (camera goes here)
}
// Point light attenuation
vec3 pointLightPos = vec3(0.0, 3.0, 0.0);
vec3 toLight = pointLightPos - hit.pos;
float lightDist = length(toLight);
vec3 lightDir = toLight / lightDist;
float attenuation = 1.0 / (1.0 + 0.1 * lightDist + 0.01 * lightDist * lightDist);
float diff = max(dot(hit.normal, lightDir), 0.0);
float shadow = castShadow(hit.pos + hit.normal * 0.01, lightDir);
vec3 lighting = vec3(0.0);
// IMPORTANT: High ambient light to prevent total darkness (required for enclosed scenes! at least 0.2)
lighting += vec3(0.25, 0.22, 0.2); // Warm ambient light
lighting += 3.0 * diff * attenuation * vec3(1.0, 0.8, 0.5) * shadow; // Point light
// Multiple torches/emissive objects (use sin for flicker animation)
vec3 torch1 = vec3(5.0, 2.0, 3.0);
vec3 torch2 = vec3(-4.0, 1.0, -5.0);
float flicker1 = 0.8 + 0.2 * sin(iTime * 5.0 + 1.0);
float flicker2 = 0.8 + 0.2 * sin(iTime * 4.3 + 2.7);
lighting += calcPointLight(hit.pos, hit.normal, torch1, vec3(1.0, 0.6, 0.2)) * flicker1;
lighting += calcPointLight(hit.pos, hit.normal, torch2, vec3(0.2, 1.0, 0.5)) * flicker2;
// Emissive materials (holographic displays, fluorescent moss, indicator lights, magic circles, etc.)
// IMPORTANT: Emissive colors are added directly to lighting, unaffected by shadows
if (hit.mat == 2) {
lighting += vec3(0.1, 0.4, 0.15); // Fluorescent moss (faint green)
}
if (hit.mat == 3) {
float pulse = 0.7 + 0.3 * sin(iTime * 2.0);
lighting += vec3(0.2, 0.6, 1.0) * pulse; // Blue pulse light
}
col = matCol * lighting;
```
### Variant 9: Voxel Character Animation
Simple voxel character animation using time-driven offsets and rotations.
```glsl
// IMPORTANT: Voxel character animation core approach:
// 1. Split the character into multiple body parts (head, torso, left arm, right arm, left leg, right leg)
// 2. Each part is an sdBox with independent offset/rotation parameters
// 3. iTime drives limb swinging (sin/cos periodic motion)
// 4. Combine all parts using SDF min()
// IMPORTANT: SwiftShader performance critical: character function is called at every DDA step!
// Must add AABB bounding box check in getVoxel: first check if c is near the character,
// skip sdBox calculations for that character if not nearby. Otherwise frame timeout → black screen
// Reduce MAX_RAY_STEPS to 64, MAX_SHADOW_STEPS to 16
int getCharacter(vec3 p, vec3 charPos, float animPhase) {
vec3 lp = p - charPos;
float limbSwing = sin(iTime * 4.0 + animPhase) * 0.5;
// Torso
float body = sdBox(lp - vec3(0, 3, 0), vec3(1.5, 2.0, 1.0));
// Head
float head = sdBox(lp - vec3(0, 6, 0), vec3(1.2, 1.2, 1.2));
// Arm swing (offset y coordinate around shoulder joint to simulate rotation)
vec3 armOffset = vec3(0, limbSwing * 2.0, limbSwing);
float leftArm = sdBox(lp - vec3(-2.5, 3, 0) - armOffset, vec3(0.5, 2.0, 0.5));
float rightArm = sdBox(lp - vec3(2.5, 3, 0) + armOffset, vec3(0.5, 2.0, 0.5));
// Alternating leg swing
vec3 legOffset = vec3(0, 0, limbSwing * 1.5);
float leftLeg = sdBox(lp - vec3(-0.7, 0, 0) - legOffset, vec3(0.5, 1.5, 0.5));
float rightLeg = sdBox(lp - vec3(0.7, 0, 0) + legOffset, vec3(0.5, 1.5, 0.5));
float d = min(body, min(head, min(leftArm, min(rightArm, min(leftLeg, rightLeg)))));
if (d < 0.0) {
if (head < 0.0) return 10; // Head (skin color)
if (leftArm < 0.0 || rightArm < 0.0) return 11; // Arms
return 12; // Torso/legs
}
return 0;
}
// Combine scene + characters in getVoxel
// IMPORTANT: Must add AABB bounding box early exit! Character sdBox calculations are expensive
int getVoxel(vec3 c) {
// Scene (floor, walls, etc.)
int scene = getSceneVoxel(c);
if (scene > 0) return scene;
// IMPORTANT: AABB check: only call getCharacter near the character
// Character 1: warrior (at position (5,0,0)), bounding box ±5 cells
if (abs(c.x - 5.0) < 5.0 && c.y >= 0.0 && c.y < 10.0 && abs(c.z) < 5.0) {
int char1 = getCharacter(c, vec3(5, 0, 0), 0.0);
if (char1 > 0) return char1;
}
// Character 2: mage (at position (-5,0,3)), bounding box ±5 cells
if (abs(c.x + 5.0) < 5.0 && c.y >= 0.0 && c.y < 10.0 && abs(c.z - 3.0) < 5.0) {
int char2 = getCharacter(c, vec3(-5, 0, 3), 3.14);
if (char2 > 0) return char2;
}
return 0;
}
```
### Variant 10: Waterfall / Flowing Water Particle Effects
Dynamic waterfall, splash particles, water mist effects. Core: time-offset noise simulates water flow, hashed particles simulate splashes, exponential decay simulates mist.
```glsl
// IMPORTANT: Key points for waterfall/flowing water/particle effects:
// 1. Waterfall stream: noise + iTime vertical offset simulates water column flowing down
// 2. Splash particles: hash-distributed voxels at the bottom, positions change with iTime to simulate splashing
// 3. Water mist: semi-transparent accumulation (reduced alpha) or density field at the bottom simulates mist diffusion
// 4. Waterfall must have a clear high point (cliff/rock wall) and low point (pool), drop ≥ 10 cells
// 5. Water stream material uses light blue-white + brightness flicker to simulate flowing water feel
float hash21(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
int getVoxel(vec3 c) {
// Cliff rock walls (both sides + back)
if (c.x < -5.0 || c.x > 5.0) {
if (c.y < 15.0 && c.z > -3.0 && c.z < 3.0) return 1; // Rock
}
if (c.z > 2.0 && c.y < 15.0 && abs(c.x) < 6.0) return 1; // Back wall
// Cliff top platform
if (c.y >= 13.0 && c.y < 15.0 && c.z > -1.0 && c.z < 3.0 && abs(c.x) < 5.0) return 1;
// Bottom pool floor
if (c.y < -2.0 && abs(c.x) < 8.0 && c.z > -6.0 && c.z < 3.0) return 2; // Pool bottom
// IMPORTANT: Waterfall stream: narrow band x ∈ [-2, 2], falling from y=13 to y=0
// Use iTime offset on y-coordinate noise to simulate downward water flow
if (abs(c.x) < 2.0 && c.y >= 0.0 && c.y < 13.0 && c.z > -1.0 && c.z < 1.0) {
float flowNoise = hash21(vec2(floor(c.x), floor(c.y - iTime * 8.0)));
if (flowNoise > 0.25) return 3; // Water (gaps simulate translucent water curtain)
}
// IMPORTANT: Splash particles: bottom y ∈ [-1, 3], x ∈ [-4, 4]
// Use hash + iTime to generate randomly bouncing voxel particles
if (c.y >= -1.0 && c.y < 3.0 && abs(c.x) < 4.0 && c.z > -3.0 && c.z < 2.0) {
float t = iTime * 3.0;
float particleHash = hash21(vec2(floor(c.x * 2.0), floor(c.z * 2.0) + floor(t)));
float yOffset = fract(t + particleHash) * 3.0; // Particle upward trajectory
if (abs(c.y - yOffset) < 0.6 && particleHash > 0.7) return 4; // Splash particle
}
// IMPORTANT: Water mist: bottom y ∈ [-1, 2], wider range than splashes
// Density decreases with height and distance from waterfall center
if (c.y >= -1.0 && c.y < 2.0 && abs(c.x) < 6.0 && c.z > -5.0 && c.z < 3.0) {
float distFromCenter = length(vec2(c.x, c.z));
float mistDensity = exp(-0.15 * distFromCenter) * exp(-0.5 * max(c.y, 0.0));
float mistNoise = hash21(vec2(floor(c.x * 0.5 + iTime * 0.5), floor(c.z * 0.5)));
if (mistNoise < mistDensity * 0.8) return 5; // Water mist
}
return 0;
}
// Material colors
vec3 getMaterialColor(int mat, vec2 uv) {
if (mat == 1) return vec3(0.45, 0.4, 0.35); // Rock
if (mat == 2) return vec3(0.35, 0.3, 0.25); // Pool bottom
if (mat == 3) { // Water stream (shimmering blue-white)
float shimmer = 0.8 + 0.2 * sin(uv.y * 20.0 + iTime * 10.0);
return vec3(0.6, 0.8, 1.0) * shimmer;
}
if (mat == 4) return vec3(0.85, 0.92, 1.0); // Splash (bright white)
if (mat == 5) return vec3(0.7, 0.82, 0.9); // Water mist (pale blue-white)
return vec3(0.5);
}
// IMPORTANT: Water mist material needs special lighting: high emissive + translucent feel
// During shading:
if (hit.mat == 5) {
lighting += vec3(0.4, 0.5, 0.6); // Water mist emissive (unaffected by shadows)
}
// Camera: side angle slightly elevated, showing the full waterfall (top to bottom + bottom splashes and mist)
// ro = vec3(12.0, 10.0, -10.0), lookAt = vec3(0.0, 6.0, 0.0)
```
### Variant 11: Multi-Building / Town / Minecraft-Style Scenes (Multi-Structure Town Composition)
Towns, villages, Minecraft-style worlds, and other scenes requiring multiple discrete structures (houses, trees, lampposts, etc.) placed on the ground.
**IMPORTANT: "Minecraft-like voxel scene" = multi-building scene; must follow the performance constraints of this template!**
```glsl
// IMPORTANT: Key points for multi-building scenes:
// 1. Define the ground first (height map or flat plane), ensure ground getVoxel returns correct material
// 2. Each building uses an independent helper function, receiving local coordinates, returning material ID
// 3. In getVoxel, check each building sequentially (using offset coordinates), return on first hit
// 4. Camera must be outside the scene facing the center, far enough to see the full view
// 5. IMPORTANT: Building coordinate ranges must be within DDA traversal range (MAX_RAY_STEPS * cell ≈ reachable distance)
// 6. IMPORTANT: Scene range should not be too large! Concentrate all buildings within -20~20 range, camera 30-50 cells away
// 7. IMPORTANT: SwiftShader performance critical: getVoxel must have AABB bounding box early exit!
// Above ground (c.y > 0), check AABB range first; return 0 immediately if outside building area
// Otherwise every DDA step checks all buildings → frame timeout → black screen / only sky renders
// 8. IMPORTANT: MAX_RAY_STEPS reduced to 64, MAX_SHADOW_STEPS to 16 (complex getVoxel requires lower step counts)
// Single house: width w, depth d, height h, with triangular roof
int makeHouse(vec3 p, float w, float d, float h, int wallMat, int roofMat) {
// Walls
if (p.x >= 0.0 && p.x < w && p.z >= 0.0 && p.z < d && p.y >= 0.0 && p.y < h) {
return wallMat;
}
// Triangular roof: starts from wall top, x range narrows by 1 per level
float roofY = p.y - h;
float roofInset = roofY; // Inset by 1 cell per level
if (roofY >= 0.0 && roofY < w * 0.5
&& p.x >= roofInset && p.x < w - roofInset
&& p.z >= 0.0 && p.z < d) {
return roofMat;
}
return 0;
}
// Tree: trunk + spherical canopy
int makeTree(vec3 p, float trunkH, float crownR, int trunkMat, int leafMat) {
// Trunk (1x1 column)
if (p.x >= -0.5 && p.x < 0.5 && p.z >= -0.5 && p.z < 0.5
&& p.y >= 0.0 && p.y < trunkH) {
return trunkMat;
}
// Spherical canopy
vec3 crownCenter = vec3(0.0, trunkH + crownR * 0.5, 0.0);
if (length(p - crownCenter) < crownR) {
return leafMat;
}
return 0;
}
// Lamppost: thin pole + glowing top block
int makeLamp(vec3 p, float h, int poleMat, int lightMat) {
if (p.x >= -0.3 && p.x < 0.3 && p.z >= -0.3 && p.z < 0.3
&& p.y >= 0.0 && p.y < h) {
return poleMat; // Pole
}
if (p.x >= -0.5 && p.x < 0.5 && p.z >= -0.5 && p.z < 0.5
&& p.y >= h && p.y < h + 1.0) {
return lightMat; // Lamp head (emissive)
}
return 0;
}
int getVoxel(vec3 c) {
// 1. Ground (y < 0 is underground, y == 0 layer is surface)
if (c.y < -1.0) return 0;
if (c.y < 0.0) return 1; // Ground (dirt/grass)
// 2. Road (along z direction, x range -2~2)
if (c.y < 1.0 && abs(c.x) < 2.0) return 2; // Road surface
// IMPORTANT: AABB bounding box early exit (required for SwiftShader!)
// All buildings are within x:-15~15, y:0~12, z:-5~15
// Return 0 immediately outside this range, avoiding per-building checks
if (c.x < -15.0 || c.x > 15.0 || c.y > 12.0 || c.z < -5.0 || c.z > 15.0) return 0;
// 3. Place buildings (each with offset coordinates)
// IMPORTANT: House width/height must be ≥ 5 cells, otherwise they look like dots from far away! Use bright material colors
int m;
// House A: position (5, 0, 3), width 6, depth 5, height 5
m = makeHouse(c - vec3(5.0, 0.0, 3.0), 6.0, 5.0, 5.0, 3, 4);
if (m > 0) return m;
// House B: position (-10, 0, 2), width 7, depth 5, height 5
m = makeHouse(c - vec3(-10.0, 0.0, 2.0), 7.0, 5.0, 5.0, 5, 4);
if (m > 0) return m;
// Tree: position (0, 0, 8)
m = makeTree(c - vec3(0.0, 0.0, 8.0), 4.0, 2.5, 6, 7);
if (m > 0) return m;
// Lamppost: position (3, 0, 0)
m = makeLamp(c - vec3(3.0, 0.0, 0.0), 5.0, 8, 9);
if (m > 0) return m;
return 0;
}
// IMPORTANT: Camera setup: must be far enough to overlook the entire town
// Recommended: ro = vec3(0, 15, -35), looking at scene center vec3(0, 3, 5)
vec3 ro = vec3(0.0, 15.0, -35.0);
vec3 lookAt = vec3(0.0, 3.0, 5.0);
vec3 forward = normalize(lookAt - ro);
vec3 right = normalize(cross(forward, vec3(0, 1, 0)));
vec3 up = cross(right, forward);
vec3 rd = normalize(forward * 0.8 + right * screenPos.x + up * screenPos.y);
// IMPORTANT: Sunset/side-lit scene key: when light comes from the side or at low angle, building fronts may be completely backlit turning into black silhouettes!
// Must satisfy all: (1) ambient light ≥ 0.3 (prevent backlit faces from going black); (2) house walls use bright materials (e.g., light yellow 0.85,0.75,0.55)
// (3) house dimensions must not be too small (width/height ≥ 5 cells), otherwise they look like dots from far away
vec3 sunDir = normalize(vec3(-0.8, 0.3, 0.5)); // Sunset low angle
vec3 sunColor = vec3(1.0, 0.6, 0.3); // Warm orange
vec3 ambientColor = vec3(0.35, 0.3, 0.4); // IMPORTANT: High ambient light (≥0.3) to prevent silhouettes
// lighting = ambientColor + diff * sunColor * shadow;
```
## Performance & Composition
**Performance Tips:**
- Early exit: break immediately when `mapPos` exceeds scene bounds
- Shadow ray steps of 16-24 are sufficient
- Use SDF sphere-tracing with large steps in open areas, switch to DDA near surfaces
- Material queries, AO, normals, etc. are only computed after hit
- Replace procedural voxel queries with `texelFetch` texture sampling
- Multi-frame accumulation + reprojection for low-noise results
- **IMPORTANT: MAX_RAY_STEPS defaults to 64, MAX_SHADOW_STEPS defaults to 16 (total 80)**. Only simple scenes (single cube/sphere) can increase to 96+24. Multi-building/Minecraft/character scenes with complex getVoxel must keep 64+16 or lower, otherwise SwiftShader frame timeout → only sky background renders
**Composition Tips:**
- **Procedural noise terrain**: use FBM/Perlin noise height maps inside `getVoxel()`
- **SDF procedural modeling**: use SDF boolean operations inside `getVoxel()` to define shapes
- **Texture mapping**: after hit, sample 16x16 pixel textures using face UV * 16
- **Atmospheric scattering / volumetric fog**: accumulate medium density during DDA traversal
- **Water surface rendering**: Fresnel reflection/refraction on a specific Y plane (see Variant 6 above)
- **Global illumination**: cone tracing or Monte Carlo hemisphere sampling
- **Temporal reprojection**: multi-frame accumulation + previous frame reprojection for anti-aliasing and denoising
## Common Errors
1. **GLSL reserved words causing compile failure**: `cast`, `class`, `template`, `namespace`, `input`, `output`, `filter`, `image`, `sampler`, `half`, `fixed`, etc. are GLSL reserved words and **must never be used as variable or function names**. Use compound names: `castRay`, `castShadow`, `shootRay`, `spellEffect` (not `cast`)
2. **Enclosed/semi-enclosed scene total darkness**: caves, interiors, sci-fi bases, mazes, and other enclosed scenes cannot rely solely on directional light (completely blocked by walls/ceiling); must use point lights + high ambient light (≥0.2) + emissive materials (see Variant 8)
3. **Camera inside voxel causing rendering anomalies**: cave/indoor scene camera origin must be inside the cavity (where getVoxel returns 0), otherwise the first DDA step hits immediately = scene invisible
4. **Complex getVoxel causing SwiftShader black screen (most common with Minecraft-style/town/character/multi-building scenes!)**: getVoxel is called once per DDA step; if it contains multiple buildings/characters/terrain+trees without early exit, frame timeout → only sky background renders. **Must do all of**: (1) AABB bounding box early exit (check coordinate range first, return 0 immediately outside building area); (2) MAX_RAY_STEPS ≤ 64, MAX_SHADOW_STEPS ≤ 16; (3) scene range within ±20 cells. **Minecraft-style scene = multi-building scene**; must follow this rule (see Variant 9, 11 template code)
5. **vec2/vec3 dimension mismatch causing compile failure**: `p.xz` returns `vec2` and cannot be passed directly to noise/fbm functions expecting `vec3` parameters or used in operations with `vec3`. Use `vec3(p.xz, val)` to construct a full vec3, or use vec2 versions of functions
6. **Mountain/terrain height-based coloring invisible**: (1) `maxH` must equal the actual max return value of the terrain noise function (don't arbitrarily use 20.0); (2) grass threshold at 0.4 (largest area ensures green is visible), rock 0.4~0.7, snow >0.7; (3) grass green must be saturated enough `vec3(0.25, 0.55, 0.15)` not grayish; (4) sun intensity ≤2.0, sky light ≤1.0, too bright washes out colors; (5) gamma correction reduces saturation, pre-compensate material colors (see Step 4 mountain terrain template)
7. **Waterfall/flowing water effect lacks recognizability**: waterfall must have a clear cliff drop (≥10 cells), visible water column (noise + iTime offset), bottom splash particles (hash random bouncing), and mist (exponential decay density field). Just a gradient color block is not a waterfall! See Variant 10 complete template
8. **"Low saturation coloring" becomes pure white/gray**: low saturation ≠ near white! Low saturation means colors are not vivid but still have clear hue (e.g., brick red `vec3(0.55, 0.35, 0.3)` not gray-white `vec3(0.8, 0.8, 0.8)`). Brick/stone textures must use UV periodic patterns (staggered rows + mortar dark lines), not solid colors. See the `getBrickColor` function in the complete template
9. **Sunset/side-lit scene buildings become black silhouettes**: when low-angle light (sunset/dawn) illuminates from the side, building fronts are completely backlit → pure black silhouettes with no visible detail. Must: (1) ambient light ≥ 0.3; (2) walls use bright materials (light yellow, off-white) not dark colors; (3) buildings large enough (width/height ≥ 5 cells). See Variant 11 sunset scene code
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/voxel-rendering.md)

View File

@@ -0,0 +1,490 @@
# Water & Ocean Rendering Skill
## Use Cases
- Rendering water body surfaces such as oceans, lakes, and rivers
- Water surface reflection/refraction, Fresnel effects
- Underwater caustics lighting effects
- Waves, foam, and water flow animation
## Core Principles
Water rendering solves three problems: **water surface shape generation**, **light-water surface interaction**, and **water body color compositing**.
### Wave Generation: Exponential Sine Stacking + Derivative Domain Warping
`wave(x) = exp(sin(x) - 1)` — sharp wave crests (`exp(0)=1`), broad flat troughs (`exp(-2)≈0.135`), similar to a trochoidal profile but at much lower computational cost than Gerstner waves.
When stacking multiple waves, use **derivative domain warping (Drag)**:
```
position += direction * derivative * weight * DRAG_MULT
```
Small ripples cluster on the crests of large waves, simulating capillary waves riding on gravity waves.
### Lighting: Schlick Fresnel + Subsurface Scattering
- **Schlick Fresnel**: `F = F0 + (1-F0) * (1-dot(N,V))^5`, water F0 ≈ 0.04
- **SSS approximation**: thicker water layer at troughs → stronger blue-green scattering; thinner layer at crests → weaker scattering
### Water Surface Intersection: Bounded Height Field Marching
The water surface is constrained within a `[0, -WATER_DEPTH]` bounding box, with adaptive step size: `step = ray_y - wave_height`.
## Implementation Steps
### Step 1: Exponential Sine Wave Function
```glsl
// Single wave: exp(sin(x)-1) produces sharp peaks and broad troughs, returns (value, negative derivative)
vec2 wavedx(vec2 position, vec2 direction, float frequency, float timeshift) {
float x = dot(direction, position) * frequency + timeshift;
float wave = exp(sin(x) - 1.0);
float dx = wave * cos(x);
return vec2(wave, -dx);
}
```
### Step 2: Multi-Octave Wave Stacking with Domain Warping
```glsl
#define DRAG_MULT 0.38 // Domain warp strength, 0=none, 0.5=strong clustering
float getwaves(vec2 position, int iterations) {
float wavePhaseShift = length(position) * 0.1;
float iter = 0.0;
float frequency = 1.0;
float timeMultiplier = 2.0;
float weight = 1.0;
float sumOfValues = 0.0;
float sumOfWeights = 0.0;
for (int i = 0; i < iterations; i++) {
vec2 p = vec2(sin(iter), cos(iter)); // Pseudo-random wave direction
vec2 res = wavedx(position, p, frequency, iTime * timeMultiplier + wavePhaseShift);
position += p * res.y * weight * DRAG_MULT; // Derivative domain warp
sumOfValues += res.x * weight;
sumOfWeights += weight;
weight = mix(weight, 0.0, 0.2); // Weight decay
frequency *= 1.18; // Frequency growth rate
timeMultiplier *= 1.07; // Dispersion
iter += 1232.399963; // Uniform direction distribution
}
return sumOfValues / sumOfWeights;
}
```
### Step 3: Bounded Bounding Box Ray Marching
```glsl
#define WATER_DEPTH 1.0
float intersectPlane(vec3 origin, vec3 direction, vec3 point, vec3 normal) {
return clamp(dot(point - origin, normal) / dot(direction, normal), -1.0, 9991999.0);
}
float raymarchwater(vec3 camera, vec3 start, vec3 end, float depth) {
vec3 pos = start;
vec3 dir = normalize(end - start);
for (int i = 0; i < 64; i++) {
float height = getwaves(pos.xz, ITERATIONS_RAYMARCH) * depth - depth;
if (height + 0.01 > pos.y) {
return distance(pos, camera);
}
pos += dir * (pos.y - height); // Adaptive step size
}
return distance(start, camera);
}
```
### Step 4: Normal Calculation and Distance Smoothing
```glsl
#define ITERATIONS_RAYMARCH 12 // For marching (fewer = faster)
#define ITERATIONS_NORMAL 36 // For normals (more = finer detail)
vec3 calcNormal(vec2 pos, float e, float depth) {
vec2 ex = vec2(e, 0);
float H = getwaves(pos.xy, ITERATIONS_NORMAL) * depth;
vec3 a = vec3(pos.x, H, pos.y);
return normalize(
cross(
a - vec3(pos.x - e, getwaves(pos.xy - ex.xy, ITERATIONS_NORMAL) * depth, pos.y),
a - vec3(pos.x, getwaves(pos.xy + ex.yx, ITERATIONS_NORMAL) * depth, pos.y + e)
)
);
}
// Distance smoothing: normals approach (0,1,0) at far distances
// N = mix(N, vec3(0.0, 1.0, 0.0), 0.8 * min(1.0, sqrt(dist * 0.01) * 1.1));
```
### Step 5: Fresnel Reflection and Subsurface Scattering
```glsl
float fresnel = 0.04 + 0.96 * pow(1.0 - max(0.0, dot(-N, ray)), 5.0);
vec3 R = normalize(reflect(ray, N));
R.y = abs(R.y); // Force upward to avoid self-intersection
vec3 reflection = getAtmosphere(R) + getSun(R);
vec3 scattering = vec3(0.0293, 0.0698, 0.1717) * 0.1
* (0.2 + (waterHitPos.y + WATER_DEPTH) / WATER_DEPTH);
vec3 C = fresnel * reflection + scattering;
```
### Step 6: Atmosphere and Tone Mapping
```glsl
vec3 extra_cheap_atmosphere(vec3 raydir, vec3 sundir) {
float special_trick = 1.0 / (raydir.y * 1.0 + 0.1);
float special_trick2 = 1.0 / (sundir.y * 11.0 + 1.0);
float raysundt = pow(abs(dot(sundir, raydir)), 2.0);
float sundt = pow(max(0.0, dot(sundir, raydir)), 8.0);
float mymie = sundt * special_trick * 0.2;
vec3 suncolor = mix(vec3(1.0), max(vec3(0.0), vec3(1.0) - vec3(5.5, 13.0, 22.4) / 22.4),
special_trick2);
vec3 bluesky = vec3(5.5, 13.0, 22.4) / 22.4 * suncolor;
vec3 bluesky2 = max(vec3(0.0), bluesky - vec3(5.5, 13.0, 22.4) * 0.002
* (special_trick + -6.0 * sundir.y * sundir.y));
bluesky2 *= special_trick * (0.24 + raysundt * 0.24);
return bluesky2 * (1.0 + 1.0 * pow(1.0 - raydir.y, 3.0));
}
vec3 aces_tonemap(vec3 color) {
mat3 m1 = mat3(
0.59719, 0.07600, 0.02840,
0.35458, 0.90834, 0.13383,
0.04823, 0.01566, 0.83777);
mat3 m2 = mat3(
1.60475, -0.10208, -0.00327,
-0.53108, 1.10813, -0.07276,
-0.07367, -0.00605, 1.07602);
vec3 v = m1 * color;
vec3 a = v * (v + 0.0245786) - 0.000090537;
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
return pow(clamp(m2 * (a / b), 0.0, 1.0), vec3(1.0 / 2.2));
}
```
## Complete Code Template
Can be pasted directly into ShaderToy to run. Distilled from `afl_ext`'s "Very fast procedural ocean".
```glsl
// Water & Ocean Rendering — ShaderToy Template
// exp(sin) wave model + derivative domain warp + Schlick Fresnel + SSS
// ==================== Tunable Parameters ====================
#define DRAG_MULT 0.38
#define WATER_DEPTH 1.0
#define CAMERA_HEIGHT 1.5
#define ITERATIONS_RAYMARCH 12
#define ITERATIONS_NORMAL 36
#define RAYMARCH_STEPS 64
#define NORMAL_EPSILON 0.01
#define FRESNEL_F0 0.04
#define SSS_COLOR vec3(0.0293, 0.0698, 0.1717)
#define SSS_INTENSITY 0.1
#define SUN_POWER 720.0
#define SUN_BRIGHTNESS 210.0
#define EXPOSURE 2.0
// ==================== Wave Functions ====================
vec2 wavedx(vec2 position, vec2 direction, float frequency, float timeshift) {
float x = dot(direction, position) * frequency + timeshift;
float wave = exp(sin(x) - 1.0);
float dx = wave * cos(x);
return vec2(wave, -dx);
}
float getwaves(vec2 position, int iterations) {
float wavePhaseShift = length(position) * 0.1;
float iter = 0.0;
float frequency = 1.0;
float timeMultiplier = 2.0;
float weight = 1.0;
float sumOfValues = 0.0;
float sumOfWeights = 0.0;
for (int i = 0; i < iterations; i++) {
vec2 p = vec2(sin(iter), cos(iter));
vec2 res = wavedx(position, p, frequency, iTime * timeMultiplier + wavePhaseShift);
position += p * res.y * weight * DRAG_MULT;
sumOfValues += res.x * weight;
sumOfWeights += weight;
weight = mix(weight, 0.0, 0.2);
frequency *= 1.18;
timeMultiplier *= 1.07;
iter += 1232.399963;
}
return sumOfValues / sumOfWeights;
}
// ==================== Ray Marching ====================
float intersectPlane(vec3 origin, vec3 direction, vec3 point, vec3 normal) {
return clamp(dot(point - origin, normal) / dot(direction, normal), -1.0, 9991999.0);
}
float raymarchwater(vec3 camera, vec3 start, vec3 end, float depth) {
vec3 pos = start;
vec3 dir = normalize(end - start);
for (int i = 0; i < RAYMARCH_STEPS; i++) {
float height = getwaves(pos.xz, ITERATIONS_RAYMARCH) * depth - depth;
if (height + 0.01 > pos.y) {
return distance(pos, camera);
}
pos += dir * (pos.y - height);
}
return distance(start, camera);
}
// ==================== Normals ====================
vec3 calcNormal(vec2 pos, float e, float depth) {
vec2 ex = vec2(e, 0);
float H = getwaves(pos.xy, ITERATIONS_NORMAL) * depth;
vec3 a = vec3(pos.x, H, pos.y);
return normalize(
cross(
a - vec3(pos.x - e, getwaves(pos.xy - ex.xy, ITERATIONS_NORMAL) * depth, pos.y),
a - vec3(pos.x, getwaves(pos.xy + ex.yx, ITERATIONS_NORMAL) * depth, pos.y + e)
)
);
}
// ==================== Camera ====================
#define NormalizedMouse (iMouse.xy / iResolution.xy)
mat3 createRotationMatrixAxisAngle(vec3 axis, float angle) {
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
return mat3(
oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s,
oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s,
oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c
);
}
vec3 getRay(vec2 fragCoord) {
vec2 uv = ((fragCoord.xy / iResolution.xy) * 2.0 - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);
vec3 proj = normalize(vec3(uv.x, uv.y, 1.5));
if (iResolution.x < 600.0) return proj;
return createRotationMatrixAxisAngle(vec3(0.0, -1.0, 0.0), 3.0 * ((NormalizedMouse.x + 0.5) * 2.0 - 1.0))
* createRotationMatrixAxisAngle(vec3(1.0, 0.0, 0.0), 0.5 + 1.5 * (((NormalizedMouse.y == 0.0 ? 0.27 : NormalizedMouse.y)) * 2.0 - 1.0))
* proj;
}
// ==================== Atmosphere ====================
vec3 getSunDirection() {
return normalize(vec3(-0.0773502691896258, 0.5 + sin(iTime * 0.2 + 2.6) * 0.45, 0.5773502691896258));
}
vec3 extra_cheap_atmosphere(vec3 raydir, vec3 sundir) {
float special_trick = 1.0 / (raydir.y * 1.0 + 0.1);
float special_trick2 = 1.0 / (sundir.y * 11.0 + 1.0);
float raysundt = pow(abs(dot(sundir, raydir)), 2.0);
float sundt = pow(max(0.0, dot(sundir, raydir)), 8.0);
float mymie = sundt * special_trick * 0.2;
vec3 suncolor = mix(vec3(1.0), max(vec3(0.0), vec3(1.0) - vec3(5.5, 13.0, 22.4) / 22.4), special_trick2);
vec3 bluesky = vec3(5.5, 13.0, 22.4) / 22.4 * suncolor;
vec3 bluesky2 = max(vec3(0.0), bluesky - vec3(5.5, 13.0, 22.4) * 0.002 * (special_trick + -6.0 * sundir.y * sundir.y));
bluesky2 *= special_trick * (0.24 + raysundt * 0.24);
return bluesky2 * (1.0 + 1.0 * pow(1.0 - raydir.y, 3.0));
}
vec3 getAtmosphere(vec3 dir) {
return extra_cheap_atmosphere(dir, getSunDirection()) * 0.5;
}
float getSun(vec3 dir) {
return pow(max(0.0, dot(dir, getSunDirection())), SUN_POWER) * SUN_BRIGHTNESS;
}
// ==================== Tone Mapping ====================
vec3 aces_tonemap(vec3 color) {
mat3 m1 = mat3(
0.59719, 0.07600, 0.02840,
0.35458, 0.90834, 0.13383,
0.04823, 0.01566, 0.83777);
mat3 m2 = mat3(
1.60475, -0.10208, -0.00327,
-0.53108, 1.10813, -0.07276,
-0.07367, -0.00605, 1.07602);
vec3 v = m1 * color;
vec3 a = v * (v + 0.0245786) - 0.000090537;
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
return pow(clamp(m2 * (a / b), 0.0, 1.0), vec3(1.0 / 2.2));
}
// ==================== Main Function ====================
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec3 ray = getRay(fragCoord);
if (ray.y >= 0.0) {
vec3 C = getAtmosphere(ray) + getSun(ray);
fragColor = vec4(aces_tonemap(C * EXPOSURE), 1.0);
return;
}
vec3 waterPlaneHigh = vec3(0.0, 0.0, 0.0);
vec3 waterPlaneLow = vec3(0.0, -WATER_DEPTH, 0.0);
vec3 origin = vec3(iTime * 0.2, CAMERA_HEIGHT, 1.0);
float highPlaneHit = intersectPlane(origin, ray, waterPlaneHigh, vec3(0.0, 1.0, 0.0));
float lowPlaneHit = intersectPlane(origin, ray, waterPlaneLow, vec3(0.0, 1.0, 0.0));
vec3 highHitPos = origin + ray * highPlaneHit;
vec3 lowHitPos = origin + ray * lowPlaneHit;
float dist = raymarchwater(origin, highHitPos, lowHitPos, WATER_DEPTH);
vec3 waterHitPos = origin + ray * dist;
vec3 N = calcNormal(waterHitPos.xz, NORMAL_EPSILON, WATER_DEPTH);
N = mix(N, vec3(0.0, 1.0, 0.0), 0.8 * min(1.0, sqrt(dist * 0.01) * 1.1));
float fresnel = FRESNEL_F0 + (1.0 - FRESNEL_F0) * pow(1.0 - max(0.0, dot(-N, ray)), 5.0);
vec3 R = normalize(reflect(ray, N));
R.y = abs(R.y);
vec3 reflection = getAtmosphere(R) + getSun(R);
vec3 scattering = SSS_COLOR * SSS_INTENSITY
* (0.2 + (waterHitPos.y + WATER_DEPTH) / WATER_DEPTH);
vec3 C = fresnel * reflection + scattering;
fragColor = vec4(aces_tonemap(C * EXPOSURE), 1.0);
}
```
## Common Variants
### Variant 1: 2D Underwater Caustic Texture
```glsl
#define TAU 6.28318530718
#define MAX_ITER 5
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
float time = iTime * 0.5 + 23.0;
vec2 uv = fragCoord.xy / iResolution.xy;
vec2 p = mod(uv * TAU, TAU) - 250.0;
vec2 i = vec2(p);
float c = 1.0;
float inten = 0.005;
for (int n = 0; n < MAX_ITER; n++) {
float t = time * (1.0 - (3.5 / float(n + 1)));
i = p + vec2(cos(t - i.x) + sin(t + i.y), sin(t - i.y) + cos(t + i.x));
c += 1.0 / length(vec2(p.x / (sin(i.x + t) / inten), p.y / (cos(i.y + t) / inten)));
}
c /= float(MAX_ITER);
c = 1.17 - pow(c, 1.4);
vec3 colour = vec3(pow(abs(c), 8.0));
colour = clamp(colour + vec3(0.0, 0.35, 0.5), 0.0, 1.0);
fragColor = vec4(colour, 1.0);
}
```
### Variant 2: FBM Bump-Mapped Lake Surface
```glsl
float waterMap(vec2 pos) {
mat2 m2 = mat2(0.60, -0.80, 0.80, 0.60);
vec2 posm = pos * m2;
return abs(fbm(vec3(8.0 * posm, iTime)) - 0.5) * 0.1;
}
// Analytic plane intersection instead of ray marching
float t = -ro.y / rd.y;
vec3 hitPos = ro + rd * t;
// Finite difference normals (central differencing)
float eps = 0.1;
vec3 normal = vec3(0.0, 1.0, 0.0);
normal.x = -bumpfactor * (waterMap(hitPos.xz + vec2(eps, 0.0)) - waterMap(hitPos.xz - vec2(eps, 0.0))) / (2.0 * eps);
normal.z = -bumpfactor * (waterMap(hitPos.xz + vec2(0.0, eps)) - waterMap(hitPos.xz - vec2(0.0, eps))) / (2.0 * eps);
normal = normalize(normal);
float bumpfactor = 0.1 * (1.0 - smoothstep(0.0, 60.0, distance(ro, hitPos)));
vec3 refracted = refract(rd, normal, 1.0 / 1.333);
```
### Variant 3: Ridge Noise Coastal Waves
```glsl
float sea(vec2 p) {
float f = 1.0;
float r = 0.0;
float time = -iTime;
for (int i = 0; i < 8; i++) {
r += (1.0 - abs(noise(p * f + 0.9 * time))) / f;
f *= 2.0;
p -= vec2(-0.01, 0.04) * (r - 0.2 * time / (0.1 - f));
}
return r / 4.0 + 0.5;
}
// Shoreline foam
float dh = seaDist - rockDist;
float foam = 0.0;
if (dh < 0.0 && dh > -0.02) {
foam = 0.5 * exp(20.0 * dh);
}
```
### Variant 4: Flow Map Water Animation
```glsl
vec3 FBM_DXY(vec2 p, vec2 flow, float persistence, float domainWarp) {
vec3 f = vec3(0.0);
float tot = 0.0;
float a = 1.0;
for (int i = 0; i < 4; i++) {
p += flow;
flow *= -0.75;
vec3 v = SmoothNoise_DXY(p);
f += v * a;
p += v.xy * domainWarp;
p *= 2.0;
tot += a;
a *= persistence;
}
return f / tot;
}
// Two-phase flow cycle (eliminates stretching)
float t0 = fract(time);
float t1 = fract(time + 0.5);
vec4 sample0 = SampleWaterNormal(uv + Hash2(floor(time)), flowRate * (t0 - 0.5));
vec4 sample1 = SampleWaterNormal(uv + Hash2(floor(time+0.5)), flowRate * (t1 - 0.5));
float weight = abs(t0 - 0.5) * 2.0;
vec4 result = mix(sample0, sample1, weight);
```
### Variant 5: Beer's Law Water Absorption
```glsl
vec3 GetWaterExtinction(float dist) {
float fOpticalDepth = dist * 6.0;
vec3 vExtinctCol = vec3(0.5, 0.6, 0.9);
return exp2(-fOpticalDepth * vExtinctCol);
}
vec3 vInscatter = vSurfaceDiffuse * (1.0 - exp(-refractDist * 0.1))
* (1.0 + dot(sunDir, viewDir));
vec3 underwaterColor = terrainColor * GetWaterExtinction(waterDepth) + vInscatter;
vec3 finalColor = mix(underwaterColor, reflectionColor, fresnel);
```
## Performance & Composition
### Performance Tips
- **Dual iteration count strategy**: 12 iterations for marching, 36 for normals — halves render time with virtually no visual loss
- **Distance-adaptive normal smoothing**: `N = mix(N, up, 0.8 * min(1.0, sqrt(dist*0.01)*1.1))`, eliminates distant flickering
- **Bounding box clipping**: pre-compute upper/lower plane intersections, early-out for sky directions
- **Adaptive step size**: `pos += dir * (pos.y - height)`, 3-5x faster than fixed steps
- **Filter-width-aware decay**: `dFdx/dFdy` driven normal LOD
- **LOD conditional detail**: only compute high-frequency displacement at close range
### Composition Tips
- **Volumetric clouds**: ray march clouds along reflection direction `R`, blend into reflection term
- **Terrain coastline**: `dh = waterSDF - terrainSDF`, render foam when `dh ≈ 0`
- **Caustics overlay**: project Variant 1 onto underwater terrain, `caustic * exp(-depth * absorption)` depth attenuation
- **Fog/atmosphere**: independent extinction + in-scatter, per-channel RGB decay:
```glsl
vec3 fogExtinction = exp2(fogExtCoeffs * -distance);
vec3 fogInscatter = fogColor * (1.0 - exp2(fogInCoeffs * -distance));
finalColor = finalColor * fogExtinction + fogInscatter;
```
- **Post-processing**: Bloom (Fibonacci spiral blur), ACES tone mapping, depth of field (DOF)
## Further Reading
For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/water-ocean.md)

View File

@@ -0,0 +1,170 @@
# WebGL2 Pitfalls & Common Errors
## Use Cases
- Avoiding common GLSL compilation errors when generating standalone WebGL2 shader pages
- Debugging shader compilation failures
- Ensuring shader templates from ShaderToy work correctly in WebGL2
## Critical WebGL2 Rules
### 1. Fragment Coordinate — Use `gl_FragCoord.xy`
**ERROR**: `'fragCoord' : undeclared identifier`
In WebGL2 fragment shaders, `fragCoord` is not a built-in variable. Use `gl_FragCoord.xy` instead.
```glsl
// WRONG
void main() {
vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
}
// CORRECT
void main() {
vec2 uv = (2.0 * gl_FragCoord.xy - iResolution.xy) / iResolution.y;
}
```
### 2. Shadertoy mainImage — Must Wrap in `main()`
**ERROR**: `'' : Missing main()`
If your fragment shader uses `void mainImage(out vec4, in vec2)`, you must provide a `main()` wrapper.
```glsl
// WRONG — only defines mainImage but no main()
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// shader code...
fragColor = vec4(col, 1.0);
}
// CORRECT
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
// shader code...
fragColor = vec4(col, 1.0);
}
void main() {
mainImage(fragColor, gl_FragCoord.xy);
}
```
### 3. Function Declaration Order — Declare Before Use
**ERROR**: `'functionName' : no matching overloaded function found`
GLSL requires functions to be declared before they are used. Forward declarations or reordering is needed.
```glsl
// WRONG — getAtmosphere() calls getSunDirection() which is defined after
vec3 getAtmosphere(vec3 dir) {
return extra_cheap_atmosphere(dir, getSunDirection()) * 0.5; // Error!
}
vec3 getSunDirection() {
return normalize(vec3(-0.5, 0.8, -0.6));
}
// CORRECT — reorder functions
vec3 getSunDirection() { // Define first
return normalize(vec3(-0.5, 0.8, -0.6));
}
vec3 getAtmosphere(vec3 dir) { // Now can call getSunDirection()
return extra_cheap_atmosphere(dir, getSunDirection()) * 0.5;
}
```
### 4. Macro Limitations — `#define` Cannot Use Functions
**ERROR**: Various compilation errors with `#define` macros
Macros are text substitution and cannot call functions or use parentheses in the same way as C++.
```glsl
// WRONG
#define SUN_DIR normalize(vec3(0.8, 0.4, -0.6))
#define WORLD_TIME (iTime * speed())
// CORRECT — use const
const vec3 SUN_DIR = vec3(0.756, 0.378, -0.567); // Pre-computed normalized value
const float WORLD_TIME = 1.0;
```
### 5. Vector Component Access — Terrain Functions
**ERROR**: `'terrainM' : no matching overloaded function found`
When passing positions to terrain functions that expect `vec2`, extract the XZ components properly.
```glsl
// WRONG — terrainM expects vec2, but passing vec3
float calcAO(vec3 pos, vec3 nor) {
float d = terrainM(pos + h * nor); // Error: pos + h*nor is vec3
...
}
// CORRECT — extract xz components
float calcAO(vec3 pos, vec3 nor) {
float d = terrainM(pos.xz + h * nor.xz);
...
}
```
### 6. Loop Index — Use Runtime Constants
**ERROR**: Loop index must be a runtime expression
GLSL ES requires loop indices to be determinable at runtime, not compile-time constants in some contexts.
```glsl
// WRONG — AA is a #define constant
for (int i = 0; i < AA; i++) { ... }
// CORRECT — use a runtime-safe approach
for (int i = 0; i < 4; i++) { ... } // Or pass as uniform
```
### 7. Uniform Usage — Avoid Unused Uniforms
**ERROR**: Uniform optimized away causes `gl.getUniformLocation()` to return `null`
If a uniform is declared but not used, the compiler may optimize it out.
```glsl
// WRONG — iTime declared but used in a conditional that might be false
uniform float iTime;
if (false) { x = iTime; } // iTime optimized away
// CORRECT — always use the uniform in a way the compiler can't optimize out
uniform float iTime;
float t = iTime * 0.0; // Always use iTime somehow
if (someCondition) { x = t; }
```
## Complete WebGL2 Adaptation Checklist
When generating standalone HTML pages:
1. **Shader Version**: `#version 300 es` must be the very first line
2. **Fragment Output**: Declare `out vec4 fragColor;`
3. **Entry Point**: Wrap `mainImage()` in `void main()` that calls `mainImage(fragColor, gl_FragCoord.xy)`
4. **Fragment Coord**: Use `gl_FragCoord.xy` not `fragCoord`
5. **Preprocessor**: Don't use functions in `#define` macros
6. **Function Order**: Declare functions before they are used, or use forward declarations
7. **Texture**: Use `texture()` not `texture2D()`
8. **Attributes**: `attribute``in`, `varying``in`/`out`
## Common Error Messages Reference
| Error Message | Likely Cause | Solution |
|---|---|---|
| `'fragCoord' : undeclared identifier` | Using `fragCoord` instead of `gl_FragCoord.xy` | Replace with `gl_FragCoord.xy` |
| `'' : Missing main()` | No `main()` function defined | Add wrapper `void main() { mainImage(...); }` |
| `'function' : no matching overloaded function` | Wrong argument types or function order | Check parameter types, reorder functions |
| `return' : function return is not matching` | Return type mismatch | Verify return expression matches declared return type |
| `#version` must be first | Leading whitespace in shader source | Use `.trim()` when extracting from script tags |
| Uniform `null` from `getUniformLocation` | Uniform optimized away | Ensure uniform is actually used in shader code |
## Further Reading
See [reference/webgl-pitfalls.md](../reference/webgl-pitfalls.md) for additional debugging techniques.