Skip to content

[Materials] Add a shared energy-conserving layered PBR model #134

Closed

Description

@proggeramlug

Parent: #126
Inventory: #11 material/BRDF section

Problem

The current PBR path covers core metallic/roughness rendering, but UE-class look development needs composable, energy-conserving material lobes rather than one-off shader forks. glTF extensions such as clearcoat, sheen, anisotropy, iridescence, specular, and IOR are also common interoperability requirements.

Outcome

A versioned layered-PBR material model shared by deferred/visibility, forward transparent, path-traced, and reference-render validation paths.

Minimum first release:

  • base metallic/roughness dielectric/conductor;
  • clearcoat with independent normal/roughness;
  • sheen;
  • tangent-space anisotropy;
  • specular/IOR controls;
  • iridescence/thin-film lobe;
  • emissive energy represented consistently for exposure and GI.

Screen-space subsurface profiles may be added as a separate milestone only after the core lobe ABI is stable.

Material ABI

  • Version the GPU material record; declare exact alignment/packing once for Rust and WGSL.
  • Use a lobe/feature bitmask plus indices into texture/resource tables.
  • Centralize BRDF functions in shared WGSL included by every relevant pipeline.
  • Preserve a compact fast path for common base-only materials; do not branch every pixel through all lobes if no lobe is active.
  • Translate glTF extensions without losing texture transforms, factors, color spaces, or tangent requirements.
  • Invalid/missing tangent data must select a documented fallback or emit an actionable import warning.

Physical correctness rules

  • Enforce energy compensation between base specular/diffuse and added lobes.
  • Use consistent normal orientation, geometric normal safeguards, and roughness floors.
  • Filter normal variance for minification; do not let added lobes reintroduce sparkle/aliasing.
  • Match Fresnel/IOR conventions across realtime and reference/path-traced implementations.
  • Explicitly define multiple-scattering approximation and reciprocity assumptions.

Implementation sequence

  1. Write a CPU/reference evaluation and sphere-matrix corpus for the base model.
  2. Introduce versioned lobe ABI without visual change.
  3. Land clearcoat + IOR/specular and validate energy.
  4. Land sheen and anisotropy with correct tangent handling.
  5. Land iridescence with bounded parameter ranges.
  6. Wire import, serialization, authoring API, debug views, and path-tracer parity.

Each lobe should be a reviewable commit/PR slice with its own tests even if one agent owns the whole issue.

Acceptance criteria

  • A generated parameter matrix compares each lobe against tools/bloom-reference or Cycles at multiple view/light angles; thresholds and known model differences are recorded.
  • White-furnace tests show no unexplained energy gain across roughness, metallic, IOR, and lobe-weight sweeps.
  • Deferred/visibility, forward, and path-traced versions agree within the approved model tolerance.
  • Canonical glTF extension assets import with correct factors/textures and no silent fallback.
  • Anisotropy orientation follows mesh tangents and survives negative scale/mirrored UV cases.
  • Normal-map minification does not create temporal sparkle in the motion corpus.
  • Base-only material cost does not regress by more than an approved measured budget; attach shader stats and pass timings.
  • Public APIs remain simple: useful defaults and one material descriptor, not a required sequence of low-level FFI calls.

Likely files

  • native/shared/src/renderer/material_pipeline.rs, material_system.rs, shaders/
  • native/shared/shaders/material_abi.wgsl
  • native/shared/src/models_gltf.rs
  • tools/bloom-reference/src/tracer.rs
  • path-tracer shaders under native/shared/src/renderer/shaders/pt.rs
  • src/models/ public material types

Non-goals

  • A node-based material editor.
  • Strand hair, eye geometry, or a full BSSRDF in the first release.
  • Maintaining two mathematically unrelated BRDF implementations.

Dependencies

Activity

  1. proggeramlug commented on Jul 26, 2026

    @proggeramlug
    ContributorAuthor

    Implemented the first #134 slice: an image-neutral, versioned base-BRDF
    target evaluator with deterministic white-furnace and parameter-matrix
    oracles
    .

    This package deliberately does not connect the new equations to any live or
    scene-rendering shader yet. An exploratory connection changed broad diffuse
    regions, so that migration is deferred until it has an external
    Cycles/reference review rather than silently replacing an image model under
    the no-regression rule.

    Version-1 target contract

    Added tools/bloom-reference/src/layered_pbr.rs, a pure evaluator defining:

    • linear base color, metallic weight, and perceptual roughness;
    • the existing 0.04 dielectric F0 and 0.04 roughness floor;
    • Schlick Fresnel;
    • GGX/Trowbridge-Reitz distribution;
    • height-correlated Smith visibility with the squared-cosine form;
    • energy-normalized reciprocal Burley diffuse;
    • reciprocal diffuse interface transmission
      (1 - F(NdotV)) (1 - F(NdotL));
    • explicit diffuse, specular, BRDF * NdotL, and current mixture-PDF outputs.

    The evaluator is side-effect-free and has no renderer, texture, scene,
    tone-map, graph, or GPU dependency.

    Defects exposed before live migration

    The current independent CPU/GPU path-tracing copies use

    sqrt((NdotV * (1 - alpha²) + alpha²) * NdotV)

    instead of the correlated-GGX squared-cosine form

    sqrt(NdotV² * (1 - alpha²) + alpha²).

    The old equation reports about 2.06 units for a rough white conductor at
    NdotV = 0.1 in a unit white furnace. After fixing that in the target
    evaluator, the old diffuse split also exposed grazing energy stacking:
    specular approached one while diffuse retained nearly full response.

    Version 1 therefore uses the squared Smith equation, the roughness-dependent
    Burley 1 -> 1/1.51 normalization, and symmetric view/light interface
    transmission. These are now pinned independently of any whole-image golden.

    Deterministic corpus

    Added the companion bloom-brdf-reference binary and checked-in
    tools/bloom-reference/reference/layered-pbr-v1.json.

    The 48 cases span:

    • neutral and saturated-red base colors;
    • dielectric and conductor endpoints;
    • roughness 0.04, 0.25, 0.5, and 1.0;
    • NdotV 0.1, 0.5, and 1.0.

    Each row records separate direct diffuse/specular, BRDF * NdotL, MIS PDF,
    and deterministic white-furnace directional reflectance. Serialization rounds
    to six decimals; tests evaluate full precision. The checked matrix SHA-256 is
    9b8b465ecf2349b6c814210ab9c11190e1ea1a9b67f4698b56ac6c252b15b91b.

    The furnace oracle uses deterministic GGX visible-normal importance sampling
    for specular and cosine sampling for diffuse. This avoids false gain from
    uniform quadrature aliasing at near-delta roughness. The checked 48-case
    matrix's maximum furnace value is 0.917948.

    Tests

    The companion binary has four passing tests:

    1. finite/non-negative and reciprocal output over the material/view matrix;
    2. bounded white-furnace energy including white dielectric/conductor,
      intermediate metallic, four roughnesses, and four view angles;
    3. one documented invalid-parameter clamping contract;
    4. exact regeneration of the checked-in version-1 JSON.

    The existing bloom-reference scene binary retains its two passing built-in
    scene/sky tests. Focused clippy with -D warnings passes for the new binary.
    The full legacy tool clippy invocation still reports its five pre-existing
    main/tracer lints; none is in the new modules.

    Exact image and performance neutrality

    No realtime shader, material ABI, renderer path, render graph, draw, image,
    allocation, or platform code changed. The existing CPU scene tracer also does
    not import the new evaluator yet.

    Preserved pre-package and current binaries rendered the built-in PT scene at
    384x384, 256 spp, four bounces, fixed camera/seed, in
    old / new / new / old order:

    • all four PNGs are byte-identical:
      50579c0528ea96ea00c1ffec943737cbc44fe277c7cd0aae5561e659786fe35b;
    • old mean render time: 3.705 s;
    • current mean render time: 3.331 s (-10.08%);
    • a separate 192x192/128-spp strict comparison is also byte-identical:
      SSIM 1.0, RMSE/OKLab/edge delta 0.

    The favorable timing movement is recorded only as evidence of no regression;
    this package claims no scene-tracer optimization because its render code is
    semantically unchanged.

    Static gates and structure

    • cargo test --manifest-path tools/bloom-reference/Cargo.toml: passed
      (4 + 2 tests across the two binaries);
    • new-binary clippy with -D warnings: passed;
    • cargo fmt --check: passed;
    • git diff --check: passed;
    • new source modules are below policy (layered_pbr.rs: 331 lines,
      brdf_reference.rs: 154 lines);
    • the repository line ratchet still reports the same six cumulative
      renderer/legacy overages and no new violation.

    Documentation is in docs/layered-pbr.md and tools/README.md. Evidence is
    under tools/quality/out/issue-134-base-reference/.

    Next slice

    The next safe package is the versioned GPU material-record/lobe-bit ABI with
    the bitmask at zero for every existing material. It must prove byte-identical
    base images and unchanged base-only shader/pipeline cost before clearcoat,
    specular/IOR, sheen, anisotropy, or iridescence is activated.

  2. proggeramlug commented on Jul 26, 2026

    @proggeramlug
    ContributorAuthor

    Implemented #134 sequence step 2: a versioned lobe ABI that is inert for
    every existing material and adds no base-path GPU work
    .

    This is intentionally an ABI/compatibility package, not a visible material
    change. It gives clearcoat, sheen, anisotropy, iridescence, specular/IOR, and
    transmission stable identities before any realtime shader is allowed to
    evaluate them.

    Exact packing

    Added native/shared/src/renderer/layered_pbr.rs as the Rust packing oracle:

    • material-record version: 1;
    • global GpuMaterialRecord.header.y: high 8 bits = version, low 24 bits =
      lobe mask;
    • bound/custom MaterialFactors.foliage_params.zw: exact u32 version/mask
      bit patterns stored through f32::from_bits and read with WGSL bitcast;
    • reserved mask assignments:
      • bit 0 clearcoat;
      • bit 1 sheen;
      • bit 2 anisotropy;
      • bit 3 iridescence;
      • bit 4 specular/IOR;
      • bit 5 transmission.

    Every established material is version 1 with mask zero. Global version-zero
    or unknown-version input is normalized to version 1/mask zero on allocation
    and update, so data from the previously unused flags lane cannot silently
    activate a future lobe.

    No record/layout growth

    The two existing representations reuse reserved words:

    • global storage record: exactly 176 bytes before and after;
    • bound/custom UBO: exactly 80 bytes before and after;
    • no new bind-group entry or layout version;
    • Tier C retains custom material ABI v3;
    • foliage setters continue modifying only foliage_params.xy, preserving the
      metadata words in zw.

    material_abi.wgsl and material_indirection.wgsl now expose named
    version/mask constants and accessors. No production shader calls those
    accessors in this package, so mask zero does not add a shader load or branch.

    Runtime observability

    Quality telemetry now reports renderer_paths.layered_pbr:

    • bound/global record versions;
    • known/default lobe masks and active-lobe material count;
    • both exact record sizes;
    • added base material bytes/branches, graph passes, and image bytes.

    The final PBR-sphere control reports version 1, both default masks 0,
    active count 0, record sizes 80/176, and all four added-cost fields 0.

    Exact visual and graph neutrality

    A preserved pre-ABI PBR-spheres binary and the current binary were run
    old/new/new/old at 512x512, quality preset 3, render scale 1, 120 warmup and
    240 measured frames. A final rebuilt current binary was captured once more
    after the source-layout cleanup.

    All five PNGs are byte-identical:

    5e20cb864081a0d179d49bfcc012428485e18f2330a939f787c57c0063da6558

    Every run also retained:

    • graph plan 7ead8d0477965a86;
    • 24 logical passes;
    • one compiled/cached plan;
    • zero physical transient slots;
    • zero transient/unaliased transient bytes.

    The ABI therefore produces no tolerance-level drift: it preserves the exact
    encoded PNG.

    Counterbalanced timing evidence

    Averages of the old pair versus the new pair:

    metric old new delta
    CPU mean 6.699 ms 6.180 ms -0.518 ms
    CPU p95 26.424 ms 17.842 ms -8.583 ms
    GPU mean 28.421 ms 29.052 ms +0.630 ms
    GPU p95 32.293 ms 32.133 ms -0.160 ms
    wall mean 9.954 ms 9.438 ms -0.516 ms

    The GPU-mean movement is below the repository's 1.0 ms same-machine absolute
    reproducibility allowance; the other throughput and tail measurements do not
    regress. Large CPU-tail variation appeared in alternating old and new runs,
    so no optimization is claimed. Structurally the live shader has no new
    instruction, read, or branch.

    Tests and targets

    • shared library: 232 passed, 0 failed, 1 ignored;
    • golden/live GPU tests: 18 passed, 0 failed, 2 ignored;
    • render-target tests: 3 passed, 0 failed;
    • native cargo check --features models3d: clean;
    • folded Web wrapper, wasm32, models3d: passed (no new layered-ABI warning);
    • Android aarch64, NDK clang, models3d: passed;
    • FFI parity: all platforms cover the 485-function manifest, 0 failures,
      0 warnings
      ;
    • cargo fmt --check: passed;
    • git diff --check: passed.

    The line policy still reports the same six cumulative legacy/renderer
    overages. The new ABI module is 126 lines and introduces no new over-limit
    file.

    Documentation is in docs/layered-pbr.md and
    docs/material-texture-indirection.md. Reproducible artifacts are under
    tools/quality/out/issue-134-lobe-abi/.

    Next slice

    The next package can activate clearcoat plus specular/IOR behind the reserved
    bits. It will first extend the CPU/reference matrix and public/import records,
    then add a base-only shader specialization so mask-zero materials remain on
    the exact established code path. Clearcoat energy compensation, roughness,
    normal handling, and white-furnace evidence will be qualified before the lobe
    is enabled in a general scene.

  3. proggeramlug commented on Jul 26, 2026

    @proggeramlug
    ContributorAuthor

    Implementation package 3 — live clearcoat + specular/IOR qualified

    The version-2 clearcoat/specular/IOR contract is now live across the forward renderer without changing the ordinary-material fast path.

    What landed

    • Lazy layered scene specialization for opaque, prepassed, sorted BLEND, TAA-reactive BLEND, and weighted OIT.
    • Cached-model and retained SceneGraph routing, including deterministic pipeline switching and localized TEXCOORD_1 streams.
    • KHR_materials_clearcoat, KHR_materials_specular, and KHR_materials_ior factors, textures, texture transforms, UV0/UV1 selectors, and independent clearcoat normals.
    • Clearcoat normal textures now use the existing linear/vector-mip/variance upload path, preventing distant sparkle and color-space corruption.
    • Clustered-light and virtual-shadow source specialization now preserve the layered BRDF call.
    • Combined physical-transmission + layered-PBR specialization with a separate lazy ABI. Scalar, UV1, double-sided, native/folded scene inputs, bounded reflection, and temporal-reactive variants are covered.
    • Ordinary SCENE_SHADER, ordinary group-2 layout, base GPU-driven records, and base pipeline selection remain unchanged and free of layered bindings/calls.

    Energy and quality behavior

    • Explicit dielectric F0/F90 from specular color/factor and IOR, including exact IOR=0 compatibility.
    • Conductors remain isolated from dielectric specular controls.
    • Reciprocal clearcoat attenuation for direct light and squared interface attenuation for IBL/transmission composition.
    • Independent clearcoat normal and roughness, with LEADR/Toksvig baked variance plus screen-space curvature filtering.
    • Smooth finite highlight compression prevents clearcoat fireflies without hard clipping.
    • Combined transmission uses colored layered Fresnel and places clearcoat over the transmitted/reflected dielectric result.

    Qualification

    • cargo check --features models3d: pass, no warnings from this package.
    • Full library suite: 239 passed, 1 ignored, 0 failed.
    • Full headless GPU/golden suite: 19 passed, 2 ignored, 0 failed.
    • Existing lit 3D, clustered-light, TAA, PT progressive, PT realtime-motion, transmission, transparent GI, sorted transparency, and weighted OIT goldens remain unchanged.
    • New real-GPU coverage validates retained/cached parity plus opaque, sorted, reactive, weighted, combined transmission, and real UV1 layered texture routes.
    • All generated scalar/UV1, clustered/plain, virtual/cascade shadow, reactive, weighted, native/folded refraction variants parse through naga.
    • git diff --check: pass.
    • New/split source files remain below the 2,000-line ceiling; the repository line checker reports the same six cumulative pre-existing violations and no new violating file.

    Strict shared-crate clippy is still blocked by two unrelated pre-existing empty-doc-comment warnings in input.rs and renderer/mod.rs. The versioned reference binary clippy gate remains clean.

    Next package on this issue: sheen + tangent-space anisotropy, followed by iridescence, public authoring/debug surfaces, and path-tracing parity.

  4. proggeramlug commented on Jul 27, 2026

    @proggeramlug
    ContributorAuthor

    Implementation package 4 — sheen + tangent-space anisotropy qualified

    Sequence step 4 is now implemented and qualified across the live forward
    material paths, the CPU reference oracle, canonical glTF import, and the
    native/folded platform layouts.

    This package does not claim the issue's final public authoring/debug or
    path-tracer transport parity; those remain sequence step 6 after iridescence.

    Realtime material model

    • KHR_materials_sheen:
      • linear sheen color and perceptual roughness factors;
      • color and roughness textures with independent KHR_texture_transform;
      • UV0/UV1 selection;
      • Charlie distribution and visibility;
      • a checked 128x128 R16F directional-albedo LUT for direct/IBL energy
        compensation.
    • KHR_materials_anisotropy:
      • scalar strength and sin/cos rotation;
      • direction/strength texture with independent transform and UV set;
      • anisotropic GGX distribution and height-correlated visibility;
      • authored tangent-frame orientation, including tangent.w;
      • derivative TBN fallback for meshes without usable authored tangents;
      • anisotropic bent-normal IBL rather than direct-light-only anisotropy.
    • Sheen attenuates the undercoat with the directional-albedo oracle before
      adding its lobe. Clearcoat remains the top interface over sheen and the
      anisotropic base lobe.
    • The existing smooth finite highlight compression remains in place for
      temporally stable point/sun highlights.

    The specialization is live for retained and cached meshes across opaque,
    prepassed, sorted BLEND, TAA-reactive BLEND, weighted OIT, and the combined
    physical-transmission/refraction path. Scalar, UV1, double-sided,
    native/folded scene-input, and reactive refraction variants parse through
    Naga.

    The expanded layered layout uses one shared sampler for all nine material
    textures. This stays under Metal's sampler limit while preserving independent
    texture views and transforms.

    Fast paths and lazy cost

    The ordinary scene shader and ordinary material layout remain untouched.
    Base-only telemetry proves:

    • scene_specialization_initialized = false;
    • combined_refraction_specialization_initialized = false;
    • sheen_lut_initialized = false;
    • additional base material bytes, bindings, branches, graph passes, and image
      bytes are all 0;
    • the graph remains plan 7ead8d0477965a86, 24 passes, and zero transient
      bytes for the PBR-sphere control.

    Within the layered specialization, version-2 clearcoat/specular/IOR materials
    return before all sheen/anisotropy texture reads, UV transforms, derivatives,
    and LUT work when both new factors are zero. Anisotropy's derivative/TBN work
    is additionally guarded by positive anisotropy strength, so sheen-only
    materials do not pay for it.

    CPU/reference contract

    bloom-brdf-reference --version 3 now records a 30-row matrix spanning
    sheen color/roughness, anisotropy strength/rotation, clearcoat, metallic,
    roughness, and three view angles.

    • matrix SHA-256:
      0324c95b489f611888163df8ac879033313af6b9190e05af251db91aaca06bfa;
    • LUT SHA-256:
      c4433c235610212432e0da83a0106b8289f08beec9c2524fd82b677945235118;
    • furnace oracle: 96x192 deterministic anisotropic-GGX VNDF, Charlie,
      clearcoat, and cosine samples;
    • 90 recorded RGB furnace channels range from 0.030422 to 0.834041;
    • all 14 release tests pass, including reciprocity/finite/non-negative,
      rotation periodicity, LUT-vs-full-Charlie agreement, bounded furnace
      energy, and exact v1/v2/v3 regeneration;
    • focused release Clippy for bloom-brdf-reference passes with
      -D warnings.

    Canonical glTF import

    The import-only validator was run against the official Khronos sample assets:

    • CompareSheen.glb
      (70b6ab5ba8006aa71079b567783a81cfe778a11cca8abc44c45e5e0b1f4252cc):
      2 meshes, 1 active sheen material;
    • AnisotropyStrengthTest.glb
      (306321ff8214727b145188ceb56d96d2882b393d0b596fa089fd20b07e959834):
      50 meshes, 42 active anisotropy materials, 53,263 authored tangent
      vertices retained.

    Both staged and plain glTF loaders preserve factors, source texture metadata,
    runtime texture indices, texture transforms, requested UV sets, and authored
    tangents. Missing UV1/runtime texture data keeps the scalar material and emits
    the documented fallback instead of silently dropping the extension.

    Real-GPU tangent and composition coverage

    Metal tests exercise:

    • explicit mirrored tangent.w with a visible negative control;
    • negative-determinant model scale with a double-wound plane, compensated
      rotation parity, and a visible uncompensated control;
    • retained/cached parity;
    • opaque, sorted, reactive, and weighted transparency;
    • combined physical transmission/refraction;
    • real secondary-UV texture routing.

    The full release GPU suite passes 21 tests, 0 failures, 2 intentionally
    ignored hardware-oracle jobs
    . Existing lit/clustered/TAA, masked coverage,
    transmission, transparent-GI, PT progressive, and PT motion goldens remain
    unchanged; both PT captures compare exactly (mean = 0, SSIM = 1).

    Image and performance neutrality

    The package-4 base-only A/B produced eight byte-identical 512x512 captures:

    6053ceca7461ed2ac4c57917d58c6876048c73e1be9d8fcdf8869882615ddb64

    The relevant main_hdr_pass movement was 1.515 ms -> 1.591 ms
    (+0.075 ms), below the repository's 1 ms same-machine reproducibility
    allowance. CPU mean/p95 and wall mean improved in that counterbalanced set.
    The original total-GPU mean was rejected as evidence because unrelated host
    contention appeared almost entirely in bloom, not the material pass.

    That investigation found eleven persistent bloom parameter buffers being
    rewritten every frame plus a per-frame mip-dimension heap allocation. The
    buffers are now initialized on construction/resize, only threshold buffer
    zero is writable when manual exposure changes, and the mip extent is computed
    without allocation.

    A separate causal old/new/new/old qualification of the final bloom cleanup
    (four runs per binary, 120 warmup + 240 measured frames) remained
    byte-identical. Averages:

    metric before final delta
    GPU mean 15.496 ms 15.484 ms -0.012 ms
    GPU p95 16.750 ms 16.709 ms -0.041 ms
    bloom GPU 8.760 ms 8.731 ms -0.029 ms
    CPU mean 1.828 ms 1.833 ms +0.004 ms
    CPU p95 2.172 ms 2.196 ms +0.025 ms
    wall mean 4.726 ms 4.728 ms +0.002 ms

    The sub-0.025 ms CPU/tail movements are noise-level; GPU mean, GPU tail, and
    the implicated bloom pass all improve slightly. No optimization is claimed
    for the material lobe itself. The structural claim is narrower: base-only
    materials retain the ordinary shader and graph, version-2 layered materials
    retain an early exit, and the final candidate introduces no measurable
    rendering regression.

    The final rebuilt source capture is still the exact hash above and reports
    zero base layered cost.

    Final gates

    • shared library: 243 passed, 0 failed, 1 ignored;
    • release GPU/golden: 21 passed, 0 failed, 2 ignored;
    • render targets/resize: 3 passed, 0 failed;
    • canonical glTF validator: passed both official assets;
    • shared wasm32 folded layout: passed;
    • complete wasm-pack build --release --target web: passed;
    • Android arm64 NDK folded layout and wrapper: passed;
    • FFI parity: all platforms cover the 485-function manifest, 0 failures,
      0 warnings
      ;
    • package-4 touched paths add no strict-Clippy finding;
    • cargo fmt --check: passed;
    • git diff --check: passed.

    Repository-wide strict Clippy remains advisory and reports 162 cumulative
    legacy/roadmap findings outside the package-4 paths. The line ratchet still
    reports the same six cumulative over-limit files; every new/split package-4
    file is below 2,000 lines (largest: layered_pbr_scene.rs, 1,556).

    Documentation and reproduction commands are in docs/layered-pbr.md and
    tools/README.md; artifacts are under
    tools/quality/out/issue-134-sheen-anisotropy/.

    Next slice

    Sequence step 5 is iridescence/thin-film interference with bounded thickness
    and IOR ranges. It will extend the CPU matrix first, then activate a lazy
    realtime lobe behind bit 3, with furnace, angular hue-shift, image-neutral
    base-path, and cross-target gates before public/debug/transport parity.

  5. proggeramlug commented on Jul 27, 2026

    @proggeramlug
    ContributorAuthor

    Implementation package 5 — iridescence + stable HDR transport qualified

    Sequence step 5 is implemented on draft PR #147 at commit 570b811. This package activates KHR_materials_iridescence behind the existing lazy layered specialization and does not claim sequence-step-6 public authoring/path-tracer closure yet.

    Reference and physical contract

    • Version-4 bloom-brdf-reference adds bounded thin-film Fresnel for dielectric and conductor bases, clearcoat-over-film, and combined clearcoat/sheen/anisotropy/iridescence cases.
    • The checked 39-row matrix is tools/bloom-reference/reference/layered-pbr-v4.json, SHA-256 cc8b586bb69123be172987b5c974e1f001e9ef5df3f99df2687396a3f7ae7209.
    • All 18 layered reference tests pass, including finite/bounded spectral response, reciprocity, exact inactive-lobe equivalence, white-furnace energy bounds, and exact v1/v2/v3/v4 regeneration.

    Import and realtime paths

    • KHR_materials_iridescence factor, IOR, thickness range, factor/thickness textures, channels, UV selection, and texture transforms are preserved through plain/staged GLB import and retained/cached draws.
    • The official Khronos CompareIridescence.glb imports as two meshes with exactly one active iridescent material; factor 1.0, IOR 1.5, thickness 100..400 nm, and the factor texture are retained.
    • Opaque, prepassed, sorted, reactive, weighted, and combined physical-transmission variants share the same layered evaluator.
    • SSR gains a lazy full-resolution RGBA8 metadata target only when visible opaque iridescence exists. RGB carries raw thin-film Fresnel and alpha carries factor; ordinary SSR bindings and graph topology remain unchanged.
    • With SSR disabled, the base IBL iridescence remains visible, proving SSR is an optional reflection source rather than the owner of the material lobe.

    Firefly/root-cause fix

    The visible colored box/pinhole clusters were traced to high-variance GGX convolution of the 61k-radiance sun texels in the HDR panorama. A material-specific minimum-mip workaround was rejected and removed.

    The shared HDR prefilter now:

    • builds finite, f16-bounded, solid-angle-preserving source radiance mips;
    • preserves exact spherical energy for odd panorama extents;
    • selects source LOD per GGX sample from the sample PDF and exact equirectangular texel solid angle;
    • avoids a duplicate full-resolution float panorama and precomputes aggregation weights;
    • performs no per-frame work.

    The final production and prefiltered-environment captures remove the clustered thin-film fireflies without changing authored material roughness. The optimized builder is byte-identical to the initially accepted candidate.

    Ordinary-path and performance evidence

    An exact same-commit Damaged Helmet A/B, with only the new shared HDR prefilter absent/present, passes the renderer image gate:

    • luminance RMSE 0.01653;
    • SSIM 0.98803;
    • mean OKLab delta 0.00473;
    • edge delta 0.00176.

    The candidate preserves texture and silhouette detail while smoothing HDR reflection energy. Base-only PBR telemetry keeps layered specialization inactive, adds no normal-frame graph pass or branch, and reports CPU mean 2.161 ms, CPU p95 2.577 ms, GPU mean 15.665 ms, and GPU p95 16.866 ms within the scene budgets. HDR mip preparation occurs only when an environment is loaded.

    Gates

    • shared library: 260 passed, 0 failed, 1 ignored;
    • release layered reference: 18 passed, 0 failed; scene-reference tests: 2 passed;
    • macOS Metal release renderer build and deterministic real-GPU captures: pass;
    • shared renderer checks for Linux, Windows, and wasm32: pass;
    • WGSL prefilter and every layered/SSR specialization parse through Naga;
    • cargo fmt --check and git diff --check: pass.

    Remaining sequence step 6

    • one simple public layered-material descriptor and platform FFI parity;
    • public debug/telemetry surface rather than test-only shader capture selection;
    • path-traced transport parity and the final forward/path/reference comparison evidence.

    The issue remains open until those items and the cumulative branch gates are complete.

  6. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Issue #134 path-traced clearcoat-normal evidence

    This checkpoint closes the remaining independent-clearcoat-normal gap in
    Bloom's layered GPU path tracer. It is qualified at revision 5c0ab20 on an
    Apple M1 Max / Metal adapter with ray query and texture arrays enabled.

    Transport contract

    The path tracer now preserves the imported clearcoat-normal texture, UV set,
    KHR_texture_transform, and normal scale at primary and bounce intersections.
    It reconstructs the committed vertex tangent and handedness only for a
    qualified anisotropy or clearcoat-normal record, maps the coat normal without
    changing the base normal, and uses the mapped normal for:

    • direct-light next-event evaluation;
    • clearcoat importance sampling;
    • clearcoat PDFs and MIS;
    • reciprocal attenuation of the undercoat;
    • subsequent-bounce surface state.

    The mapped normal is constrained to the base geometric hemisphere. The
    normal-length/variance stored by the existing vector-mip upload path widens
    the coat roughness for minification. Path tracing deliberately does not use
    screen derivatives; realtime shading additionally retains its established
    screen-space curvature variance.

    On adapters without the complete texture-array feature pair, only the normal
    map is ignored. Scalar clearcoat factor and roughness remain active, and no
    normal or UV1 sidecar is allocated.

    Lazy resource cost

    Clearcoat-normal metadata has its own 48-byte-per-instance sidecar:

    • 16-byte header;
    • 2x2 UV transform matrix;
    • UV offset and normal scale.

    It uses group 2 binding 8, resource-key bit 7, and pipeline-key bit 9. The
    existing 64-byte clearcoat factor/roughness record and 96-byte scalar layered
    record do not grow. Base, scalar-clearcoat, and factor/roughness-textured
    scenes report:

    • path_tracing_clearcoat_normal_specialization_initialized = false;
    • path_tracing_clearcoat_normal_sidecar_allocated_bytes = 0.

    A normal-only clearcoat material allocates no factor/roughness texture
    sidecar. There is no added render-graph pass, image, base-material branch, or
    base-pipeline binding. The shared sidecar upload helper retains the established
    power-of-two capacity, dirty-upload, and bind-group invalidation behavior.

    Metal image gates

    The release ray-query golden runs the complete texture-array path rather than
    the former scalar-fallback path. Its fixture owns real per-face UVs locally,
    so unrelated transparent-GI geometry remains unchanged.

    The zero-scale flat normal is byte-identical to scalar clearcoat. A directional
    normal map produces a bounded response without mean display-energy gain:

    Comparison Mean RGB difference Maximum channel difference SSIM
    Flat vs directional normal 0.635595 146 0.980410
    Directional vs 90-degree UV rotation 0.538315 150 0.985408
    Directional UV0 vs UV1 0.601186 154 0.983375

    Flat and mapped mean display luminance are 84.161719 and 83.784545
    respectively. The existing layered transport gate supplies the finite,
    visible-response, and bounded-energy checks; its thresholds were not weakened.
    The rotation and UV1 controls retain their established minimum mean-RGB
    difference of 0.02.

    Enabling texture arrays exposed a previous golden-harness blind spot: the
    device requested ray query but not its supported texture-array feature pair,
    so older textured-lobe assertions always exercised the documented fallback.
    The harness now requests both supported features. All textured specular,
    clearcoat, sheen, iridescence, and anisotropy variants consequently execute on
    Metal. The anisotropy fixture was strengthened to full authored strength and a
    clear directional texture while retaining its original visibility threshold.

    Regression qualification

    ./scripts/ci-check.sh --quick passed in 23 seconds:

    • 328 shared unit tests passed, 1 intentionally ignored;
    • 57 runnable GPU goldens passed, 2 intentionally ignored;
    • all 4 render-target integration tests passed;
    • format, strict lint policy, FFI/schema parity, web arity, wasm32
      compilation, quality governance, visual fault controls, and canonical
      example inventory passed.

    The formerly affected
    physical_transmission_gi_specializations_run_on_hardware_ray_query control
    also passes after localizing the fixture UVs (20,050 affected pixels,
    RGB delta [28, 19, 247]).

    All clearcoat-normal WGSL feature combinations parse through Naga. The
    layered PT implementation remains below the repository's 2,000-line file
    limit (layered_pbr_pt.rs: 1,958 lines; shared resource helper: 34 lines;
    clearcoat-normal module: 328 lines).

    Commits

    • e69b23e — independent clearcoat-normal transport and qualification;
    • 8585425 — shared typed sidecar resource helper and line-policy split;
    • a44bc9b — real Metal texture-array golden coverage;
    • 5c0ab20 — fixture-local UVs preserving unrelated test geometry.

    Machine-readable measurements accompany this note in
    docs/evidence/issue-134-clearcoat-normal-pt.json.

  7. proggeramlug commented on Jul 28, 2026

    @proggeramlug
    ContributorAuthor

    Issue #134 layered-PBR qualification

    This checkpoint qualifies Bloom's shared layered material model against a
    generated angular oracle, the realtime forward renderer, the hardware
    ray-query path tracer, and a motion sequence. It is qualified at revision
    21356b8 on an Apple M1 Max / Metal adapter.

    Angular reference corpus

    bloom-brdf-reference --angular generates the checked
    tools/bloom-reference/reference/layered-pbr-angular-v1.json corpus. Its 63
    rows cover base, specular/IOR, clearcoat, sheen, anisotropy, iridescence, and
    combined materials at three view directions by three light directions per
    scenario. Every row records the material, directions, individual BRDF lobes,
    total BRDF * NdotL, PDF, and reciprocity error.

    Regeneration is byte-exact. CPU component and reciprocity tolerances are both
    3e-5; the measured maximum reciprocity error is zero. The corpus records five
    intentional comparison boundaries: it excludes environment lighting and
    post-processing, realtime finite-highlight compression is assessed in image
    space, normal minification belongs to the motion corpus, stochastic path
    transport uses converged-radiance tolerances, and iridescence uses Bloom's
    bounded Khronos approximation rather than a spectral conductor model.

    The checked corpus SHA-256 is
    110142a66bc22acaf0e7f6664a86f135b194fc62d203f1a92b685fbf2487f309.

    Renderer parity

    layered_pbr_parity selects the same v1-l2 record for every lobe, disables
    IBL, SSR, SSGI, SSAO, bloom, automatic exposure, motion blur, shadows, and
    environment energy, then compares the 32x32 center region of the forward MRT
    renderer and a 64-frame ray-query accumulation. It also compares each
    path-traced lobe response with the linear oracle direction.

    Lobe Forward/PT mean RGB MAE Final RGB cosine Luminance relative error PT/oracle response cosine
    Specular/IOR 3.7106 0.999774 0.0779 0.969229
    Clearcoat 6.5781 0.999883 0.1340 0.985841
    Sheen 5.5430 0.997531 0.0743 0.981640
    Anisotropy 20.4333 0.985835 0.2365 0.904537
    Iridescence 3.8477 0.999813 0.0854 0.994356
    Combined 8.3441 0.991555 0.1411 0.948319

    The gates require mean display RGB MAE at most 24, final RGB cosine at least
    0.96, relative display luminance error at most 0.30, and PT/oracle response
    cosine at least 0.85 for significant linear responses. Smaller responses are
    bounded to a maximum display response of 12. The worst measured values are the
    anisotropy row, and all retain useful margin without weakening a threshold.

    Clearcoat-normal motion stability

    layered_pbr_motion renders a high-frequency alternating tangent-space
    clearcoat normal through Bloom's vector/variance mip chain while the camera
    moves slowly, then subtracts the flat-normal control. The measured camera
    motion mean RGB is 2.432037, the filtered response mean is 0.205539, the
    maximum texture-specific adjacent-frame residual mean is 0.532235, and the
    coherent outlier-channel fraction is 0.0%.

    The hard sparkle gates are a maximum residual mean of 1.5 and maximum
    coherent outlier fraction of 2%. This test covers minification and temporal
    stability independently from the static clearcoat-normal qualification in
    issue-134-clearcoat-normal-pt.md.

    Regression and cost qualification

    ./scripts/ci-check.sh --quick --summary target/ci/quick-issue-134-final.json passed at the qualified revision in 25
    seconds:

    • 328 shared unit tests passed, 1 intentionally ignored;
    • 59 runnable GPU goldens passed, 2 intentionally ignored;
    • all 4 render-target integration tests passed;
    • strict lint, formatting, FFI/schema parity, web/wasm compilation, quality
      governance, visual-fault controls, and 20 canonical examples passed;
    • 19 release tests and strict Clippy passed for bloom-reference.

    The angular generator and both renderer gates add no shipping render pass,
    image, shader branch, GPU allocation, binding, or per-frame work. Their
    production runtime and binary shader cost is zero.

    Remaining cross-path dependency

    Bloom currently uses the forward MRT renderer; the visibility-buffer renderer
    tracked by issue #27 is not implemented. Consequently, issue #134's
    forward/visibility/path-traced parity item must remain open. When #27 lands,
    its material evaluator must consume this same checked corpus and satisfy the
    same documented parity contract. The generated-matrix and normal-minification
    acceptance items are independently complete.

    Commits

    • eca754e — clearcoat-normal motion stability gate;
    • ad97e77 — versioned layered-PBR angular reference corpus;
    • 21356b8 — forward, path-traced, and oracle parity gate.

    Machine-readable measurements accompany this note in
    docs/evidence/issue-134-layered-pbr-qualification.json.

  8. proggeramlug commented on Aug 8, 2026

    @proggeramlug
    ContributorAuthor

    Capability-contract checkpoint landed in PR #147 as 65a24b4.

    The visibility-shading compatibility test exposed that the lazy scene layered-PBR pipeline needs 22 sampled textures per fragment stage, while production device negotiation granted only the 19-texture core contract. The full audit also found the VSM variant needs 24 and combined layered refraction can need up to 30.

    The preferred high-end request now asks for the bounded optional budget the adapter can grant (22–30). The featureless/lower-tier fallback remains at 19. Every lazy specialization checks the exact granted limit before pipeline creation and falls back to base PBR or ordinary physical refraction instead of causing a wgpu validation failure. Capability and quality reports expose grant, requirement, availability, and initialization state.

    Verified locally on Metal:

    • 8 device-plan unit tests pass, including 24-texture bounded hardware, 19-texture high-end fallback, forced modern, and forced baseline.
    • Production negotiated device constructs with a 30-texture grant.
    • Visibility PBR runtime test passes with a layered compatibility draw.
    • Forward-vs-visibility capture remains within 1 LSB (89 / 81,920 changed channels, max delta 1).

    This removes a crash/fallback hole but does not complete the remaining forward/visibility/path-traced lobe parity criterion.

  9. proggeramlug commented on Aug 9, 2026

    @proggeramlug
    ContributorAuthor

    Progress in commit 40ff96e (fix(models): preserve textured specular glossiness):

    • Preserves KHR_materials_pbrSpecularGlossiness RGB specular + alpha glossiness textures instead of collapsing them to scalar metallic/roughness values.
    • Carries authored specular/glossiness factors through MeshData, scene attachment, compatibility uniforms, and the GPU material-indirection record.
    • Reuses the existing metallic/roughness texture binding and sample. Workflow 0 remains scalar, workflow 1 remains glTF metallic/roughness GB, and workflow 2 performs the Khronos conversion per pixel. Base metallic/roughness materials retain their previous shader branch.
    • Applies exact sRGB decoding to specular RGB while leaving glossiness alpha linear.
    • Covers ordinary, GPU-driven visibility, refractive, and transmitted-shadow material ABI variants.
    • Bistro audit: 109 materials carry specular/glossiness textures that were previously dropped.

    Validation:

    • 414 unit/shader tests passed, 1 ignored.
    • New importer mapping, 48-byte compatibility UBO, 192-byte storage-record ABI, workflow selection, and material-cache identity tests pass.
    • All generated ordinary, visibility-buffer, virtual-geometry, refractive, and transmitted-shadow WGSL variants parse.
    • Rebuilt native Bistro smoke test loads and attaches all 2,909 bistrox.gltf placements and produces a valid warmed frame.
  10. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    Final cross-path closure — layered visibility parity

    The last acceptance item is complete on Apple M1 Max / Metal.

    Commit 2b99693 expands the process-isolated forward/visibility fixture from one clearcoat draw to six moving compatibility draws covering specular/IOR, clearcoat, sheen, anisotropy, iridescence, and the combined material. It asserts the authored lobe fixture is active, the layered specialization and sheen LUT initialize, and visibility mode records the compatibility population rather than silently falling back to base PBR.

    Forward vs visibility results:

    • final RGBA8: 150 / 81,920 changed components, max 1 code value, mean 0.00183105;
    • HDR scene RGBA16F: 1,838 / 81,920 changed, max 0.000854492, mean 0.000003760681;
    • material RG8: 19 / 40,960 changed, max 1, mean 0.000463867;
    • motion RG16F: 55 / 40,960 changed, max 0.000003815, mean 0.000000005122;
    • albedo RGBA8: 80 / 81,920 changed, max 1, mean 0.000976562.

    All existing sub-LSB/f16 gates pass. The independent 64-frame hardware ray-query lobe-response test also passes unchanged against the checked angular reference corpus, including anisotropy and the combined material.

    This matches the implemented architecture: visibility shading owns eligible ordinary geometry, while layered materials retain the authoritative forward compatibility evaluator and final depth ownership. The gate adds no shipping pass, image, allocation, shader branch, binding, or per-frame work.

    Evidence is updated in docs/evidence/issue-134-layered-pbr-qualification.{md,json} at e8d67e7.

    Validation passed:

    • cargo test --test visibility_buffer_parity -- --nocapture (2/2);
    • cargo test --test visibility_buffer_shading_runtime -- --nocapture (1/1);
    • cargo test --test golden_render layered_pbr_parity:: -- --nocapture (1/1);
    • formatting, JSON validation, and diff checks.

    The repository quick wrapper currently stops before tests on an unrelated pre-existing file-size ratchet: renderer/mod.rs is 20 lines above its stored grandfathered baseline. This package does not modify that file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions