528 lines
17 KiB
Markdown
528 lines
17 KiB
Markdown
# 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)
|