Skip to content

3D: fog and light shafts, tiled local lights, shadow face cache, glTF material extensions - #164

Merged
SpectralDragon merged 12 commits into
mainfrom
feature/3d-fog-lights-materials
Oct 19, 2026
Merged

SpectralDragon merged 12 commits into
mainfrom
feature/3d-fog-lights-materials

Conversation

@SpectralDragon

@SpectralDragon SpectralDragon commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Work from the ADR-0027 3D roadmap (Stages 2, 5 and 6). Each part is measured and recorded in the ADR, including its limits.

Stage 5: atmosphere

  • Fog3D in Environment3D: distance and exponential height fog in linear HDR before tone mapping, with sky influence and sun in-scattering. Free when off, and free in the benchmark when on.
  • Sun light shafts: a half-resolution raymarch through the fog that samples the cascaded shadow map, with a depth-aware upsample. About 0.26 ms at 16 steps and 0.65 ms at 48 steps (1080p, Apple M-class). Chosen over a compute froxel grid because it needs no compute path and reuses the sun shadows (the approach Bevy ships).

Stage 6: lighting scale

  • Tiled local lights: the CPU bins up to 32 local lights into screen tiles with a 32-bit mask each, so the main pass loops only over the lights that reach a pixel. With 32 lights geometry goes from 2.57 ms to 2.09 ms; with 8 lights the change is within noise.
  • Local shadow face cache: unchanged shadow faces keep their atlas tile, and a scene digest skips rebuilding per-face lists while casters and light matrices are unchanged. The local shadow pass goes from 1.10 ms to 0 ms on a static scene; the node's CPU time on 4,900 meshes goes from 0.49 ms to 0.21 ms. Skinned casters and LOD alternatives keep the per-face path.

Stage 3: morph targets

  • glTF targets, mesh and node weights, extras.targetNames and animated weights channels import (sparse accessors included). Each mesh part packs its deltas into one half-float texture; a draw applies the 8 heaviest targets, fetched in the vertex shader before skinning, so unmorphed meshes keep the plain shaders.
  • The main, motion, directional and local shadow shaders and the occlusion prepass all have morph vertex variants. Bounds grow by the displacement the weights can cause, a morphed mesh is its own batch and counts as dynamic for the shadow face cache, and the previous frame's weights feed motion vectors.
  • Weights animate through glTF clips, crossfades and the animation graph (blend, masks, additive), and MorphWeights3DComponent is a reflected component with scalar controls for scripts and the inspector.
  • The Editor has a Morph Targets mode in the Animator panel: a slider per named target, live preview, and an undoable apply stored as morphWeights on the Model 3D component (authored weights are kept unless a clip animates that mesh's weights).
  • Limits: deltas are dense half floats (memory grows with targets x vertices; 8192 x 2048 texels maximum) and there is no web build.

Stage 2: materials

  • KHR_texture_transform, KHR_materials_ior and KHR_materials_specular factors. The dielectric reflectance replaces the fixed 0.04 and equals it by default, so existing frames are unchanged.
  • KHR_materials_clearcoat (factors): a second GGX lobe on the geometric normal with energy-conserving dimming of the base layer and an environment reflection under image-based lighting. Together with sheen and anisotropy the three layers cost about 8 percent of main-pass geometry time on scenes that use none of them (bare 1.77 to 1.92 ms on the benchmark scene, most likely register pressure from the larger lighting function); this is now fixed: the main shader is compiled per material from only the layers and layer textures it uses (ADA_CLEARCOAT, ADA_SHEEN, ADA_ANISOTROPY and one macro per texture), which brings bare geometry back to 1.77 ms. The plain shader keeps the disk shader cache; layered variants compile in the process on first use (about 1 second each), and web builds have no layers.
  • KHR_materials_sheen (Charlie distribution, Neubelt visibility) and KHR_materials_anisotropy (anisotropic GGX, authored or UV-derivative tangent frame, bent IBL reflections), factors only. Their textures (clearcoat strength, roughness and normal, sheen color and roughness, anisotropy direction and strength, specular strength and color) are read too, each with its own UV set and KHR_texture_transform; they bind only in the shader variant of a material that has them and are sampled with the base color sampler (Metal has no free sampler slot).
  • Extension policy: NativeGLTFLoader.supportedExtensions, import warnings for optional extensions that are not implemented, and ModelAsset3D.importWarnings. The ADR also corrects the audit: the loader only rejected required unknown extensions; optional ones were silently ignored.

main was merged in. The only real conflict was with parallax occlusion mapping (#163 area): the uniform block is now ... emission, parallax, transforms, dielectric in flat3d.glsl, directional_shadow_3d.glsl and motion_3d.glsl, and a parallax UV shift is applied before a texture's own transform. Measured on the benchmark scene, the parallax code from main had already moved bare main-pass geometry from 1.69 to 1.77 ms.

Not validated / known limits

  • WebGPU is not validated. This machine has no Swan/Dawn or Swift 6.3.2, so script/validate_webgpu.sh was not run. The shaders are shared with the Metal path, but the new golden frames exist for Metal only.
  • Light shafts: sun only, no temporal accumulation, one shadow tap per step.
  • Tiled lights: 2D tiles without depth slicing; the 8-light case does not improve.
  • Shadow face cache: meshes edited in place (same buffers) are not noticed.
  • glTF: KHR_lights_punctual is still open; layer textures use the base color sampler; anisotropy on meshes without tangents is faceted on coarse geometry (UV-derivative frame); without image-based lighting the coat shows light highlights but not a sky reflection; the specular F90 term is not modeled.
  • Layered materials have no web build (variants cannot be compiled on WebAssembly), and the first draw of a new layer combination compiles a shader.
  • Editor package: the six new Editor tests pass (Xcode toolchain, SLOPPY_RUNTIME_LOCAL_PATH set). loaderAppliesAuthoredWeights was not run: Editor tests that go through EditorSceneFileLoader end the test process silently on my machine, including the existing EditorAnimationGraphTests round trip. Also, EditorAgentWorkspaceMCPTools.swift (from #251e688b, not part of this PR) uses Metadata/_meta, which AdaMCP v0.2.0 lacks, so the Editor does not build at main as is; I built with those three arguments temporarily removed and restored them.
  • The Environment 3D inspector in the Editor does not yet list the new fog, light-shaft, tiled-light and shadow-cache settings.
  • Not run here: Vulkan, OpenGL, Windows and Apple mobile.

Test plan

  • ADAENGINE_GOLDEN_METAL=1 swift test --filter Render3DGPUTests (80 tests, includes the new fog, light-shaft, light-tile, shadow-cache and material-extension suites and the existing golden frames)
  • CPU suites: glTF material import, Render3DMaterialExtensionConstantTests, light tile math, fog and shadow-cache settings, LocalLighting, VisibleMesh3DListsEquivalence, PBR3DSceneTests
  • Benchmarks: ADAENGINE_PERF=1 ADAENGINE_GOLDEN_METAL=1 swift test -c release -Xswiftc -enable-testing --filter Render3DGPUTests/Benchmarks
  • script/validate_webgpu.sh on a machine with Swan/Dawn and Swift 6.3.2
  • Re-record or add WebGPU golden frames for fog and light shafts

A test caveat: mixing the LocalLighting CPU suite with Render3DGPUTests in one --filter makes the GPU tests fail (one process-wide renderer), so run them separately. After changing PBRMaterial layout, clear the dependent module build directories before running GPU tests, or the incremental build can crash.

🤖 Generated with Claude Code

SpectralDragon and others added 12 commits October 19, 2026 01:46
… material extensions

Stage 5 (atmosphere)
- Fog3D in Environment3D: distance and exponential height fog in linear HDR before tone mapping, with sky influence and sun in-scattering.
- Sun light shafts: half-resolution raymarch through the fog that samples the cascaded shadow map, composited with a depth-aware upsample.

Stage 6 (lighting scale)
- Tiled local lights: the CPU bins up to 32 local lights into screen tiles with a 32-bit mask each, and the main pass loops only over the lights in its tile.
- Local shadow face cache: unchanged shadow faces keep their atlas tile; a scene digest skips rebuilding per-face lists while casters and light matrices are unchanged.

Stage 2 (materials)
- KHR_texture_transform, KHR_materials_ior and KHR_materials_specular (factors); the dielectric reflectance replaces the fixed 0.04.
- Extension policy: NativeGLTFLoader.supportedExtensions, import warnings for optional extensions that are not implemented, ModelAsset3D.importWarnings.

Adds Metal golden frames for fog and light shafts, benchmark configurations, and records measurements and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-materials

# Conflicts:
#	Documentation/ArchitectureDecisions/0027-3d-rendering-pipeline-gap-roadmap.md
#	Sources/AdaCorePipelines/Assets/Shaders/flat3d.glsl
#	Sources/AdaCorePipelines/CorePipelines/PBR3DUniforms.swift
A clear lacquer layer over the base surface: a second GGX lobe with its own roughness on the geometric normal, a highlight for every light, energy-conserving dimming of the base layer, and an environment reflection with image-based lighting. The importer reads the clearcoat factors (validated to 0...1) and reports clearcoat textures as skipped. Adds the pbr-sphere-clearcoat golden frame (Metal) and records the cost and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… PBR materials

Sheen uses the Charlie distribution and Neubelt visibility from the glTF specification with a constant albedo scale in place of its lookup table. Anisotropy uses anisotropic GGX with the authored tangent or a UV-derivative frame, rotated by anisotropyRotation, and bends image-based reflections toward the stretch. The sun now goes through evaluateDirect, so every layer applies to every light once. The importer validates the factors and reports sheen and anisotropy textures as skipped. Adds pbr-sphere-sheen and pbr-sphere-anisotropy golden frames (Metal) and records the cost (about 8 percent of main-pass geometry with all three layered features, none used) and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The temporal resolve is not exactly repeatable at a bright highlight: two untouched renders of the probe at the first sphere's highlight differ by one step in some runs and three in others, and the identity-LUT check required at most three steps from the first render. It failed in about half of the full GPU runs and never in isolation. The identity and inverted-LUT checks now allow the drift measured between the two untouched renders on top of their tolerance on the temporal path, so they test the LUT and not the noise floor. Four consecutive full runs pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…othing

PBRMaterial.shaderFeatures (clearcoat, sheen, anisotropy) joins the PBR pipeline key, and Flat3DPipeline takes a fragment shader variant that contains only those layers. The layer predicates in flat3d.glsl are constant false unless ADA_CLEARCOAT, ADA_SHEEN or ADA_ANISOTROPY is defined, so the plain shader (disk cached, and the only one on the web) has no layer code. Variants are compiled from the file text on first use because the on-disk shader cache is keyed by file and stage.

Main-pass geometry on the benchmark scene goes from 1.92 ms back to 1.77 ms (32 lights: 2.40 to 2.17 ms). Records the result and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r extensions

Eight layer textures (clearcoat, clearcoat roughness and normal, sheen color and roughness, anisotropy direction and strength, specular strength and color) are imported with their UV set, KHR_texture_transform and scale, mapped to PBRMaterial.layerTextures, and read in flat3d.glsl, where each multiplies its factor. They bind at 32 to 39 and only in the shader variant of a material that has them (one feature bit per texture), so other materials and the plain shader are unchanged. Layer textures share the base color sampler because the main pass has no free sampler slot. The dielectric reflectance is now a per-fragment value that the specular textures scale.

Adds import, mapping, feature and uniform tests and one GPU test per texture, with equivalence checks against the factor-only results. Records the design and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ghts

Primitives carry their morph targets (POSITION, NORMAL and TANGENT deltas per vertex; sparse accessors expand to dense), meshes carry default weights and the extras.targetNames convention, nodes may override the weights, and animation channels with the weights path import with one value per target and key (three per key for cubic splines), also on matrix-placed nodes. Inconsistent data (weights or names that do not match the targets, primitives with different target counts, accessors of the wrong size, a weights channel on a node whose mesh has no targets) fails with invalidMorphTargets, invalidNode or invalidAnimation. Morphed attributes other than the three deltas are reported as warnings.

This is data only: weights channels are not yet played (ModelAsset3D skips them) and nothing is rendered with the targets yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n vectors and bounds

Each mesh part packs its morph target deltas into one half-float texture; a draw applies the eight targets with the largest weights, fetched by the vertex shader (Morph.glsl, behind ADA_MORPH) before skinning, so unmorphed meshes keep the plain shaders. The weights live in a per-entity, per-part uniform with the previous frame's targets for motion vectors. The morph feature selects vertex shader variants of the main, motion, directional shadow, local shadow and occlusion prepass shaders (Shadow3DConfiguration joins the shadow pipeline key). Source bounds grow by the displacement the active weights can cause (scaled by the largest joint scale for skinned meshes); a morphed mesh is its own batch, has no LOD alternatives, and counts as dynamic for the shadow face cache. MorphWeights3DComponent holds the weights and ModelAsset3D instantiates it with the file's defaults.

Tests compare a weighted shift with moving the entity (sun and local-light shadows included), the eight-target limit, culling of a mesh that rests off screen, normal deltas, the occlusion prepass, the temporal motion pass, previous-frame weights, and morph-before-skin on a glTF built in the test. Records the design and limits in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…on graph, and script access

Weights channels import as MorphWeightsTrack (linear, step, cubic spline), join the clip duration, and are sampled by SkeletalAnimationPlayer into morphWeights by rig node, with rest weights from the file, crossfades, and blend, mask and additive rules matching the poses in the animation graph. SkeletalAnimation3DSystem writes them to the node's MorphWeights3DComponent. The component becomes a reflected, Codable, registered @component; because reflection exposes scalars only it offers a selected target (index or name) and its weight for scripts and the inspector.

Adds 17 CPU animation tests, a GPU test that plays an animated glTF end to end, and component reflection and Codable tests. Records the design and what remains (an Editor panel) in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tes them

Model3DSource gains morphWeights (target name to weight), applied at instantiation to every mesh that has a target of that name; scenes written before it decode to none. The animation system now writes player weights only to nodes some clip animates (SkeletalAnimationPlayer.animatedMorphNodes); before, a rigged model's rest weights from the file would have overwritten authored ones on the first frame.

Tests cover the encoding and older scenes, authored weights surviving on a rigged model, and a clip taking over the weights it animates.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ble apply

The Animator panel gains a Morph Targets mode that lists the model's named targets with a slider each, shows the result live in the model preview, and applies it to the scene as one undoable transaction stored as a structured morphWeights map on the Model 3D component (a weight equal to the file's default is not an override; clearing removes the key). The Model 3D descriptor decodes the map, EditorComponentRegistry registers MorphWeights3DComponent, and the strings are localized (en, ru).

Six Editor tests cover the panel model, the payload, the scene transaction with save, undo and redo, the preview, and the weight list; one more opens the saved scene through the scene loader and is not run on machines where that loader ends the test process (the existing animation graph round trip does the same there). Records the design and gaps in ADR-0027.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@SpectralDragon
SpectralDragon marked this pull request as ready for review October 19, 2026 22:09
@SpectralDragon
SpectralDragon merged commit 7b63476 into main Oct 19, 2026
1 of 3 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-19T22:10:38.070672Z 0fb989c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant