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