Add ios-application-dev and shader-dev skills
Power by MiniMax
This commit is contained in:
364
skills/shader-dev/techniques/ambient-occlusion.md
Normal file
364
skills/shader-dev/techniques/ambient-occlusion.md
Normal 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)
|
||||
542
skills/shader-dev/techniques/analytic-ray-tracing.md
Normal file
542
skills/shader-dev/techniques/analytic-ray-tracing.md
Normal 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)
|
||||
124
skills/shader-dev/techniques/anti-aliasing.md
Normal file
124
skills/shader-dev/techniques/anti-aliasing.md
Normal 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)
|
||||
522
skills/shader-dev/techniques/atmospheric-scattering.md
Normal file
522
skills/shader-dev/techniques/atmospheric-scattering.md
Normal 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)
|
||||
115
skills/shader-dev/techniques/camera-effects.md
Normal file
115
skills/shader-dev/techniques/camera-effects.md
Normal 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)
|
||||
531
skills/shader-dev/techniques/cellular-automata.md
Normal file
531
skills/shader-dev/techniques/cellular-automata.md
Normal 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)
|
||||
380
skills/shader-dev/techniques/color-palette.md
Normal file
380
skills/shader-dev/techniques/color-palette.md
Normal 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)
|
||||
491
skills/shader-dev/techniques/csg-boolean-operations.md
Normal file
491
skills/shader-dev/techniques/csg-boolean-operations.md
Normal 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)
|
||||
333
skills/shader-dev/techniques/domain-repetition.md
Normal file
333
skills/shader-dev/techniques/domain-repetition.md
Normal 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)
|
||||
414
skills/shader-dev/techniques/domain-warping.md
Normal file
414
skills/shader-dev/techniques/domain-warping.md
Normal 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)
|
||||
1175
skills/shader-dev/techniques/fluid-simulation.md
Normal file
1175
skills/shader-dev/techniques/fluid-simulation.md
Normal file
File diff suppressed because it is too large
Load Diff
436
skills/shader-dev/techniques/fractal-rendering.md
Normal file
436
skills/shader-dev/techniques/fractal-rendering.md
Normal 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)
|
||||
527
skills/shader-dev/techniques/lighting-model.md
Normal file
527
skills/shader-dev/techniques/lighting-model.md
Normal 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)
|
||||
455
skills/shader-dev/techniques/matrix-transform.md
Normal file
455
skills/shader-dev/techniques/matrix-transform.md
Normal 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)
|
||||
922
skills/shader-dev/techniques/multipass-buffer.md
Normal file
922
skills/shader-dev/techniques/multipass-buffer.md
Normal 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)
|
||||
318
skills/shader-dev/techniques/normal-estimation.md
Normal file
318
skills/shader-dev/techniques/normal-estimation.md
Normal 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)
|
||||
1203
skills/shader-dev/techniques/particle-system.md
Normal file
1203
skills/shader-dev/techniques/particle-system.md
Normal file
File diff suppressed because it is too large
Load Diff
623
skills/shader-dev/techniques/path-tracing-gi.md
Normal file
623
skills/shader-dev/techniques/path-tracing-gi.md
Normal 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)
|
||||
373
skills/shader-dev/techniques/polar-uv-manipulation.md
Normal file
373
skills/shader-dev/techniques/polar-uv-manipulation.md
Normal 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)
|
||||
788
skills/shader-dev/techniques/post-processing.md
Normal file
788
skills/shader-dev/techniques/post-processing.md
Normal 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)
|
||||
346
skills/shader-dev/techniques/procedural-2d-pattern.md
Normal file
346
skills/shader-dev/techniques/procedural-2d-pattern.md
Normal 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)
|
||||
554
skills/shader-dev/techniques/procedural-noise.md
Normal file
554
skills/shader-dev/techniques/procedural-noise.md
Normal 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)
|
||||
467
skills/shader-dev/techniques/ray-marching.md
Normal file
467
skills/shader-dev/techniques/ray-marching.md
Normal 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)
|
||||
631
skills/shader-dev/techniques/sdf-2d.md
Normal file
631
skills/shader-dev/techniques/sdf-2d.md
Normal 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)
|
||||
589
skills/shader-dev/techniques/sdf-3d.md
Normal file
589
skills/shader-dev/techniques/sdf-3d.md
Normal 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)
|
||||
100
skills/shader-dev/techniques/sdf-tricks.md
Normal file
100
skills/shader-dev/techniques/sdf-tricks.md
Normal 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)
|
||||
776
skills/shader-dev/techniques/shadow-techniques.md
Normal file
776
skills/shader-dev/techniques/shadow-techniques.md
Normal 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)
|
||||
1542
skills/shader-dev/techniques/simulation-physics.md
Normal file
1542
skills/shader-dev/techniques/simulation-physics.md
Normal file
File diff suppressed because it is too large
Load Diff
490
skills/shader-dev/techniques/sound-synthesis.md
Normal file
490
skills/shader-dev/techniques/sound-synthesis.md
Normal 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)
|
||||
408
skills/shader-dev/techniques/terrain-rendering.md
Normal file
408
skills/shader-dev/techniques/terrain-rendering.md
Normal 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)
|
||||
121
skills/shader-dev/techniques/texture-mapping-advanced.md
Normal file
121
skills/shader-dev/techniques/texture-mapping-advanced.md
Normal 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)
|
||||
382
skills/shader-dev/techniques/texture-sampling.md
Normal file
382
skills/shader-dev/techniques/texture-sampling.md
Normal 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)
|
||||
375
skills/shader-dev/techniques/volumetric-rendering.md
Normal file
375
skills/shader-dev/techniques/volumetric-rendering.md
Normal 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)
|
||||
458
skills/shader-dev/techniques/voronoi-cellular-noise.md
Normal file
458
skills/shader-dev/techniques/voronoi-cellular-noise.md
Normal 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)
|
||||
985
skills/shader-dev/techniques/voxel-rendering.md
Normal file
985
skills/shader-dev/techniques/voxel-rendering.md
Normal 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)
|
||||
490
skills/shader-dev/techniques/water-ocean.md
Normal file
490
skills/shader-dev/techniques/water-ocean.md
Normal 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)
|
||||
170
skills/shader-dev/techniques/webgl-pitfalls.md
Normal file
170
skills/shader-dev/techniques/webgl-pitfalls.md
Normal 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.
|
||||
Reference in New Issue
Block a user