Updating the OpenPBR-viewer (WebGL pathtracer + rasterizer) to fully support OpenPBR v1.2 spec.
Full plan at: .claude/plans/refactored-juggling-snowglobe.md
Reference document: ~/dev/light_transport/s2025_openpbr/arxiv/course_notes.tex
Reference code: ~/dev/light_transport/s2025_openpbr/arxiv/code/
- File:
glsl/pathtracing/openpbr_surface.glsl - Rewrote the function which had duplicated/interleaved code from thin-walled weight logic
- Proper CDF lobe selection, per-lobe sampling, MIS PDF computation, throughput calculation
- 2.1 PR #253 — Coat darkening fix
- Files:
openpbr_surface.glsl,openpbr.frag.glsl - Corrected to match reference
coat_darkening.cpp:base_darkening = mix(1, Delta, coat_darkening)withDelta = max(1-K,0) / (1 - E_base*K*coat_color) - Rasterizer was also missing
coat_colorin Delta denominator — fixed
- Files:
- 2.2 PR #256 — Clamp negative metallic Fresnel (
FresnelF82Tint/FresnelConductorF82)- Files:
main.glsl,openpbr.frag.glsl
- Files:
- 2.3 PR #277 — Input parameter clamping per spec section 4
- Files:
openpbr_surface.glsl,openpbr.frag.glsl
- Files:
- 2.4 PR #286 —
transmission_scatteras volume albedo (simpler parametrization)- Files:
openpbr_surface.glsl
- Files:
- 2.5 PR #247 —
specular_weightas decoupled IOR viaFresnelDielectricReflectanceModulated- Already implemented; verified correctness and added PR hyperlink comments
- File:
glsl/pathtracing/openpbr_surface.glsl - Fixed layering to maintain fuzz + coat layers in thin-walled mode
- Added
ID_DIFF_BTDFlobe for thin-walled diffuse transmission - Fixed evaluation and sampling functions for thin-walled branches
- 4.1a: Complete rewrite of
glsl/pathtracing/thin-film.glsl- Replaced per-channel Belcour & Barla with hero wavelength Airy summation
- Reference: Kutz & Portsmouth, "Thin-Film Iridescence via Complex Arithmetic"
- Python reference:
C:/Users/jamportz/dev/light_transport/thin-film-paper/code/airy_complex.py - Key functions:
conductor_ior_at_wavelength(),FresnelThinFilm()(scalar at wavelength_nm) - Wrapper API unchanged:
FresnelThinFilmOverDielectric/Conductorstill return vec3
- 4.1b: Added thin-film integration to
glsl/pathtracing/specular_brdf.glsl#ifdef THIN_FILM_ENABLEDblocks in both evaluate and sample functions- Blends modulated Fresnel with thin-film Fresnel via
thin_film_weight
- 4.1c: Updated
glsl/pathtracing/pathtracer.glslspectral rendering- Wavelength sampling guard:
#if defined(TRANSMISSION_ENABLED) || defined(THIN_FILM_ENABLED) - Spectral weight at path start:
throughput *= xyzToRgb(xyzFit_1931(wavelength_nm)) * SPECTRAL_NORM - Guard against double spectral weight when both thin-film and dispersion active
- Wavelength sampling guard:
- Added textured ground plane to pathtracer matching the rasterizer's existing ground mesh
- Analytical ray-plane intersection in
pathtracer.glsltrace() - Ground BRDF (Lambertian with texture lookup) in
pathtracer.glsl - UV mapping:
vec2(pW.x, -pW.z) / 200.0 * 2.0 + 0.5to match rasterizer's PlaneGeometry repeat/offset - Ground texture upgraded to 7500x7500 from USD-WG StandardShaderBall asset (ACEScg EXR → sRGB PNG)
- Async compilation fix:
trigger_recompile()now compiles FullScreenQuad material viarenderer.compileAsync(pathtracedQuad._mesh, tmp_cam)— prevents browser freeze - Warm-up render: Added post-compilation warm-up render to flush GPU pipeline stalls
- Loop bounds as uniforms: Converted
BOUNCESandMAX_VOLUME_STEPSfrom#definetouniform int— changing these in GUI no longer triggers recompilation - BVH stack reduced:
stack[60]→stack[32]inpathtracer.glsl(32 levels supports ~4B leaves)
- Coat BRDF DV denominator: Added
abs()on denominator incoat_brdf_evaluate(line 49) - PR hyperlinks: Added full GitHub URLs for PRs #247, #253, #256, #277, #286 across all modified files
- Removed accidentally committed
OpenPBR/submodule from history viagit filter-repo - Removed large env maps (etzwihl_16k.hdr, metro_vijzelgracht_16k.hdr/.png) from history
- File:
glsl/rasterization/openpbr.frag.glsl - Port thin-film using same Airy formula at 3 fixed wavelengths (R=700nm, G=546.1nm, B=435.8nm)
- Apply to both specular and metal BRDF evaluations
- Needs complex arithmetic helpers,
FresnelDielectricReflectionPolarizations,FresnelDielectricTransmissionPolarizations - Also needs
FresnelConductorReflectionPolarizationsfor metal substrate
- New params:
specular_haze,specular_haze_roughening - Blend two GGX lobes in specular/metal BRDFs
- Guard with
#ifdef HAZE_ENABLED - Files:
specular_brdf.glsl,specular_btdf.glsl,metal_brdf.glsl,main.glsl,openpbr.frag.glsl
- New param:
specular_retroreflectivity - Modify specular/metal BRDFs with retroreflective component
- Guard with
#ifdef RETRO_ENABLED
- Add new uniform params (haze, retroreflectivity)
- Add
#definetoggles (HAZE_ENABLED,RETRO_ENABLED) - Add GUI controls in appropriate folders
- Pathtracer lobe IDs:
ID_FUZZ_BRDF=0, ID_COAT_BRDF=1, ID_META_BRDF=2, ID_SPEC_BRDF=3, ID_SPEC_BTDF=4, ID_DIFF_BRDF=5, ID_DIFF_BTDF=6, ID_SSSC_BTDF=7(NUM_LOBES=8) - Rasterizer lobe IDs:
ID_FUZZ_BRDF=0, ID_COAT_BRDF=1, ID_META_BRDF=2, ID_SPEC_BRDF=3, ID_SPEC_BTDF=4, ID_DIFF_BRDF=5, ID_SSSC_BTDF=6(NUM_LOBES=7, no DIFF_BTDF) - Spectral rendering:
SPECTRAL_NORMconstant normalizes CIE→RGB so E[spectral_weight] ≈ (1,1,1) for uniform λ∈[360,700]nm - Thin-film API:
FresnelThinFilmOverDielectric(mui, eta_fe)/FresnelThinFilmOverConductor(mui, eta_fe)— both return vec3 #ifdefguards:THIN_FILM_ENABLED,TRANSMISSION_ENABLED,SUBSURFACE_ENABLEDcontrolled bymain.js- Rasterizer uses struct-based approach:
OpenPBRMaterial pbrpassed to all functions; pathtracer uses global uniforms directly - Coat darkening reference:
~/dev/light_transport/s2025_openpbr/arxiv/code/coat_darkening.cpp - coat_color = T²_coat: The square of coat transmittance at normal incidence. absorb_tint = pow(coat_color, 0.5*(1/μᵢ + 1/μₒ))
- Pathtracer has
specular_weightin both lobe weight computation ANDFresnelDielectricReflectanceModulated, while rasterizer only has it in Fresnel. Potential double-counting in pathtracer (out of scope for this update).