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