From aa35c8afe71b42854d196de6130ab8763a277411 Mon Sep 17 00:00:00 2001 From: Nick Newson Date: Sat, 12 Sep 2026 14:46:31 +0100 Subject: [PATCH] Verify punctual shadow reuse, and measure what it is worth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arc 2 #15 asked for punctual-shadow change detection. Arc 2 #4 had already built it: the residency mechanism is family-agnostic, a point face's light position and range are in the content descriptor, and per-face frustum filtering predates both. So this branch changes no engine code. What it owes instead is evidence, because "we believe the mechanism covers this family" is not the same claim as "it does". THE AUDIT. Every shadow SAMPLING input — spotViewProj, cascadeViewProj, selfShadowViewProj and all four *BiasMetrics arrays — is copied wholesale from the completed view set every frame, unconditionally, never from the plan and never gated on whether a family recorded. Every RASTER input is in the content descriptor. Nothing sits outside both, which is what makes a reused map unable to go stale through a sampling parameter. Shadow draws are also VDPM-free by construction (object.cpp clears the indirect handle and the GPU front), so a per-frame GPU-emitted index buffer — whose handle repeats out of a two-deep ring while its contents change — can never enter the descriptor. WHAT IS PINNED. Eight headless cases, each a way a convincing stale map could have survived. A RANGE-only change re-records all six faces: every matrix is identical across the two frames and every texel still differs, because the stored value is a ratio against that range — the case a transform-only descriptor would miss. A moving light re-records all six, since its position is an input to the radial depth each face stores. A slot inherited by another light re-records rather than lighting one light with its predecessor's shadows, pinned for a single spot view and for a whole cube, because a law that held for one entry could still leave five faces of a departed light's map in place. Bias metrics reuse, on both paths, which is the audit's boundary stated as a test. PER-FACE GRANULARITY is where the punctual saving lives, and it is caster-driven: a cube is compared face by face, but a light's own movement invalidates every face through the content law, so only a caster can change one face and not another. ShadowResidencyCasterMotionTest moves one rigid caster inside a single face and holds recorded=1 reused=5 at a median 0.007 ms, against 0.141 ms forced on the same scene — about twenty times less for an identical image. ShadowResidencyLightMotionTest is the ceiling: recorded=6 reused=0 at 0.172 ms, which is what punctual reuse cannot save. Two files rather than two phases of one animation, because the evidence is a per-family count and a scene that changed regime part-way would make every aggregate a mixture of two answers. SLOT CHURN is parked, not solved. Punctual slots are assigned per frame in gather order, so a light leaving compacts the ones after it and every inherited slot re-records a cube it could in principle have kept. That is safe — identity is part of the comparison, and two tests pin it — and its cost is currently unreachable: there is no runtime light removal or enable/disable path and gatherLights() walks a stable node order, so churn frequency in production is zero. A stable-assignment scheme would be machinery guarding against nothing. Trigger for revisiting it is recorded in the roadmap. Two traps found in the existing test fixture along the way, both now commented where the next person will hit them: populatedViews() gives all six point faces the same marked matrix, so their frusta are identical and every face admits every caster; and candidateDraws counts draws OFFERED while drawnDraws counts those the filter ACCEPTED, so a premise checked against the wrong counter reports six faces seeing an on-axis caster. --- .../ShadowResidencyCasterMotionTest.gltf | 1010 +++++++++++++++++ .../ShadowResidencyLightMotionTest.gltf | 1010 +++++++++++++++++ assets/shadow_residency/generate.py | 81 +- docs/acceptance-testing.md | 43 + docs/architecturalreview.md | 2 +- docs/roadmap.md | 41 +- docs/shadowplans.md | 32 + tests/graphics/test_shadow_pass_prepare.cpp | 255 +++++ 8 files changed, 2436 insertions(+), 38 deletions(-) create mode 100644 assets/shadow_residency/ShadowResidencyCasterMotionTest.gltf create mode 100644 assets/shadow_residency/ShadowResidencyLightMotionTest.gltf diff --git a/assets/shadow_residency/ShadowResidencyCasterMotionTest.gltf b/assets/shadow_residency/ShadowResidencyCasterMotionTest.gltf new file mode 100644 index 0000000..f30063e --- /dev/null +++ b/assets/shadow_residency/ShadowResidencyCasterMotionTest.gltf @@ -0,0 +1,1010 @@ +{ + "asset": { + "version": "2.0", + "generator": "fireEngine shadow_residency generate.py" + }, + "scene": 0, + "scenes": [ + { + "name": "Scene", + "nodes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ] + } + ], + "nodes": [ + { + "name": "PointLight", + "translation": [ + 0.0, + 0.0, + 0.0 + ], + "extensions": { + "KHR_lights_punctual": { + "light": 0 + } + } + }, + { + "name": "WallPosX", + "mesh": 0, + "translation": [ + 8.2, + 0.0, + 0.0 + ] + }, + { + "name": "CasterPosX", + "mesh": 1, + "translation": [ + 3.0, + 0.0, + 0.0 + ] + }, + { + "name": "WallNegX", + "mesh": 2, + "translation": [ + -8.2, + 0.0, + 0.0 + ] + }, + { + "name": "CasterNegX", + "mesh": 3, + "translation": [ + -3.0, + 0.0, + 0.0 + ] + }, + { + "name": "WallPosY", + "mesh": 4, + "translation": [ + 0.0, + 8.2, + 0.0 + ] + }, + { + "name": "CasterPosY", + "mesh": 5, + "translation": [ + 0.0, + 3.0, + 0.0 + ] + }, + { + "name": "WallNegY", + "mesh": 6, + "translation": [ + 0.0, + -8.2, + 0.0 + ] + }, + { + "name": "CasterNegY", + "mesh": 7, + "translation": [ + 0.0, + -3.0, + 0.0 + ] + }, + { + "name": "WallPosZ", + "mesh": 8, + "translation": [ + 0.0, + 0.0, + 8.2 + ] + }, + { + "name": "CasterPosZ", + "mesh": 9, + "translation": [ + 0.0, + 0.0, + 3.0 + ] + }, + { + "name": "WallNegZ", + "mesh": 10, + "translation": [ + 0.0, + 0.0, + -8.2 + ] + }, + { + "name": "CasterNegZ", + "mesh": 11, + "translation": [ + 0.0, + 0.0, + -3.0 + ] + }, + { + "name": "Camera", + "camera": 0, + "translation": [ + 6.2, + 3.6, + 6.6 + ], + "rotation": [ + -0.17834651899579498, + 0.3599036106085298, + 0.07029763117992845, + 0.9130827746067259 + ] + } + ], + "meshes": [ + { + "name": "WallPosX", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 1 + }, + "indices": 2, + "material": 0 + } + ] + }, + { + "name": "CasterPosX", + "primitives": [ + { + "attributes": { + "POSITION": 3, + "NORMAL": 4 + }, + "indices": 5, + "material": 1 + } + ] + }, + { + "name": "WallNegX", + "primitives": [ + { + "attributes": { + "POSITION": 6, + "NORMAL": 7 + }, + "indices": 8, + "material": 0 + } + ] + }, + { + "name": "CasterNegX", + "primitives": [ + { + "attributes": { + "POSITION": 9, + "NORMAL": 10 + }, + "indices": 11, + "material": 1 + } + ] + }, + { + "name": "WallPosY", + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13 + }, + "indices": 14, + "material": 0 + } + ] + }, + { + "name": "CasterPosY", + "primitives": [ + { + "attributes": { + "POSITION": 15, + "NORMAL": 16 + }, + "indices": 17, + "material": 1 + } + ] + }, + { + "name": "WallNegY", + "primitives": [ + { + "attributes": { + "POSITION": 18, + "NORMAL": 19 + }, + "indices": 20, + "material": 2 + } + ] + }, + { + "name": "CasterNegY", + "primitives": [ + { + "attributes": { + "POSITION": 21, + "NORMAL": 22 + }, + "indices": 23, + "material": 1 + } + ] + }, + { + "name": "WallPosZ", + "primitives": [ + { + "attributes": { + "POSITION": 24, + "NORMAL": 25 + }, + "indices": 26, + "material": 0 + } + ] + }, + { + "name": "CasterPosZ", + "primitives": [ + { + "attributes": { + "POSITION": 27, + "NORMAL": 28 + }, + "indices": 29, + "material": 1 + } + ] + }, + { + "name": "WallNegZ", + "primitives": [ + { + "attributes": { + "POSITION": 30, + "NORMAL": 31 + }, + "indices": 32, + "material": 0 + } + ] + }, + { + "name": "CasterNegZ", + "primitives": [ + { + "attributes": { + "POSITION": 33, + "NORMAL": 34 + }, + "indices": 35, + "material": 1 + } + ] + } + ], + "materials": [ + { + "name": "Mat0", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.52, + 0.51, + 0.49, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + }, + { + "name": "Mat1", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.85, + 0.35, + 0.25, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + }, + { + "name": "Mat2", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.4, + 0.4, + 0.43, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.2, + -8.4, + -8.4 + ], + "max": [ + 0.2, + 8.4, + 8.4 + ] + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 4, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 5, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 6, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.2, + -8.4, + -8.4 + ], + "max": [ + 0.2, + 8.4, + 8.4 + ] + }, + { + "bufferView": 7, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 8, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 9, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 10, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 11, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 12, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -0.2, + -8.4 + ], + "max": [ + 8.4, + 0.2, + 8.4 + ] + }, + { + "bufferView": 13, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 14, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 15, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 16, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 17, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 18, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -0.2, + -8.4 + ], + "max": [ + 8.4, + 0.2, + 8.4 + ] + }, + { + "bufferView": 19, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 20, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 21, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 22, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 23, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 24, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -8.4, + -0.2 + ], + "max": [ + 8.4, + 8.4, + 0.2 + ] + }, + { + "bufferView": 25, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 26, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 27, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 28, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 29, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 30, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -8.4, + -0.2 + ], + "max": [ + 8.4, + 8.4, + 0.2 + ] + }, + { + "bufferView": 31, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 32, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 33, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 34, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 35, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 36, + "componentType": 5126, + "count": 3, + "type": "SCALAR", + "min": [ + 0.0 + ], + "max": [ + 4.0 + ] + }, + { + "bufferView": 37, + "componentType": 5126, + "count": 3, + "type": "VEC3" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 288, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 648, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 936, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1224, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 1296, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1584, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1872, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 1944, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2232, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2520, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 2592, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2880, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3168, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 3240, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3528, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3816, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 3888, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4176, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4464, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 4536, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4824, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5112, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 5184, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5472, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5760, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 5832, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6120, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6408, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 6480, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6768, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7056, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 7128, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7416, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7704, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 7776, + "byteLength": 12 + }, + { + "buffer": 0, + "byteOffset": 7788, + "byteLength": 36 + } + ], + "buffers": [ + { + "byteLength": 7824, + "uri": "data:application/octet-stream;base64,zcxMPmZmBsFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBkFmZgZBzcxMPmZmBsFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBkFmZgbBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMvmZmBkFmZgZBzcxMPmZmBkFmZgZBzcxMPmZmBkFmZgbBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMPmZmBsFmZgbBzcxMPmZmBsFmZgZBzcxMPmZmBsFmZgZBzcxMPmZmBkFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBsFmZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAzcxMPmZmBsFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBkFmZgZBzcxMPmZmBsFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBkFmZgbBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMvmZmBkFmZgZBzcxMPmZmBkFmZgZBzcxMPmZmBkFmZgbBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMPmZmBsFmZgbBzcxMPmZmBsFmZgZBzcxMPmZmBsFmZgZBzcxMPmZmBkFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBsFmZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQc3MTL5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTD5mZgZBZmYGQc3MTL5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTD5mZgbBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGwc3MTD5mZgZBZmYGQc3MTD5mZgZBZmYGQc3MTD5mZgbBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGQc3MTL5mZgbBZmYGQc3MTL5mZgZBZmYGQc3MTL5mZgZBZmYGQc3MTD5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTL5mZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQc3MTL5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTD5mZgZBZmYGQc3MTL5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTD5mZgbBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGwc3MTD5mZgZBZmYGQc3MTD5mZgZBZmYGQc3MTD5mZgbBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGQc3MTL5mZgbBZmYGQc3MTL5mZgZBZmYGQc3MTL5mZgZBZmYGQc3MTD5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTL5mZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQWZmBsHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBkHNzEw+ZmYGQWZmBsHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBkHNzEy+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGwWZmBkHNzEw+ZmYGQWZmBkHNzEw+ZmYGQWZmBkHNzEy+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGQWZmBsHNzEy+ZmYGQWZmBsHNzEw+ZmYGQWZmBsHNzEw+ZmYGQWZmBkHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBsHNzEy+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQWZmBsHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBkHNzEw+ZmYGQWZmBsHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBkHNzEy+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGwWZmBkHNzEw+ZmYGQWZmBkHNzEw+ZmYGQWZmBkHNzEy+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGQWZmBsHNzEy+ZmYGQWZmBsHNzEw+ZmYGQWZmBsHNzEw+ZmYGQWZmBkHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBsHNzEy+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAAAAAAEAAAIBAzcwMQAAAAAAAAAAAMzNzQAAAAAAAAAAAzcwMQAAAAAAAAAAA" + } + ], + "cameras": [ + { + "name": "Camera", + "type": "perspective", + "perspective": { + "yfov": 0.7, + "znear": 0.05, + "zfar": 500.0 + } + } + ], + "animations": [ + { + "name": "CasterInOneFace", + "samplers": [ + { + "input": 36, + "output": 37, + "interpolation": "LINEAR" + } + ], + "channels": [ + { + "sampler": 0, + "target": { + "node": 2, + "path": "translation" + } + } + ] + } + ], + "extensionsUsed": [ + "KHR_lights_punctual" + ], + "extensions": { + "KHR_lights_punctual": { + "lights": [ + { + "name": "Point", + "type": "point", + "color": [ + 1.0, + 0.96, + 0.9 + ], + "intensity": 26.0, + "range": 22.0 + } + ] + } + } +} diff --git a/assets/shadow_residency/ShadowResidencyLightMotionTest.gltf b/assets/shadow_residency/ShadowResidencyLightMotionTest.gltf new file mode 100644 index 0000000..aa8e268 --- /dev/null +++ b/assets/shadow_residency/ShadowResidencyLightMotionTest.gltf @@ -0,0 +1,1010 @@ +{ + "asset": { + "version": "2.0", + "generator": "fireEngine shadow_residency generate.py" + }, + "scene": 0, + "scenes": [ + { + "name": "Scene", + "nodes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13 + ] + } + ], + "nodes": [ + { + "name": "PointLight", + "translation": [ + 0.0, + 0.0, + 0.0 + ], + "extensions": { + "KHR_lights_punctual": { + "light": 0 + } + } + }, + { + "name": "WallPosX", + "mesh": 0, + "translation": [ + 8.2, + 0.0, + 0.0 + ] + }, + { + "name": "CasterPosX", + "mesh": 1, + "translation": [ + 3.0, + 0.0, + 0.0 + ] + }, + { + "name": "WallNegX", + "mesh": 2, + "translation": [ + -8.2, + 0.0, + 0.0 + ] + }, + { + "name": "CasterNegX", + "mesh": 3, + "translation": [ + -3.0, + 0.0, + 0.0 + ] + }, + { + "name": "WallPosY", + "mesh": 4, + "translation": [ + 0.0, + 8.2, + 0.0 + ] + }, + { + "name": "CasterPosY", + "mesh": 5, + "translation": [ + 0.0, + 3.0, + 0.0 + ] + }, + { + "name": "WallNegY", + "mesh": 6, + "translation": [ + 0.0, + -8.2, + 0.0 + ] + }, + { + "name": "CasterNegY", + "mesh": 7, + "translation": [ + 0.0, + -3.0, + 0.0 + ] + }, + { + "name": "WallPosZ", + "mesh": 8, + "translation": [ + 0.0, + 0.0, + 8.2 + ] + }, + { + "name": "CasterPosZ", + "mesh": 9, + "translation": [ + 0.0, + 0.0, + 3.0 + ] + }, + { + "name": "WallNegZ", + "mesh": 10, + "translation": [ + 0.0, + 0.0, + -8.2 + ] + }, + { + "name": "CasterNegZ", + "mesh": 11, + "translation": [ + 0.0, + 0.0, + -3.0 + ] + }, + { + "name": "Camera", + "camera": 0, + "translation": [ + 6.2, + 3.6, + 6.6 + ], + "rotation": [ + -0.17834651899579498, + 0.3599036106085298, + 0.07029763117992845, + 0.9130827746067259 + ] + } + ], + "meshes": [ + { + "name": "WallPosX", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 1 + }, + "indices": 2, + "material": 0 + } + ] + }, + { + "name": "CasterPosX", + "primitives": [ + { + "attributes": { + "POSITION": 3, + "NORMAL": 4 + }, + "indices": 5, + "material": 1 + } + ] + }, + { + "name": "WallNegX", + "primitives": [ + { + "attributes": { + "POSITION": 6, + "NORMAL": 7 + }, + "indices": 8, + "material": 0 + } + ] + }, + { + "name": "CasterNegX", + "primitives": [ + { + "attributes": { + "POSITION": 9, + "NORMAL": 10 + }, + "indices": 11, + "material": 1 + } + ] + }, + { + "name": "WallPosY", + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13 + }, + "indices": 14, + "material": 0 + } + ] + }, + { + "name": "CasterPosY", + "primitives": [ + { + "attributes": { + "POSITION": 15, + "NORMAL": 16 + }, + "indices": 17, + "material": 1 + } + ] + }, + { + "name": "WallNegY", + "primitives": [ + { + "attributes": { + "POSITION": 18, + "NORMAL": 19 + }, + "indices": 20, + "material": 2 + } + ] + }, + { + "name": "CasterNegY", + "primitives": [ + { + "attributes": { + "POSITION": 21, + "NORMAL": 22 + }, + "indices": 23, + "material": 1 + } + ] + }, + { + "name": "WallPosZ", + "primitives": [ + { + "attributes": { + "POSITION": 24, + "NORMAL": 25 + }, + "indices": 26, + "material": 0 + } + ] + }, + { + "name": "CasterPosZ", + "primitives": [ + { + "attributes": { + "POSITION": 27, + "NORMAL": 28 + }, + "indices": 29, + "material": 1 + } + ] + }, + { + "name": "WallNegZ", + "primitives": [ + { + "attributes": { + "POSITION": 30, + "NORMAL": 31 + }, + "indices": 32, + "material": 0 + } + ] + }, + { + "name": "CasterNegZ", + "primitives": [ + { + "attributes": { + "POSITION": 33, + "NORMAL": 34 + }, + "indices": 35, + "material": 1 + } + ] + } + ], + "materials": [ + { + "name": "Mat0", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.52, + 0.51, + 0.49, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + }, + { + "name": "Mat1", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.85, + 0.35, + 0.25, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + }, + { + "name": "Mat2", + "doubleSided": true, + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.4, + 0.4, + 0.43, + 1.0 + ], + "metallicFactor": 0.0, + "roughnessFactor": 0.6 + } + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.2, + -8.4, + -8.4 + ], + "max": [ + 0.2, + 8.4, + 8.4 + ] + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 4, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 5, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 6, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.2, + -8.4, + -8.4 + ], + "max": [ + 0.2, + 8.4, + 8.4 + ] + }, + { + "bufferView": 7, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 8, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 9, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 10, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 11, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 12, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -0.2, + -8.4 + ], + "max": [ + 8.4, + 0.2, + 8.4 + ] + }, + { + "bufferView": 13, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 14, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 15, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 16, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 17, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 18, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -0.2, + -8.4 + ], + "max": [ + 8.4, + 0.2, + 8.4 + ] + }, + { + "bufferView": 19, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 20, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 21, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 22, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 23, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 24, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -8.4, + -0.2 + ], + "max": [ + 8.4, + 8.4, + 0.2 + ] + }, + { + "bufferView": 25, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 26, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 27, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 28, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 29, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 30, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -8.4, + -8.4, + -0.2 + ], + "max": [ + 8.4, + 8.4, + 0.2 + ] + }, + { + "bufferView": 31, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 32, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 33, + "componentType": 5126, + "count": 24, + "type": "VEC3", + "min": [ + -0.5, + -0.5, + -0.5 + ], + "max": [ + 0.5, + 0.5, + 0.5 + ] + }, + { + "bufferView": 34, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 35, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 36, + "componentType": 5126, + "count": 3, + "type": "SCALAR", + "min": [ + 0.0 + ], + "max": [ + 5.0 + ] + }, + { + "bufferView": 37, + "componentType": 5126, + "count": 3, + "type": "VEC3" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 288, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 648, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 936, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1224, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 1296, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1584, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 1872, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 1944, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2232, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2520, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 2592, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 2880, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3168, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 3240, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3528, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 3816, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 3888, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4176, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4464, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 4536, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 4824, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5112, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 5184, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5472, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 5760, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 5832, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6120, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6408, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 6480, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 6768, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7056, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 7128, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7416, + "byteLength": 288, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 7704, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 7776, + "byteLength": 12 + }, + { + "buffer": 0, + "byteOffset": 7788, + "byteLength": 36 + } + ], + "buffers": [ + { + "byteLength": 7824, + "uri": "data:application/octet-stream;base64,zcxMPmZmBsFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBkFmZgZBzcxMPmZmBsFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBkFmZgbBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMvmZmBkFmZgZBzcxMPmZmBkFmZgZBzcxMPmZmBkFmZgbBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMPmZmBsFmZgbBzcxMPmZmBsFmZgZBzcxMPmZmBsFmZgZBzcxMPmZmBkFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBsFmZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAzcxMPmZmBsFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBkFmZgZBzcxMPmZmBsFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBkFmZgbBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMvmZmBkFmZgZBzcxMPmZmBkFmZgZBzcxMPmZmBkFmZgbBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMPmZmBsFmZgbBzcxMPmZmBsFmZgZBzcxMPmZmBsFmZgZBzcxMPmZmBkFmZgZBzcxMvmZmBkFmZgZBzcxMvmZmBsFmZgZBzcxMvmZmBsFmZgbBzcxMvmZmBkFmZgbBzcxMPmZmBkFmZgbBzcxMPmZmBsFmZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQc3MTL5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTD5mZgZBZmYGQc3MTL5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTD5mZgbBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGwc3MTD5mZgZBZmYGQc3MTD5mZgZBZmYGQc3MTD5mZgbBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGQc3MTL5mZgbBZmYGQc3MTL5mZgZBZmYGQc3MTL5mZgZBZmYGQc3MTD5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTL5mZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQc3MTL5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTD5mZgZBZmYGQc3MTL5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTD5mZgbBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGwc3MTD5mZgZBZmYGQc3MTD5mZgZBZmYGQc3MTD5mZgbBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGQc3MTL5mZgbBZmYGQc3MTL5mZgZBZmYGQc3MTL5mZgZBZmYGQc3MTD5mZgZBZmYGwc3MTD5mZgZBZmYGwc3MTL5mZgZBZmYGwc3MTL5mZgbBZmYGwc3MTD5mZgbBZmYGQc3MTD5mZgbBZmYGQc3MTL5mZgbBAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQWZmBsHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBkHNzEw+ZmYGQWZmBsHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBkHNzEy+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGwWZmBkHNzEw+ZmYGQWZmBkHNzEw+ZmYGQWZmBkHNzEy+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGQWZmBsHNzEy+ZmYGQWZmBsHNzEw+ZmYGQWZmBsHNzEw+ZmYGQWZmBkHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBsHNzEy+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAZmYGQWZmBsHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBkHNzEw+ZmYGQWZmBsHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBkHNzEy+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGwWZmBkHNzEw+ZmYGQWZmBkHNzEw+ZmYGQWZmBkHNzEy+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGQWZmBsHNzEy+ZmYGQWZmBsHNzEw+ZmYGQWZmBsHNzEw+ZmYGQWZmBkHNzEw+ZmYGwWZmBkHNzEw+ZmYGwWZmBsHNzEw+ZmYGwWZmBsHNzEy+ZmYGwWZmBkHNzEy+ZmYGQWZmBkHNzEy+ZmYGQWZmBsHNzEy+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAAAAAAAAAIEAAAKBAAACAvwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAA" + } + ], + "cameras": [ + { + "name": "Camera", + "type": "perspective", + "perspective": { + "yfov": 0.7, + "znear": 0.05, + "zfar": 500.0 + } + } + ], + "animations": [ + { + "name": "LightSweep", + "samplers": [ + { + "input": 36, + "output": 37, + "interpolation": "LINEAR" + } + ], + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "translation" + } + } + ] + } + ], + "extensionsUsed": [ + "KHR_lights_punctual" + ], + "extensions": { + "KHR_lights_punctual": { + "lights": [ + { + "name": "Point", + "type": "point", + "color": [ + 1.0, + 0.96, + 0.9 + ], + "intensity": 26.0, + "range": 22.0 + } + ] + } + } +} diff --git a/assets/shadow_residency/generate.py b/assets/shadow_residency/generate.py index 41c8f0c..dad647a 100644 --- a/assets/shadow_residency/generate.py +++ b/assets/shadow_residency/generate.py @@ -98,11 +98,24 @@ def scaled(axis, distance): return tuple(component * distance for component in axis) -def build(): +def build(motion=None): + """The room, the light and the six casters. + + `motion` selects which of the three scenes this is: `None` for the static gate, + `"caster"` for one caster moving inside a single cube face, `"light"` for the light + itself moving. They are separate FILES rather than phases of one animation, and + deliberately: the diagnostic evidence is a per-family count (`recorded=1 reused=5` + against `recorded=6 reused=0`), and a scene that changed regime part-way would make + every aggregate a mixture of two answers with no way to say which frame is which. + """ s = Scene(GENERATOR) + # Node indices are captured as they are created — `Scene` hands one back from every + # builder — because the animation channels below address nodes by index. + index = {} + # The light first, so it is light 0 and the scene's only one. - s.add_node( + index["PointLight"] = s.add_node( "PointLight", light=s.light( "Point", @@ -121,7 +134,7 @@ def build(): colour = FLOOR_COLOUR if name == "NegY" else WALL_COLOUR s.box(f"Wall{name}", wall_half_extent(axis), wall_centre, colour) # One occluder per face, on the axis between the light and that wall. - s.box( + index[f"Caster{name}"] = s.box( f"Caster{name}", (CASTER_HALF, CASTER_HALF, CASTER_HALF), scaled(axis, CASTER_DISTANCE), @@ -129,10 +142,41 @@ def build(): ) s.camera(CAMERA_EYE, CAMERA_TARGET) + + if motion == "caster": + # CONTINUOUS, and confined to ONE face. Radial motion along the +X axis keeps the + # caster inside the +X face's 90-degree frustum for the whole loop while changing + # what that face stores every frame — so the steady state is `recorded=1 reused=5`, + # which is the per-face granularity this item is about, stated as a number. + # + # Continuous rather than a single hop: the diagnostic sample is periodic, and a + # one-off transition can fall between two samples and read as a scene that never + # changed. + near = CASTER_DISTANCE - 0.8 + far = CASTER_DISTANCE + 0.8 + s.animation( + "CasterInOneFace", + [ + (index["CasterPosX"], "translation", [0.0, 2.0, 4.0], + [(near, 0.0, 0.0), (far, 0.0, 0.0), (near, 0.0, 0.0)], "LINEAR"), + ], + ) + elif motion == "light": + # The light itself moves, so every face's stored depth changes: its position is an + # input to the radial ratio each of the six faces writes, not merely to the matrices. + # Steady state is therefore `recorded=6 reused=0` — the honest ceiling for what + # punctual reuse can save when the light is the thing in motion. + s.animation( + "LightSweep", + [ + (index["PointLight"], "translation", [0.0, 2.5, 5.0], + [(-1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (-1.0, 0.0, 0.0)], "LINEAR"), + ], + ) return s -def validate(doc): +def validate(doc, motion=None): """Structural checks, because this scene's whole value is what it does NOT contain. A later edit that adds a sun, or animates a caster to make a screenshot livelier, would @@ -148,7 +192,18 @@ def validate(doc): # Temporal or deforming content would change the content descriptor every frame, so a # reused view could never happen and the gate would fail for the wrong reason. - assert not doc.get("animations"), "the residency gate scene must not animate" + if motion is None: + assert not doc.get("animations"), "the static gate scene must not animate" + else: + # Exactly ONE animated node, and the right one. A second channel would mix two + # regimes into one family count and make `recorded=N` unreadable. + animations = doc.get("animations", []) + assert len(animations) == 1, f"expected one animation, found {len(animations)}" + channels = animations[0]["channels"] + assert len(channels) == 1, f"expected one animated node, found {len(channels)}" + animated = doc["nodes"][channels[0]["target"]["node"]]["name"] + expected = "CasterPosX" if motion == "caster" else "PointLight" + assert animated == expected, f"{motion} scene animates '{animated}', not '{expected}'" assert not doc.get("skins"), "the residency gate scene must not contain skinned casters" for mesh in doc["meshes"]: for primitive in mesh["primitives"]: @@ -189,12 +244,16 @@ def validate(doc): def main(): - scene = build() - doc = scene.to_gltf() - validate(doc) - out = Path(__file__).resolve().parent / "ShadowResidencyTest.gltf" - write_gltf(out, doc) - print(f"wrote {out}") + here = Path(__file__).resolve().parent + for motion, name in ((None, "ShadowResidencyTest.gltf"), + ("caster", "ShadowResidencyCasterMotionTest.gltf"), + ("light", "ShadowResidencyLightMotionTest.gltf")): + scene = build(motion) + doc = scene.to_gltf() + validate(doc, motion) + out = here / name + write_gltf(out, doc) + print(f"wrote {out}") if __name__ == "__main__": diff --git a/docs/acceptance-testing.md b/docs/acceptance-testing.md index 0b67873..49f3025 100644 --- a/docs/acceptance-testing.md +++ b/docs/acceptance-testing.md @@ -591,6 +591,49 @@ Look at the image as well as the hash. The two large hard-edged rectangles on th cast shadows; if a run ever produces an image with a shadow in the wrong place rather than a different hash, that is a stale map and `--no-shadow-reuse` will confirm it in one run. +### 4. Per-face granularity — the punctual payoff (arc 2 #15) + +The static gate above proves reuse happens; these two scenes prove what it is WORTH when something +is actually moving, which is the case a shipping scene is in. They are separate files rather than +phases of one animation on purpose: the evidence is a per-family count, and a scene that changed +regime part-way would make every aggregate a mixture of two answers. + +```bash +# One rigid caster moving INSIDE a single cube face; the light and the other five casters are static. +FE_LOG=render:debug ./fireEngineApp --no-taa \ + shadow_residency/ShadowResidencyCasterMotionTest.gltf nightbox.hdr +``` + +Steady state must be **`point sampleable recorded=1 reused=5 passes=1`**. One face's content changed, +so one face re-rendered; the other five are still resident and still sampleable. Then the same scene +with the cache off: + +```bash +FE_LOG=render:debug ./fireEngineApp --no-taa --no-shadow-reuse \ + shadow_residency/ShadowResidencyCasterMotionTest.gltf nightbox.hdr +``` + +`recorded=6 reused=0 passes=6`. Measured here (macOS/arm64, MoltenVK, 15 warm samples each): +**0.007 ms** median for the point family with reuse (0.004–0.022) against **0.141 ms** forced +(0.124–0.192) — the same scene, the same image, a factor of about twenty, and a direct measurement +of per-face granularity rather than of reuse in general. + +```bash +# The light itself moving — the ceiling on what punctual reuse can save. +FE_LOG=render:debug ./fireEngineApp --no-taa \ + shadow_residency/ShadowResidencyLightMotionTest.gltf nightbox.hdr +``` + +Expect **`recorded=6 reused=0`** (measured: 0.172 ms median, 9 samples) even with reuse enabled, +and that is correct rather than a failure: +a point light's position is an input to the radial depth EVERY face stores, so a light that moves +invalidates its whole cube. A run that showed anything less would mean a face had kept depth measured +against a position the light has left. + +**No reference screenshot for either motion scene.** An animated frame has no reproducible timestamp, +so a capture proves nothing; the State column, the family counters and the timing comparison are the +evidence. + ### 4. Vulkan validation stays clean Add `--require-validation` to any of the above. A reused view is skipped **entirely** — no barrier, diff --git a/docs/architecturalreview.md b/docs/architecturalreview.md index 4f2e2bd..d980768 100644 --- a/docs/architecturalreview.md +++ b/docs/architecturalreview.md @@ -387,7 +387,7 @@ exists; the tiered review inherited the filename.)* | 12 | ✅ Skip frustum extraction for inactive shadow slots *(same branch; coarse-cull pushes only active cascade/spot/point slots — also tightens the cull)* | C | XS | §5.3 | | 13 | ✅ Comment: chart-set immutability rationale *(same branch)* | C | XS | §4.2 | | 14 | ✅ Comment: hinge axis rows prepare-frozen by design *(same branch)* | C | XS | §3.4 | -| 15 | Punctual-shadow change detection (spot/point re-render every face every frame; point is 6×1024² each) — same epoch/dirty-bit mechanism as item 4 | B/C | M | §2.3 | +| 15 | ✅ Punctual-shadow change detection *(branch `shadow-punctual-change-detection`)* — verified, closed with NO engine change. Arc 2 #4's residency mechanism is family-agnostic and already covered spot and point; this item's value was the evidence. Audit: every shadow sampling input is re-uploaded from the view set each frame, every raster input is in the content descriptor, nothing is outside both. Pinned headlessly: range-only changes re-record a whole cube, a moving light re-records all six faces (its position is an input to every face's radial depth), an inherited slot re-records rather than reusing a predecessor's map, and bias metrics are sampling rather than content. Measured: a caster moving inside one face holds `recorded=1 reused=5` at 0.007 ms against 0.141 ms forced. Slot churn is parked behind runtime scene mutation. | B/C | M | §2.3 | | 16 | `hash_combine`-style mix for the mesh-triangle warm-start key (XOR admits collisions across (pair, triangle); consequence is a wrong warm-start seed, self-correcting) | C | XS | §3.3 | | 17 | Retire the TAA resolve→blit full-res 16F copy by treating `history[cur]` as the scene target — **measure on MoltenVK first**; costs per-frame or double-buffered descriptors for three consumers | C | M | §2.5 | | 18 | `vdpmDrawCounts_` / `findSelfShadowViewProj` linear scans are O(fronts²) — a watch-item, not a defect at today's front counts | C | XS | §1.6 | diff --git a/docs/roadmap.md b/docs/roadmap.md index 8977f19..74e7cee 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -81,32 +81,21 @@ The eight small/XS items landed on `review-shadow-taa-fixes` + `review-xs-cleanu the five the review prioritised, then five a later coverage audit found had no action item. In the review's priority order: -- **#15 [B/C, M] Punctual-shadow change detection** (§2.3) — **next, and now mostly verification.** - Arc 2 #4 landed the whole mechanism on `shadow-residency-reuse` and it is family-agnostic: a - static point light's six faces already reuse today, which is what the gate scene measures. What - this item still owes is the evidence and the follow-through — a scene with a light that MOVES - (proving the faces re-record on the frame the light's position or range changes, since both are in - the content descriptor), a spot equivalent, and a decision about per-face granularity: the cube is - compared per face, but slot assignment is per-light, so a light entering or leaving reshuffles - slots and invalidates its neighbours' residency by identity. Worth measuring before assuming it - matters. (Per-face frustum filtering already exists and is correct.) -- **#5 [B/L] Compute pre-skinning pass** (§1.3) — skinning/morphing re-runs in every pass's vertex - shader (~11× per skinned vertex per frame). `SoftBodySystem` already proves the compute pattern - in-engine. The one genuinely architectural piece here; it also retires SH-04's deformable - full-detail fallback by exposing pre-deformed vertices + exact deformed bounds + a deformation - revision. -- **#7 [B/S] Physics per-step scratch persistence** (§3.1) — remove the per-step heap allocation in - the solver hot path. Golden-neutral if done as pure allocation reuse. -- **#10 [B/S] Front-to-back sort of the opaque bucket** (§1.1) — improves depth-prepass rejection. -- **#6 [C/S] Batch image barriers into single `DependencyInfo`s** (§1.2) — compounds on MoltenVK - (§5.2); coordinate with SH-* so barrier grouping doesn't change per-view LOD decisions. - -**Added by a coverage audit** (2026-07-26). The review's §6 table was a *prioritised* list, not an -exhaustive one: five actionable findings in its body had no row. They are now rows 15–19 there and -items here. All five are genuinely lower-value than the above — three are conditional or watch-items -in the review's own words — and are recorded so the arc is scoped honestly, not because each is -worth doing: - +- **#15 ✅ Punctual-shadow change detection** (§2.3) — verified on `shadow-punctual-change-detection` + and closed with **no engine change**, which was the honest outcome: arc 2 #4's mechanism is + family-agnostic and already covered the punctual families. What the branch added is the evidence — + an audit showing every shadow sampling input is re-uploaded from the view set each frame while + every raster input is in the content descriptor (so nothing sits outside both), eight headless + cases pinning the punctual specifics (range-only change, moving light, slot inheritance for both + spot and cube, metrics-are-not-content on both), and two motion scenes measuring the payoff: + `recorded=1 reused=5` at 0.007 ms against 0.141 ms forced, for an identical image. Detail in + [`shadowplans.md`](shadowplans.md) § Interaction; runbook in + [`acceptance-testing.md`](acceptance-testing.md). + + **Parked, not done:** slot churn. A light leaving compacts punctual slots and every inherited slot + re-records a whole cube. Correct today (identity is compared) and unreachable in production (no + runtime light removal or enable/disable path, stable gather order), so a stable-assignment scheme + would guard against nothing. **Trigger: runtime scene mutation, or a light enable/disable toggle.** - **#16 [C, XS] `hash_combine`-style mix for the mesh-triangle warm-start key** (§3.3) — `in.key ^= subKey * 0x9E3779B97F4A7C15ULL` (`physics_world.cpp`) is a decent mix, but XOR over the pair key admits collisions across (pair, triangle) combinations. The consequence is only a wrong diff --git a/docs/shadowplans.md b/docs/shadowplans.md index 4499f29..f186810 100644 --- a/docs/shadowplans.md +++ b/docs/shadowplans.md @@ -1034,6 +1034,38 @@ contracts from this work: `--no-shadow-reuse` (overlay: "Reuse unchanged shadow views"); the runbook is [`acceptance-testing.md`](acceptance-testing.md) § Shadow-residency gate scene. + **Punctual change detection (arc 2 #15) is the same mechanism, verified rather than built.** The + audit that item owed is done, and its result is a boundary worth stating once: every shadow + SAMPLING input — `spotViewProj`, `cascadeViewProj`, `selfShadowViewProj` and all four + `*BiasMetrics` arrays — is copied wholesale from the completed view SET every frame, + unconditionally, never from the plan and never gated on whether a family recorded. Every RASTER + input is in the content descriptor. Nothing sits outside both, which is why a reused map cannot go + stale through a sampling parameter. (Shadow draws are also VDPM-free by construction — `object.cpp` + clears the indirect handle and the GPU front — so a per-frame GPU-emitted index buffer, whose + handle repeats while its contents change, can never enter the descriptor.) + + The punctual specifics are pinned headlessly: a RANGE-only change re-records all six faces (every + matrix identical, every texel different — the case a transform-only descriptor would miss); a + moving light re-records all six, because its position is an input to the radial depth each face + stores; a cube inherited by another light re-records all six rather than lighting one light with + its predecessor's shadows; and metrics-only changes reuse, on both the spot and point paths. + + **Per-face granularity is where the punctual saving actually lives, and it is caster-driven.** + A cube is compared face by face, but a light's own movement invalidates every face by the content + law, so only a CASTER can change one face and not another. Measured on + `ShadowResidencyCasterMotionTest` (one rigid caster moving inside a single face, 15 warm samples): + the point family holds `recorded=1 reused=5` at a median **0.007 ms**, against **0.141 ms** forced + on the same scene — about twenty times less for an identical image. The light-motion scene is the + ceiling: `recorded=6 reused=0`, 0.172 ms, which is what punctual reuse cannot save. + + **Slot churn is correct and deliberately un-optimised.** Punctual slots are assigned per frame in + gather order, so a light leaving compacts the ones after it and every inherited slot re-records a + whole cube it could in principle have kept. That is SAFE — identity is part of the comparison, and + the tests above pin it — and its cost is currently unreachable: the scene graph has no runtime + light removal or enable/disable path, and `gatherLights()` walks a stable node order, so churn + frequency in production is zero. Revisit only when runtime scene mutation or a light enable toggle + arrives; until then a stable-slot-assignment scheme would be machinery guarding against nothing. + **What this does NOT buy: CPU preparation.** A reused view is filtered, resolved and observed in full — the comparison cannot be made without the work that produces its operand. The saving is GPU raster only, which is also why the honest headline case is a static PUNCTUAL light rather than diff --git a/tests/graphics/test_shadow_pass_prepare.cpp b/tests/graphics/test_shadow_pass_prepare.cpp index 2bdaace..3b48e14 100644 --- a/tests/graphics/test_shadow_pass_prepare.cpp +++ b/tests/graphics/test_shadow_pass_prepare.cpp @@ -1,5 +1,7 @@ #include +#include + #include using namespace fire_engine; @@ -651,3 +653,256 @@ TEST_CASE("a view that never engaged reports no disposition at all", "[ShadowPas CHECK(out.plan.disposition(ShadowViewGroup::Spot, unusedSpot) == ShadowViewDisposition::Invalid); } + +// --- punctual change detection (arc 2 #15) ------------------------------------------------------- +// +// The mechanism is arc 2 #4's and is family-agnostic; what these pin is that it actually HOLDS for +// the punctual families, whose lights are the ones that move independently of the camera. Each case +// below is a way a stale punctual map could survive a change nobody compared. + +namespace +{ + +// The fixture's cube, rebuilt around a light that may have moved or re-ranged. Both halves are +// SHADER INPUTS for a point face — the stored depth is `length(worldPos - lightPos) / range` — so +// each must be able to force a re-record on its own. +[[nodiscard]] ShadowRenderViewSet pointCubeAt(Vec3 position, float range, float faceMark = 0.0f) +{ + ShadowRenderViewSet views = populatedViews(); + const std::array forwards{Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0}, + Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1}}; + const auto face = [&](std::uint8_t f) + { return ShadowPointFace{markedMatrix(faceMark), somePerspective(forwards[f], position)}; }; + const std::array cube{face(0), face(1), face(2), + face(3), face(4), face(5)}; + REQUIRE(views.setPointLight(0, kLight, ShadowViewMetrics::pointLight(0.004f, range), range, + std::span{cube})); + return views; +} + +// A cube whose faces carry REAL projection matrices, so each face's frustum admits only what that +// face can see. The shared fixture deliberately uses one marked matrix for all six — fine for the +// accounting rules it was built for, useless here, because six identical frusta admit every caster +// to every face and "only this face re-recorded" would pass without meaning anything. +[[nodiscard]] ShadowRenderViewSet pointCubeWithRealFaces(Vec3 position, float range) +{ + ShadowRenderViewSet views = populatedViews(); + const std::array forwards{Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0}, + Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1}}; + const auto face = [&](std::uint8_t f) + { + // 90 degrees, square aspect — a cube face exactly. `up` must not be parallel to forward, + // which is why the ±Y faces take a different one. + const Vec3 forward = forwards[f]; + const Vec3 up = (f == 2 || f == 3) ? Vec3{0.0f, 0.0f, 1.0f} : Vec3{0.0f, 1.0f, 0.0f}; + const Mat4 viewProj = Mat4::perspective(1.5708f, 1.0f, 0.1f, range) * + Mat4::lookAt(position, position + forward, up); + return ShadowPointFace{viewProj, somePerspective(forward, position)}; + }; + const std::array cube{face(0), face(1), face(2), + face(3), face(4), face(5)}; + REQUIRE(views.setPointLight(0, kLight, ShadowViewMetrics::pointLight(0.004f, range), range, + std::span{cube})); + return views; +} + +} // namespace + +TEST_CASE("a point light that only changes RANGE re-records every face", "[ShadowPassPrepare]") +{ + // The sharpest of the punctual cases, and the one a matrix-only descriptor would fail. Range + // does not appear in a face's projection at all: every matrix here is identical across the two + // frames, and every texel of all six faces still changes, because the stored value is a RATIO + // against that range. A cache comparing only what it could see in the transform would keep six + // maps whose depths are now measured against a different denominator. + const std::vector draws{nearCaster()}; + Prepared out{}; + prepare(out, inputsFor(draws), pointCubeAt(kPointPosition, kPointRange), allFamilies()); + submitFrame(out); + + prepare(out, inputsFor(draws), pointCubeAt(kPointPosition, kPointRange * 2.0f), allFamilies()); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Point, ShadowViewDisposition::Recorded)); + // And nothing else was disturbed: the cascades and the spot saw no change and still reuse. + CHECK(everySlotIs(out.plan, ShadowViewGroup::Cascade, ShadowViewDisposition::Reused)); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Spot, ShadowViewDisposition::Reused)); +} + +TEST_CASE("a point light that moves re-records every face", "[ShadowPassPrepare]") +{ + const std::vector draws{nearCaster()}; + Prepared out{}; + prepare(out, inputsFor(draws), pointCubeAt(kPointPosition, kPointRange), allFamilies()); + submitFrame(out); + + // A cube is one light's map: moving the light changes what every face stores, so a partial + // re-record would leave a light lit from two different positions depending on receiver facing. + prepare(out, inputsFor(draws), + pointCubeAt(kPointPosition + Vec3{0.75f, 0.0f, 0.0f}, kPointRange), allFamilies()); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Point, ShadowViewDisposition::Recorded)); + CHECK(out.plan.pointCubesWhole()); +} + +TEST_CASE("a caster confined to one face re-records that face alone", "[ShadowPassPrepare]") +{ + // Per-FACE granularity is the saving this item is about, and CASTERS are the only thing that + // can exercise it. Not because of how a cube is installed — a light that moves invalidates all + // six faces through the CONTENT LAW, since its position is an input to the radial depth every + // face stores — but because a caster is the only input that can differ BETWEEN faces. (Atomic + // installation is a separate rule: it guarantees a cube is whole, not that it is invalid.) A + // caster only one face can see is where five sixths of the cube stays resident. + const Vec3 onAxis = kPointPosition + Vec3{9.0f, 0.0f, 0.0f}; + std::vector draws{caster(2, boundsAt(onAxis))}; + Prepared out{}; + prepare(out, inputsFor(draws), pointCubeWithRealFaces(kPointPosition, kPointRange), + allFamilies()); + + // The premise, checked rather than assumed: exactly one face offers this caster. Six identical + // frusta would make the assertions below vacuous, and that is precisely what the shared fixture + // has (one marked matrix per face) — hence the real-matrix cube. + // DRAWN, not candidate: every face is OFFERED every caster (that is what a candidate is), and + // the per-face frustum is what decides which of them actually rasterise it. Counting candidates + // here would report six faces seeing it and make the assertions below vacuous. + std::size_t facesDrawingIt = 0; + for (std::size_t slot = 0; slot < kCubeFaceCount; ++slot) + { + facesDrawingIt += out.stats.view(ShadowViewGroup::Point, slot).drawnDraws > 0 ? 1 : 0; + } + REQUIRE(facesDrawingIt == 1); + REQUIRE(out.stats.view(ShadowViewGroup::Point, 0).drawnDraws == 1); + submitFrame(out); + + // Move it along the same axis: only the +X face's content changed. + draws[0].shadowRequest.pose = + ShadowCasterPose::fromModel(Mat4::translate(onAxis + Vec3{0.4f, 0.0f, 0.0f})); + draws[0].shadowBounds = boundsAt(onAxis + Vec3{0.4f, 0.0f, 0.0f}); + prepare(out, inputsFor(draws), pointCubeWithRealFaces(kPointPosition, kPointRange), + allFamilies()); + + CHECK(out.plan.disposition(ShadowViewGroup::Point, 0) == ShadowViewDisposition::Recorded); + for (std::size_t slot = 1; slot < kCubeFaceCount; ++slot) + { + CHECK(out.plan.disposition(ShadowViewGroup::Point, slot) == ShadowViewDisposition::Reused); + } + // Five sixths reused is still a WHOLE cube to the receiver: reuse is sampleable, so the light + // is not half-shadowed while one face catches up. + CHECK(out.plan.pointCubesWhole()); + CHECK(out.plan.sampleableCount(ShadowViewGroup::Point) == kCubeFaceCount); +} + +TEST_CASE("a spot light that moves re-records its view", "[ShadowPassPrepare]") +{ + const std::vector draws{nearCaster()}; + Prepared out{}; + prepare(out, inputsFor(draws), populatedViews(), allFamilies()); + submitFrame(out); + + ShadowRenderViewSet moved = populatedViews(); + REQUIRE(moved.setSpot(0, kLight, markedMatrix(0.25f), + somePerspective(Vec3{0.0f, 0.0f, -1.0f}, Vec3{0.0f, 0.0f, 5.0f}), + ShadowViewMetrics::spot(0.002f, 0.1f, 50.0f))); + prepare(out, inputsFor(draws), moved, allFamilies()); + CHECK(out.plan.disposition(ShadowViewGroup::Spot, 0) == ShadowViewDisposition::Recorded); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Point, ShadowViewDisposition::Reused)); +} + +TEST_CASE("a slot inherited by a different light records rather than reusing", + "[ShadowPassPrepare]") +{ + // Punctual slots are assigned per frame in gather order, so a light leaving the scene COMPACTS + // the ones after it. The residency in slot 0 then describes a map rendered for a light that no + // longer occupies it. Identity is what saves this — content is compared including the logical + // view id — and the failure it prevents is the worst kind: one light lit by another's shadows, + // with every counter and timing still plausible. + const std::vector draws{nearCaster()}; + constexpr auto kOtherLight = static_cast(77); + Prepared out{}; + prepare(out, inputsFor(draws), populatedViews(), allFamilies()); + submitFrame(out); + + ShadowRenderViewSet inherited = populatedViews(); + // The SAME matrix, metrics and slot — only the light differs, which is exactly the case a + // slot-keyed cache would call a hit. + REQUIRE(inherited.setSpot(0, kOtherLight, markedMatrix(0.0f), + somePerspective(Vec3{0.0f, 0.0f, -1.0f}, Vec3{0.0f, 0.0f, 5.0f}), + ShadowViewMetrics::spot(0.002f, 0.1f, 50.0f))); + prepare(out, inputsFor(draws), inherited, allFamilies()); + CHECK(out.plan.disposition(ShadowViewGroup::Spot, 0) == ShadowViewDisposition::Recorded); +} + +TEST_CASE("bias metrics are sampling inputs, not content", "[ShadowPassPrepare]") +{ + // The boundary this whole cache depends on. Metrics never reach the shadow rasteriser — they + // are uploaded to LightUBO every frame from the view SET and read by the RECEIVER — so a + // metrics change must not re-render a map whose depth is identical. If this ever starts + // failing, something has moved a sampling parameter into the raster path, and the fix is there + // rather than here. + const std::vector draws{nearCaster()}; + Prepared out{}; + prepare(out, inputsFor(draws), populatedViews(), allFamilies()); + submitFrame(out); + + ShadowRenderViewSet rebiased = populatedViews(); + REQUIRE(rebiased.setSpot(0, kLight, markedMatrix(0.0f), + somePerspective(Vec3{0.0f, 0.0f, -1.0f}, Vec3{0.0f, 0.0f, 5.0f}), + ShadowViewMetrics::spot(0.03f, 0.2f, 90.0f))); + prepare(out, inputsFor(draws), rebiased, allFamilies()); + CHECK(out.plan.disposition(ShadowViewGroup::Spot, 0) == ShadowViewDisposition::Reused); +} + +TEST_CASE("a point cube inherited by another light records all six faces", "[ShadowPassPrepare]") +{ + // The cube's version of slot inheritance, and it needs its own case rather than trusting the + // spot one: a cube is SIX residency entries, so a law that held for a single view could still + // leave five faces of a departed light's map in place while one re-rendered — a light lit + // correctly from one direction and by its predecessor from the other five. + const std::vector draws{nearCaster()}; + constexpr auto kOtherLight = static_cast(78); + Prepared out{}; + prepare(out, inputsFor(draws), pointCubeAt(kPointPosition, kPointRange), allFamilies()); + submitFrame(out); + + // Same position, same range, same matrices, same slots — only the light's identity differs, + // which is what a compaction after a light leaves the scene produces. + ShadowRenderViewSet inherited = populatedViews(); + const std::array forwards{Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0}, + Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1}}; + const auto face = [&](std::uint8_t f) + { return ShadowPointFace{markedMatrix(0.0f), somePerspective(forwards[f], kPointPosition)}; }; + const std::array cube{face(0), face(1), face(2), + face(3), face(4), face(5)}; + REQUIRE(inherited.setPointLight(0, kOtherLight, + ShadowViewMetrics::pointLight(0.004f, kPointRange), kPointRange, + std::span{cube})); + + prepare(out, inputsFor(draws), inherited, allFamilies()); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Point, ShadowViewDisposition::Recorded)); + CHECK(out.plan.pointCubesWhole()); +} + +TEST_CASE("point bias metrics are sampling inputs, not content", "[ShadowPassPrepare]") +{ + // The point family's version of the boundary, and the distinction it draws is finer than the + // spot one: a point light's RANGE is content (it is the denominator of the stored ratio) while + // the metrics DERIVED from that range are sampling. Changing the metrics alone must therefore + // reuse all six faces — a cube that re-rendered because the receiver's bias inputs moved would + // be doing the work this item exists to avoid, and doing it six times over. + const std::vector draws{nearCaster()}; + Prepared out{}; + prepare(out, inputsFor(draws), pointCubeAt(kPointPosition, kPointRange), allFamilies()); + submitFrame(out); + + ShadowRenderViewSet rebiased = populatedViews(); + const std::array forwards{Vec3{1, 0, 0}, Vec3{-1, 0, 0}, Vec3{0, 1, 0}, + Vec3{0, -1, 0}, Vec3{0, 0, 1}, Vec3{0, 0, -1}}; + const auto face = [&](std::uint8_t f) + { return ShadowPointFace{markedMatrix(0.0f), somePerspective(forwards[f], kPointPosition)}; }; + const std::array cube{face(0), face(1), face(2), + face(3), face(4), face(5)}; + // A different texel scale in the metrics; the RANGE the faces store against is unchanged. + REQUIRE(rebiased.setPointLight(0, kLight, ShadowViewMetrics::pointLight(0.05f, kPointRange), + kPointRange, + std::span{cube})); + + prepare(out, inputsFor(draws), rebiased, allFamilies()); + CHECK(everySlotIs(out.plan, ShadowViewGroup::Point, ShadowViewDisposition::Reused)); +}