Files
skills/shader-dev/techniques/shadow-techniques.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

777 lines
25 KiB
Markdown

# 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)