376 lines
12 KiB
Markdown
376 lines
12 KiB
Markdown
# 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)
|