Skip to content

[CRITICAL] PBR Lighting Energy Conservation: Sun emission missing π division, IBL not pre-filtered #230

Description

@MichaelFisher1997

Severity

CRITICAL - Direct lighting has ~3.14x excess energy causing over-bright surfaces

Summary

The PBR lighting system has two energy conservation violations:

  1. Sun emission is not divided by π before entering Cook-Torrance BRDF, causing ~3.14x energy excess in direct lighting
  2. Environment map (IBL) is not pre-convolved for roughness, causing incorrect ambient energy for non-diffuse surfaces

Problem 1: Sun Emission Missing π Division (CRITICAL)

Location

  • File: assets/shaders/vulkan/terrain.frag
  • Line: 452-453

Current Code

vec3 sunColor = global.sun_color.rgb * global.params.w * 4.0;
vec3 Lo = (kD * albedo / PI + specular) * sunColor * NdotL_final * (1.0 - totalShadow);

Issue

The BRDF diffuse term divides by π (albedo / PI), but the light source is not scaled accordingly. In physically-based rendering:

  • Diffuse BRDF is albedo / π
  • Light should be radiance * π (energy conserved)

OR:

  • Light remains as radiance
  • BRDF should NOT divide by π

Current implementation does both: light is radiance AND diffuse divides by π, resulting in ~3.14x too much energy.

Impact

  • Direct lighting term is 3.14x brighter than physically correct
  • Over-exposed surfaces in sunlit areas
  • Inconsistent energy balance with ambient lighting
  • Tone-mapping compensates but hides the root cause

Proposed Fix

Option A (Preferred - fix light source):

// Line 452: Divide sun color by π to match BRDF
vec3 sunColor = global.sun_color.rgb * global.params.w * 4.0 / PI;
vec3 Lo = (kD * albedo / PI + specular) * sunColor * NdotL_final * (1.0 - totalShadow);

Option B (Alternative - fix BRDF):

// Line 452: Keep light as radiance
vec3 sunColor = global.sun_color.rgb * global.params.w * 4.0;
// Line 453: Remove π from diffuse term (light is already irradiance)
vec3 Lo = (kD * albedo + specular) * sunColor * NdotL_final * (1.0 - totalShadow);

Recommendation: Option A - keeps consistent with standard PBR implementations where light sources are irradiance.

Sun Color Reference Values

From src/engine/atmosphere/sky_palette.zig:35-38 (after toLinear() conversion):

  • day_sun: ≈ (1.0, 0.88, 0.77) × 4.0 = (4.0, 3.5, 3.1)
  • dawn_sun: ≈ (1.0, 0.68, 0.30) × 4.0 = (4.0, 2.7, 1.2)
  • night_sun: ≈ (0.0014, 0.0014, 0.01) × 4.0 = (0.006, 0.006, 0.04)

Problem 2: IBL Environment Map Not Pre-Filtered

Location

  • File: assets/shaders/vulkan/terrain.frag
  • Line: 456-457, 572

Current Code

vec2 envUV = SampleSphericalMap(normalize(N));
vec3 envColor = textureLod(uEnvMap, envUV, 8.0).rgb;

Issue

The environment map is sampled at a fixed mip level (8.0) regardless of surface roughness. For energy-conserving IBL:

  • Rough surfaces need pre-convolved radiance (blurrier, higher mip level)
  • Smooth surfaces need sharp radiance (lower mip level)

Current fixed mip level causes:

  • Incorrect ambient energy for glossy/metallic surfaces
  • Loss of material detail on rough surfaces
  • Inconsistent reflection blur matching roughness

Impact

  • Secondary issue (less critical than sun emission)
  • Ambient lighting quality degradation for PBR materials
  • Especially noticeable on smooth surfaces that should have sharp reflections

Proposed Fix

  1. Generate pre-filtered envmap (offline or at load time):

    • For each mip level, convolve with importance sampling using roughness matching that mip
    • Store as separate texture or texture array
  2. Update shader to use roughness for mip selection:

float roughness = clamp(packedPBR.r, 0.05, 1.0);
float envMipLevel = roughness * MAX_ENV_MIPS;
vec3 envColor = textureLod(uEnvMap, envUV, envMipLevel).rgb;

Verification Steps

After implementing fixes:

  1. Render a white Lambertian sphere facing the sun
  2. Verify luminance ≈ π (in linear space) at surface facing the sun directly
  3. Check that shadows don't cause ambient > direct lighting
  4. Validate IBL reflections blur correctly with roughness
  5. Compare tone-mapped output with reference PBR renders

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions