-
Concept: Next Event Estimation (NEE)
-
The Problem Without NEE
- Pure path tracing: bounce rays randomly until they hit a light
- For small light sources: extremely rare to hit by chance → very high variance
- Example: a point light — probability of random ray hitting it = 0
- NEE solves this by explicitly connecting each path vertex to a light
-
How NEE Works
- At each path vertex, in addition to the bounce ray:
- Sample a point
y on a light source
- Trace a shadow ray from hit point to
y
- If unoccluded: add direct lighting contribution
- Direct lighting contribution
L_direct = f_r(ω_o, ω_light) * L_e(y) * G(x, y) * V(x, y) / p_light(y)
G(x, y) = cos(θ_x) * cos(θ_y) / r² — geometry term
V(x, y) — visibility (0 or 1 from shadow ray)
p_light(y) — PDF of sampling point y on the light
-
Avoiding Double Counting
- Without correction: direct lighting counted twice
- Once by NEE (explicit light sampling)
- Once when bounce ray happens to hit the light
- Solution 1: Never count emission from hit surfaces (only NEE)
- Simple but biased — misses specular reflections of lights
- Solution 2: MIS (Multiple Importance Sampling)
- Weight NEE contribution by
w_nee = p_light² / (p_light² + p_brdf²)
- Weight BRDF contribution by
w_brdf = p_brdf² / (p_light² + p_brdf²)
- Unbiased — handles both diffuse and specular correctly
- See PathTracer Learning - Concept - MIS
-
Shadow Ray
- Origin: hit point + offset along normal
- Direction:
normalize(light_point - hit_point)
t_max = distance(hit_point, light_point) * 0.999 — don’t overshoot
- Flags:
gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsSkipClosestHitShaderEXT
- Only need occlusion — skip shading
- If shadow ray hits anything:
V = 0, skip contribution
- Offset magnitude:
0.001 or 1e-4 — balance between self-intersection and light leaking
-
Multiple Lights
- With many lights: which light to sample?
- Uniform random: pick one light, multiply by light count
L_direct = N_lights * f_r * L_e * G * V / p_light
- Weighted by power: sample proportional to light power
- Reduces variance for scenes with lights of very different intensities
- Build a CDF over light powers, sample with inverse CDF
- PathTracer Learning - ReSTIR — optimal light sampling with many lights
- Handles thousands of lights efficiently
-
NEE for Area Lights
- Sample uniform point on triangle light:
y = (1-√ξ_1)*v0 + √ξ_1*(1-ξ_2)*v1 + √ξ_1*ξ_2*v2
- PDF:
p(y) = 1 / area
- Convert to solid angle:
p(ω) = p(y) * r² / |cos(θ_light)|
- Degenerate case:
cos(θ_light) ≈ 0 (grazing angle) → very high PDF → clamp
- Also check:
dot(light_normal, -shadow_dir) > 0 — light must face the hit point
-
NEE for Environment Maps
- Sample direction from environment map CDF (proportional to luminance)
- Trace shadow ray in that direction with
t_max = infinity
- If no hit: add environment contribution
- PDF:
p(ω) = luminance(L(ω)) / total_luminance * (W*H) / (2π² * sin(θ))
- See PathTracer Learning - Concept - Environment Map
-
NEE for Emissive Triangles
- Collect all emissive triangles into a light list at scene load
- Sample one triangle (uniform or weighted by area × emission)
- Sample a point on that triangle
- Compute PDF:
p(ω) = (1/N_lights) * (1/area) * r² / cos(θ_light)
- Or:
p(ω) = (area_i / total_area) * (1/area_i) * r² / cos(θ_light) = r² / (total_area * cos(θ_light))