@forgeax/engine-runtime 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +183 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/active-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/active-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts +2 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts.map +1 -0
- package/dist/__tests__/append-injection.test.d.ts +2 -0
- package/dist/__tests__/append-injection.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts.map +1 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts +2 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts +2 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts +2 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts.map +1 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts +2 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts +3 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts +2 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts +3 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -0
- package/dist/__tests__/components.test-d.d.ts +2 -0
- package/dist/__tests__/components.test-d.d.ts.map +1 -0
- package/dist/__tests__/components.unit.test.d.ts +2 -0
- package/dist/__tests__/components.unit.test.d.ts.map +1 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts +2 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mipmap-srgb-linear.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mipmap-srgb-linear.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts +2 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts.map +1 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts +2 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-published.d.ts +5 -0
- package/dist/__tests__/draw-published.d.ts.map +1 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts +8 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts +2 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts.map +1 -0
- package/dist/__tests__/error-exhaustive.test.d.ts +2 -0
- package/dist/__tests__/error-exhaustive.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts.map +1 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts +2 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-manifest.d.ts +131 -0
- package/dist/__tests__/fog-producer-manifest.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts +42 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts +5 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts +3 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +1 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +1 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/geometry.unit.test.d.ts +2 -0
- package/dist/__tests__/geometry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts +2 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +2 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +1 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts +2 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts +11 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts.map +1 -0
- package/dist/__tests__/helpers/primitive-world.d.ts +8 -0
- package/dist/__tests__/helpers/primitive-world.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts +15 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts +9 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts +6 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -0
- package/dist/__tests__/instances.test-d.d.ts +2 -0
- package/dist/__tests__/instances.test-d.d.ts.map +1 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts +2 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts.map +1 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts +2 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts.map +1 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts +2 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts.map +1 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts +2 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts.map +1 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights.unit.test.d.ts +2 -0
- package/dist/__tests__/lights.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts +2 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts.map +1 -0
- package/dist/__tests__/materials.unit.test.d.ts +2 -0
- package/dist/__tests__/materials.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts +2 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/name.unit.test.d.ts +2 -0
- package/dist/__tests__/name.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts +2 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-spec.test.d.ts +2 -0
- package/dist/__tests__/pipeline-spec.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +2 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts +2 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts +2 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts +2 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts.map +1 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts +2 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-mega.test.d.ts +2 -0
- package/dist/__tests__/render-system-mega.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts +2 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-result.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-result.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts +2 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-component.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-component.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts +2 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts +2 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts +83 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts +2 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts +14 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts +2 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts +2 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts +2 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts.map +1 -0
- package/dist/__tests__/ssao-bgl.test.d.ts +2 -0
- package/dist/__tests__/ssao-bgl.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-buffers.test.d.ts +2 -0
- package/dist/__tests__/ssao-buffers.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-data.test.d.ts +2 -0
- package/dist/__tests__/ssao-data.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-errors.test.d.ts +2 -0
- package/dist/__tests__/ssao-errors.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-manifest.test.d.ts +2 -0
- package/dist/__tests__/ssao-manifest.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-passes.test.d.ts +2 -0
- package/dist/__tests__/ssao-passes.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts +2 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts +2 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts +2 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts.map +1 -0
- package/dist/__tests__/systems.unit.test.d.ts +2 -0
- package/dist/__tests__/systems.unit.test.d.ts.map +1 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts +2 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +1 -0
- package/dist/__tests__/text.unit.test.d.ts +2 -0
- package/dist/__tests__/text.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts +2 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts +2 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts +2 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts +2 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts +2 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts +2 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts +2 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-validate.test.d.ts +2 -0
- package/dist/__tests__/tilemap-validate.test.d.ts.map +1 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts +2 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts.map +1 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts +2 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts.map +1 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts +2 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts +2 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts.map +1 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts +2 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts +2 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts.map +1 -0
- package/dist/__tests__/world-entity-key.test.d.ts +2 -0
- package/dist/__tests__/world-entity-key.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts +2 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts +2 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-nested.test.d.ts +2 -0
- package/dist/__tests__/world-scene-nested.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-override.test.d.ts +2 -0
- package/dist/__tests__/world-scene-override.test.d.ts.map +1 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts +2 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts.map +1 -0
- package/dist/asset-contributions.d.ts +3 -0
- package/dist/asset-contributions.d.ts.map +1 -0
- package/dist/asset-kind.d.ts +4 -0
- package/dist/asset-kind.d.ts.map +1 -0
- package/dist/collect-scene-asset.d.ts +16 -0
- package/dist/collect-scene-asset.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test-d.d.ts +2 -0
- package/dist/components/__tests__/camera.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test.d.ts +2 -0
- package/dist/components/__tests__/camera.test.d.ts.map +1 -0
- package/dist/components/__tests__/child-of.test.d.ts +2 -0
- package/dist/components/__tests__/child-of.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts +2 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts +2 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts +2 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts.map +1 -0
- package/dist/create-renderer-env-classify.d.ts +21 -0
- package/dist/create-renderer-env-classify.d.ts.map +1 -0
- package/dist/createRenderer.d.ts +3 -0
- package/dist/createRenderer.d.ts.map +1 -0
- package/dist/dev-import-transport.d.ts +24 -0
- package/dist/dev-import-transport.d.ts.map +1 -0
- package/dist/errors/environment.d.ts +2 -0
- package/dist/errors/environment.d.ts.map +1 -0
- package/dist/errors/sprite-animation.d.ts +3 -0
- package/dist/errors/sprite-animation.d.ts.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +814 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scene-asset-port.d.ts +17 -0
- package/dist/scene-asset-port.d.ts.map +1 -0
- package/dist/scene-collect-errors.d.ts +22 -0
- package/dist/scene-collect-errors.d.ts.map +1 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts +2 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts.map +1 -0
- package/dist/scene-utils/mount-override-fold.d.ts +16 -0
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -0
- package/dist/shadow-m3-calibrate.d.ts +40 -0
- package/dist/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/sprite-param-values.d.ts +62 -0
- package/dist/sprite-param-values.d.ts.map +1 -0
- package/dist/systems/__tests__/_xorshift.d.ts +28 -0
- package/dist/systems/__tests__/_xorshift.d.ts.map +1 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts +2 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts.map +1 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts +2 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts.map +1 -0
- package/dist/systems/sprite-animation-tick.d.ts +50 -0
- package/dist/systems/sprite-animation-tick.d.ts.map +1 -0
- package/dist/systems/tonemap.d.ts +27 -0
- package/dist/systems/tonemap.d.ts.map +1 -0
- package/dist/systems/transparent-sort.d.ts +51 -0
- package/dist/systems/transparent-sort.d.ts.map +1 -0
- package/dist/tilemap-register-validate.d.ts +21 -0
- package/dist/tilemap-register-validate.d.ts.map +1 -0
- package/package.json +100 -0
- package/src/__tests__/active-camera.unit.test.ts +193 -0
- package/src/__tests__/animation-graph-add.unit.test.ts +67 -0
- package/src/__tests__/animation-graph-asset-union.test-d.ts +83 -0
- package/src/__tests__/animation-graph-blend.unit.test.ts +66 -0
- package/src/__tests__/animation-graph-build.unit.test.ts +100 -0
- package/src/__tests__/animation-graph-clip-missing.unit.test.ts +62 -0
- package/src/__tests__/animation-graph-clip.unit.test.ts +77 -0
- package/src/__tests__/animation-graph-default-path.integration.test.ts +65 -0
- package/src/__tests__/animation-graph-describe.unit.test.ts +68 -0
- package/src/__tests__/animation-graph-errors.unit.test.ts +75 -0
- package/src/__tests__/animation-graph-nesting.unit.test.ts +84 -0
- package/src/__tests__/animation-graph-orthogonal-weight.unit.test.ts +98 -0
- package/src/__tests__/animation-graph-slot-fill.unit.test.ts +95 -0
- package/src/__tests__/animation-nslot.unit.test.ts +63 -0
- package/src/__tests__/animation-slot-length-guard.unit.test.ts +81 -0
- package/src/__tests__/append-injection.test.ts +105 -0
- package/src/__tests__/asi-world-baseline-export.unit.test.ts +173 -0
- package/src/__tests__/asi-world-ci-config.unit.test.ts +186 -0
- package/src/__tests__/asi-world-smoke.unit.test.ts +192 -0
- package/src/__tests__/asset-registry-scene.test-d.ts +49 -0
- package/src/__tests__/bind-group-cache-binding.test.ts +2513 -0
- package/src/__tests__/bind-group-cache-frame.test.ts +1511 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +295 -0
- package/src/__tests__/block-upload-math.unit.test.ts +192 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +151 -0
- package/src/__tests__/cache-false-eviction.test.ts +126 -0
- package/src/__tests__/cache-isolation-matrix.test.ts +182 -0
- package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +76 -0
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +183 -0
- package/src/__tests__/clamp-to-last.e2e.dawn.test.ts +325 -0
- package/src/__tests__/collect-scene-asset.test.ts +876 -0
- package/src/__tests__/collect-subtree.unit.test.ts +76 -0
- package/src/__tests__/collect-transient-children-absent.test.ts +154 -0
- package/src/__tests__/collect-transient-roundtrip.test.ts +245 -0
- package/src/__tests__/collect-transient-skip.test.ts +165 -0
- package/src/__tests__/components.test-d.ts +236 -0
- package/src/__tests__/components.unit.test.ts +2665 -0
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +378 -0
- package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -0
- package/src/__tests__/createRenderer-dispose.test.ts +416 -0
- package/src/__tests__/createRenderer-requiredFeatures.unit.test.ts +134 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +59 -0
- package/src/__tests__/createRenderer.unit.test.ts +231 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +393 -0
- package/src/__tests__/dawn/ibl-compose.dawn.test.ts +151 -0
- package/src/__tests__/dawn/ibl-modules-compose.dawn.test.ts +223 -0
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +241 -0
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +611 -0
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +443 -0
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +313 -0
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +304 -0
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +64 -0
- package/src/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.ts +106 -0
- package/src/__tests__/dawn/mesh-update.dawn.test.ts +342 -0
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +117 -0
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +567 -0
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +251 -0
- package/src/__tests__/dawn/tonemap.dawn.test.ts +107 -0
- package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +291 -0
- package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +204 -0
- package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +122 -0
- package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +99 -0
- package/src/__tests__/demo-sortscope-migration.unit.test.ts +115 -0
- package/src/__tests__/derive-bgl-integration.test.ts +398 -0
- package/src/__tests__/derive-render-data-skin.unit.test.ts +90 -0
- package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +204 -0
- package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +297 -0
- package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +271 -0
- package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +321 -0
- package/src/__tests__/draw-published.ts +10 -0
- package/src/__tests__/environment-error-exhaustive.test-d.ts +28 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +35 -0
- package/src/__tests__/error-exhaustive.test.ts +220 -0
- package/src/__tests__/extract-frames-graceful-degradation.test.ts +298 -0
- package/src/__tests__/extract-frames-merge.test.ts +643 -0
- package/src/__tests__/extract-frames-reset.test.ts +158 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +92 -0
- package/src/__tests__/fog-producer-manifest.ts +162 -0
- package/src/__tests__/fog-producer-pixel-probe.ts +263 -0
- package/src/__tests__/fog-producer-pixel.browser.test.ts +26 -0
- package/src/__tests__/fog-producer-pixel.dawn.test.ts +55 -0
- package/src/__tests__/fog-producer-real-fixture.ts +805 -0
- package/src/__tests__/fog-producer-vfx-effect.ts +272 -0
- package/src/__tests__/fog-scene-asset.integration.test.ts +123 -0
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +28 -0
- package/src/__tests__/fullscreen-post-bgl-depth.test.ts +131 -0
- package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +84 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +320 -0
- package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +107 -0
- package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +292 -0
- package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +358 -0
- package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +254 -0
- package/src/__tests__/geometry.unit.test.ts +2265 -0
- package/src/__tests__/glyph-tilemap-worldid.test.ts +146 -0
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +388 -0
- package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +207 -0
- package/src/__tests__/gpu-resource.test.ts +1206 -0
- package/src/__tests__/hdrp-bgl-slots.browser.test.ts +50 -0
- package/src/__tests__/hdrp-buffers.unit.test.ts +70 -0
- package/src/__tests__/hdrp-execute-filter.unit.test.ts +200 -0
- package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +64 -0
- package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +139 -0
- package/src/__tests__/hdrp-ssao-boundary.test.ts +237 -0
- package/src/__tests__/hdrp-ssao-falsification.test.ts +289 -0
- package/src/__tests__/hello-tilemap-smoke-upgrade.unit.test.ts +147 -0
- package/src/__tests__/helpers/mock-shader-registry.ts +139 -0
- package/src/__tests__/helpers/primitive-world.ts +91 -0
- package/src/__tests__/helpers/register-runtime-components.ts +69 -0
- package/src/__tests__/helpers/register-scene-components.ts +10 -0
- package/src/__tests__/helpers/render-asset-port.ts +65 -0
- package/src/__tests__/helpers/scene-asset-port.ts +23 -0
- package/src/__tests__/helpers/tilemap-assets.ts +29 -0
- package/src/__tests__/instances.test-d.ts +61 -0
- package/src/__tests__/light-attenuation-formula.test.ts +171 -0
- package/src/__tests__/light-casters-9-light.browser.test.ts +880 -0
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +163 -0
- package/src/__tests__/light-snapshot.test-d.ts +84 -0
- package/src/__tests__/lights-vec-negative.unit.test.ts +52 -0
- package/src/__tests__/lights.unit.test.ts +2936 -0
- package/src/__tests__/m3-vec-negative.unit.test.ts +45 -0
- package/src/__tests__/m5-browser-dawn-sweep.integration.test.ts +8 -0
- package/src/__tests__/material-derived-coordinates.integration.test.ts +100 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +49 -0
- package/src/__tests__/materials.unit.test.ts +271 -0
- package/src/__tests__/mesh-ssbo-grow.unit.test.ts +244 -0
- package/src/__tests__/mip-gate.unit.test.ts +79 -0
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +406 -0
- package/src/__tests__/multi-uv-8set.e2e.dawn.test.ts +296 -0
- package/src/__tests__/name.unit.test.ts +123 -0
- package/src/__tests__/node-shims.d.ts +33 -0
- package/src/__tests__/pass-selector.unit.test.ts +102 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +204 -0
- package/src/__tests__/pipeline-builder-msaa.unit.test.ts +224 -0
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +316 -0
- package/src/__tests__/pipeline-layout-selection.test.ts +298 -0
- package/src/__tests__/pipeline-spec.test.ts +1061 -0
- package/src/__tests__/pipeline.unit.test.ts +6974 -0
- package/src/__tests__/point-light-shadow.browser.test.ts +278 -0
- package/src/__tests__/point-light-shadow.dawn.test.ts +822 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +716 -0
- package/src/__tests__/post-process-error-codes.unit.test.ts +158 -0
- package/src/__tests__/post-process-params-component.unit.test.ts +153 -0
- package/src/__tests__/post-process-params-extract.unit.test.ts +100 -0
- package/src/__tests__/post-process-reads-narrowing.test-d.ts +52 -0
- package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +230 -0
- package/src/__tests__/postprocess-camera-consistency.test.ts +378 -0
- package/src/__tests__/profiler-assembly.integration.test.ts +74 -0
- package/src/__tests__/propagate-transforms.dawn.test.ts +253 -0
- package/src/__tests__/record-worldid-isolation.test.ts +247 -0
- package/src/__tests__/recover-error-exhaustive.test-d.ts +35 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +245 -0
- package/src/__tests__/render-feature-assembly.test-d.ts +28 -0
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +124 -0
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +582 -0
- package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +602 -0
- package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +264 -0
- package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +361 -0
- package/src/__tests__/render-feature-recover.integration.test.ts +142 -0
- package/src/__tests__/render-feature-runtime-surface.test-d.ts +45 -0
- package/src/__tests__/render-feature-zero-feature.browser.test.ts +152 -0
- package/src/__tests__/render-pipeline-trivial.dawn.test.ts +525 -0
- package/src/__tests__/render-schema-consumer.integration.test.ts +25 -0
- package/src/__tests__/render-schema-owner-boundary.unit.test.ts +26 -0
- package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +172 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +322 -0
- package/src/__tests__/render-system-extract.test.ts +1399 -0
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +288 -0
- package/src/__tests__/render-system-fold-fallback.unit.test.ts +148 -0
- package/src/__tests__/render-system-fold-material.unit.test.ts +145 -0
- package/src/__tests__/render-system-fold-metrics.unit.test.ts +170 -0
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +166 -0
- package/src/__tests__/render-system-fold-scene-instances.unit.test.ts +51 -0
- package/src/__tests__/render-system-fold.dawn.test.ts +306 -0
- package/src/__tests__/render-system-mega.test.ts +1578 -0
- package/src/__tests__/render-system-owner-split-contract.test.ts +200 -0
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +684 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +464 -0
- package/src/__tests__/render-system-record.test.ts +1505 -0
- package/src/__tests__/render-system-record.unit.test.ts +270 -0
- package/src/__tests__/render-system-skin-bg.unit.test.ts +147 -0
- package/src/__tests__/renderer-consumer.test-d.ts +5 -0
- package/src/__tests__/renderer-health.unit.test.ts +935 -0
- package/src/__tests__/renderer-lifecycle.integration.test.ts +29 -0
- package/src/__tests__/renderer-recover.unit.test.ts +1075 -0
- package/src/__tests__/renderer-result.test-d.ts +26 -0
- package/src/__tests__/renderer-surface.unit.test.ts +416 -0
- package/src/__tests__/resolve-depth-only-view.test.ts +173 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +273 -0
- package/src/__tests__/rhi-null-noop-behavior.unit.test.ts +196 -0
- package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +182 -0
- package/src/__tests__/roots-to-scene-asset.test.ts +605 -0
- package/src/__tests__/runtime-authority-scan.unit.test.ts +19 -0
- package/src/__tests__/runtime-domain-residue.unit.test.ts +13 -0
- package/src/__tests__/scene-equirect-roundtrip.test.ts +33 -0
- package/src/__tests__/scene-instance-bridge.integration.test.ts +8 -0
- package/src/__tests__/scene-instance-component.test.ts +45 -0
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +66 -0
- package/src/__tests__/serialize-scene-asset.test.ts +336 -0
- package/src/__tests__/shadow-caster-empty-schema.test.ts +83 -0
- package/src/__tests__/shadow-csm-atlas.test.ts +178 -0
- package/src/__tests__/shadow-csm-blend.dawn.test.ts +230 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +334 -0
- package/src/__tests__/shadow-csm-caster-membership.test.ts +232 -0
- package/src/__tests__/shadow-csm-component.test.ts +286 -0
- package/src/__tests__/shadow-csm-extract.test.ts +334 -0
- package/src/__tests__/shadow-csm-orthoz.test.ts +151 -0
- package/src/__tests__/shadow-csm-pssm.test.ts +142 -0
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +198 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +358 -0
- package/src/__tests__/shadow-csm-temporal.browser.test.ts +145 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +419 -0
- package/src/__tests__/shadow-csm-ubo.dawn.test.ts +293 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +300 -0
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +85 -0
- package/src/__tests__/shadow-csm-viewport.test.ts +74 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +615 -0
- package/src/__tests__/shadow-m1.dawn.test.ts +105 -0
- package/src/__tests__/shadow-m2.dawn.test.ts +394 -0
- package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +248 -0
- package/src/__tests__/shadow-m3-calibrate.ts +203 -0
- package/src/__tests__/shadow-m3.dawn.test.ts +288 -0
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +64 -0
- package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +264 -0
- package/src/__tests__/shadow-quality-metrics.dawn.test.ts +324 -0
- package/src/__tests__/skin-error-exhaustive.test-d.ts +74 -0
- package/src/__tests__/skin-errors.unit.test.ts +10 -0
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +23 -0
- package/src/__tests__/skin-parented-double-transform.test.ts +268 -0
- package/src/__tests__/skylight-lazy-projection.test.ts +359 -0
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +43 -0
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +470 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +141 -0
- package/src/__tests__/sprite-lit-extract-branch.test.ts +117 -0
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +123 -0
- package/src/__tests__/sprite-param-values.test-d.ts +104 -0
- package/src/__tests__/ssao-bgl.test.ts +347 -0
- package/src/__tests__/ssao-buffers.test.ts +211 -0
- package/src/__tests__/ssao-data.test.ts +121 -0
- package/src/__tests__/ssao-errors.test.ts +162 -0
- package/src/__tests__/ssao-manifest.test.ts +109 -0
- package/src/__tests__/ssao-passes.test.ts +1229 -0
- package/src/__tests__/ssao-pipelines.test.ts +275 -0
- package/src/__tests__/ssao-uniform-layout.test.ts +178 -0
- package/src/__tests__/static-rig.integration.test.ts +9 -0
- package/src/__tests__/stencil-outline-pixel.dawn.test.ts +295 -0
- package/src/__tests__/swap-chain-format.unit.test.ts +117 -0
- package/src/__tests__/systems.unit.test.ts +9957 -0
- package/src/__tests__/temporal-off-path.unit.test.ts +69 -0
- package/src/__tests__/text.unit.test.ts +466 -0
- package/src/__tests__/tilemap-childed-despawn.test.ts +468 -0
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +314 -0
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +80 -0
- package/src/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.ts +424 -0
- package/src/__tests__/tilemap-chunk-extract.test.ts +236 -0
- package/src/__tests__/tilemap-chunk-extract.unit.test.ts +383 -0
- package/src/__tests__/tilemap-chunk-y-sort-bench.unit.test.ts +109 -0
- package/src/__tests__/tilemap-dirty-rebuild.test.ts +300 -0
- package/src/__tests__/tilemap-material-cache.test.ts +137 -0
- package/src/__tests__/tilemap-multi-atlas.test.ts +237 -0
- package/src/__tests__/tilemap-owner-boundary.unit.test.ts +63 -0
- package/src/__tests__/tilemap-region-pack.test.ts +153 -0
- package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +274 -0
- package/src/__tests__/tilemap-uv-inset.test.ts +204 -0
- package/src/__tests__/tilemap-validate.test.ts +201 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +274 -0
- package/src/__tests__/transient-fields-collect.test.ts +203 -0
- package/src/__tests__/transitional-authoring.integration.test.ts +7 -0
- package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +227 -0
- package/src/__tests__/variant-selection.unit.test.ts +21 -0
- package/src/__tests__/vertex-attribute-layout.test.ts +324 -0
- package/src/__tests__/video-cache-regression.unit.test.ts +186 -0
- package/src/__tests__/video-extract-bindgroup.dawn.test.ts +450 -0
- package/src/__tests__/view-ubo-layout.browser.test.ts +183 -0
- package/src/__tests__/world-entity-key.test.ts +92 -0
- package/src/__tests__/world-scene-despawn.test.ts +132 -0
- package/src/__tests__/world-scene-instantiate.test.ts +228 -0
- package/src/__tests__/world-scene-nested.test.ts +230 -0
- package/src/__tests__/world-scene-override.test.ts +199 -0
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +107 -0
- package/src/asset-contributions.ts +48 -0
- package/src/asset-kind.ts +7 -0
- package/src/collect-scene-asset.ts +689 -0
- package/src/components/__tests__/camera.test-d.ts +57 -0
- package/src/components/__tests__/camera.test.ts +95 -0
- package/src/components/__tests__/child-of.test.ts +35 -0
- package/src/components/__tests__/sprite-animation.test-d.ts +160 -0
- package/src/components/__tests__/sprite-instances.unit.test.ts +108 -0
- package/src/components/__tests__/sprite-region-override.test-d.ts +101 -0
- package/src/components/__tests__/tile-layer.test-d.ts +33 -0
- package/src/components/__tests__/tile-layer.unit.test.ts +125 -0
- package/src/components/__tests__/tilemap.test-d.ts +35 -0
- package/src/components/__tests__/transform-vec-schema.test.ts +121 -0
- package/src/create-renderer-env-classify.ts +110 -0
- package/src/createRenderer.ts +61 -0
- package/src/dev-import-transport.ts +97 -0
- package/src/errors/environment.ts +6 -0
- package/src/errors/sprite-animation.ts +5 -0
- package/src/index.ts +442 -0
- package/src/scene-asset-port.ts +32 -0
- package/src/scene-collect-errors.ts +30 -0
- package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +183 -0
- package/src/scene-utils/mount-override-fold.ts +217 -0
- package/src/shadow-m3-calibrate.ts +171 -0
- package/src/sprite-param-values.ts +61 -0
- package/src/systems/__tests__/_xorshift.ts +83 -0
- package/src/systems/__tests__/argsort-in-place.test.ts +155 -0
- package/src/systems/__tests__/transparent-sort.bench.ts +218 -0
- package/src/systems/sprite-animation-tick.ts +302 -0
- package/src/systems/tonemap.ts +68 -0
- package/src/systems/transparent-sort.ts +434 -0
- package/src/tilemap-register-validate.ts +100 -0
|
@@ -0,0 +1,2936 @@
|
|
|
1
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
2
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
|
|
3
|
+
//
|
|
4
|
+
// Source files (N=13):
|
|
5
|
+
// - packages/runtime/src/__tests__/directional-light-defaults.test.ts
|
|
6
|
+
// - packages/runtime/src/__tests__/directional-light-shadow.test.ts
|
|
7
|
+
// - packages/runtime/src/__tests__/extract-frame-lights.test.ts
|
|
8
|
+
// - packages/runtime/src/__tests__/inspector-lights-bucket.test.ts
|
|
9
|
+
// - packages/runtime/src/__tests__/light-attenuation-cone.test.ts
|
|
10
|
+
// - packages/runtime/src/__tests__/light-buffer-layout.test.ts
|
|
11
|
+
// - packages/runtime/src/__tests__/light-helpers.test.ts
|
|
12
|
+
// - packages/runtime/src/__tests__/lightslot-layout.test.ts
|
|
13
|
+
// - packages/runtime/src/__tests__/point-light-defaults.test.ts
|
|
14
|
+
// - packages/runtime/src/__tests__/point-light-spawn-bounds.test.ts
|
|
15
|
+
// - packages/runtime/src/__tests__/render-multi-light-cap.test.ts
|
|
16
|
+
// - packages/runtime/src/__tests__/spot-light-defaults.test.ts
|
|
17
|
+
// - packages/runtime/src/__tests__/spot-light-spawn-bounds.test.ts
|
|
18
|
+
//
|
|
19
|
+
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
20
|
+
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
21
|
+
|
|
22
|
+
import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
23
|
+
import { World } from '@forgeax/engine-ecs';
|
|
24
|
+
import { componentDefinition, componentSchema } from '@forgeax/engine-ecs/internal';
|
|
25
|
+
import { vec3 } from '@forgeax/engine-math';
|
|
26
|
+
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
27
|
+
import type {
|
|
28
|
+
PointLightSnapshot,
|
|
29
|
+
ShadowInvalidConfigError,
|
|
30
|
+
SpotLightSnapshot,
|
|
31
|
+
} from '@forgeax/engine-render/internal';
|
|
32
|
+
import {
|
|
33
|
+
BYTES_PER_LIGHT_SLOT,
|
|
34
|
+
buildPbrViewBglEntries,
|
|
35
|
+
Camera,
|
|
36
|
+
computeInvRangeSquared,
|
|
37
|
+
DirectionalLight,
|
|
38
|
+
degToCos,
|
|
39
|
+
extractFrame,
|
|
40
|
+
LIGHTSLOT_LAYOUT,
|
|
41
|
+
LightSlotKind,
|
|
42
|
+
PointLight,
|
|
43
|
+
packLightArrayHeader,
|
|
44
|
+
packPointLight,
|
|
45
|
+
packSpotLight,
|
|
46
|
+
prepareExtractContext,
|
|
47
|
+
Skylight,
|
|
48
|
+
SpotLight,
|
|
49
|
+
validateDirectionalLightData,
|
|
50
|
+
validateSpotLightData,
|
|
51
|
+
} from '@forgeax/engine-render/internal';
|
|
52
|
+
import { propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
53
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
54
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
55
|
+
|
|
56
|
+
type LightSpawnResult = ReturnType<World['spawn']>;
|
|
57
|
+
|
|
58
|
+
function spawnValidatedLight(
|
|
59
|
+
world: World,
|
|
60
|
+
component: typeof DirectionalLight | typeof SpotLight,
|
|
61
|
+
data: Readonly<Record<string, unknown>>,
|
|
62
|
+
): LightSpawnResult {
|
|
63
|
+
const validation =
|
|
64
|
+
component === DirectionalLight
|
|
65
|
+
? validateDirectionalLightData(data)
|
|
66
|
+
: validateSpotLightData(data);
|
|
67
|
+
if (!validation.ok) return validation as unknown as LightSpawnResult;
|
|
68
|
+
return world.spawn({ component, data: data as never });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
// ─── feat-20260709 M2 / w4: vec-collapse schema shape + default equivalence ───
|
|
73
|
+
// AC-01 + E1: direction is array<f32,3> with NO default (D-5 -- the only
|
|
74
|
+
// field deliberately left defaultless so an omitted/zero direction is a
|
|
75
|
+
// fail-fast validate rejection, not a silent all-zero); color is array<f32,3>
|
|
76
|
+
// with an explicit layer-2 default [1,1,1] (Skylight/Point/Spot/Dir share the
|
|
77
|
+
// white default; the array layer-3 fallback is all-zero, so the default MUST
|
|
78
|
+
// be explicit -- Transform quat/scale precedent).
|
|
79
|
+
describe('lights vec-collapse schema shape (w4, AC-01 + E1)', () => {
|
|
80
|
+
it('DirectionalLight direction/color are array<f32,3>; color default [1,1,1], direction no default', () => {
|
|
81
|
+
expect(componentSchema(DirectionalLight).direction).toBe('array<f32, 3>');
|
|
82
|
+
expect(componentSchema(DirectionalLight).color).toBe('array<f32, 3>');
|
|
83
|
+
expect('directionX' in componentSchema(DirectionalLight)).toBe(false);
|
|
84
|
+
expect('colorR' in componentSchema(DirectionalLight)).toBe(false);
|
|
85
|
+
const defaults = componentDefinition(DirectionalLight).defaults as Record<string, unknown>;
|
|
86
|
+
expect(Array.from(defaults.color as Float32Array)).toEqual([1, 1, 1]);
|
|
87
|
+
expect('direction' in defaults).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('SpotLight direction/color are array<f32,3>; color default [1,1,1], direction no default', () => {
|
|
91
|
+
expect(componentSchema(SpotLight).direction).toBe('array<f32, 3>');
|
|
92
|
+
expect(componentSchema(SpotLight).color).toBe('array<f32, 3>');
|
|
93
|
+
expect('directionX' in componentSchema(SpotLight)).toBe(false);
|
|
94
|
+
expect('colorR' in componentSchema(SpotLight)).toBe(false);
|
|
95
|
+
const defaults = componentDefinition(SpotLight).defaults as Record<string, unknown>;
|
|
96
|
+
expect(Array.from(defaults.color as Float32Array)).toEqual([1, 1, 1]);
|
|
97
|
+
expect('direction' in defaults).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('PointLight color is array<f32,3> with default [1,1,1]', () => {
|
|
101
|
+
expect(componentSchema(PointLight).color).toBe('array<f32, 3>');
|
|
102
|
+
expect('colorR' in componentSchema(PointLight)).toBe(false);
|
|
103
|
+
const defaults = componentDefinition(PointLight).defaults as Record<string, unknown>;
|
|
104
|
+
expect(Array.from(defaults.color as Float32Array)).toEqual([1, 1, 1]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('Skylight color is array<f32,3> with default [1,1,1]', () => {
|
|
108
|
+
expect(componentSchema(Skylight).color).toBe('array<f32, 3>');
|
|
109
|
+
expect('colorR' in componentSchema(Skylight)).toBe(false);
|
|
110
|
+
const defaults = componentDefinition(Skylight).defaults as Record<string, unknown>;
|
|
111
|
+
expect(Array.from(defaults.color as Float32Array)).toEqual([1, 1, 1]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('E1: omitting color spawns the same white light as the pre-collapse scalar default', () => {
|
|
115
|
+
const world = new World();
|
|
116
|
+
const dir = world
|
|
117
|
+
.spawn({ component: DirectionalLight, data: { direction: [0, -1, 0] } })
|
|
118
|
+
.unwrap();
|
|
119
|
+
expect(Array.from(world.get(dir, DirectionalLight).unwrap().color)).toEqual([1, 1, 1]);
|
|
120
|
+
|
|
121
|
+
const point = world.spawn({ component: PointLight, data: {} }).unwrap();
|
|
122
|
+
expect(Array.from(world.get(point, PointLight).unwrap().color)).toEqual([1, 1, 1]);
|
|
123
|
+
|
|
124
|
+
const sky = world.spawn({ component: Skylight, data: {} }).unwrap();
|
|
125
|
+
expect(Array.from(world.get(sky, Skylight).unwrap().color)).toEqual([1, 1, 1]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('serialized spawn values round-trip as array<f32,3> direction/color', () => {
|
|
129
|
+
const world = new World();
|
|
130
|
+
const e = world
|
|
131
|
+
.spawn({
|
|
132
|
+
component: SpotLight,
|
|
133
|
+
data: { direction: [0.1, -0.9, 0.2], color: [0.3, 0.4, 0.5] },
|
|
134
|
+
})
|
|
135
|
+
.unwrap();
|
|
136
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
137
|
+
expect(Array.from(view.direction)).toEqual([
|
|
138
|
+
expect.closeTo(0.1, 5),
|
|
139
|
+
expect.closeTo(-0.9, 5),
|
|
140
|
+
expect.closeTo(0.2, 5),
|
|
141
|
+
]);
|
|
142
|
+
expect(Array.from(view.color)).toEqual([
|
|
143
|
+
expect.closeTo(0.3, 5),
|
|
144
|
+
expect.closeTo(0.4, 5),
|
|
145
|
+
expect.closeTo(0.5, 5),
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
describe('direct-light physical semantic inputs', () => {
|
|
153
|
+
it('keeps the three public light kinds on the shared intensity and range surface', () => {
|
|
154
|
+
expect(componentSchema(DirectionalLight).intensity).toBe('f32');
|
|
155
|
+
expect(componentSchema(PointLight).intensity).toBe('f32');
|
|
156
|
+
expect(componentSchema(SpotLight).intensity).toBe('f32');
|
|
157
|
+
expect(componentSchema(PointLight).range).toBe('f32');
|
|
158
|
+
expect(componentSchema(SpotLight).range).toBe('f32');
|
|
159
|
+
expect('physicalIntensity' in componentSchema(PointLight)).toBe(false);
|
|
160
|
+
expect('physicalIntensity' in componentSchema(SpotLight)).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('uses meters for finite point and spot ranges without a hidden zero-range multiplier', () => {
|
|
164
|
+
const world = new World();
|
|
165
|
+
const point = world.spawn({ component: PointLight, data: { range: 2 } }).unwrap();
|
|
166
|
+
const spot = world
|
|
167
|
+
.spawn({ component: SpotLight, data: { direction: [0, -1, 0], range: 2 } })
|
|
168
|
+
.unwrap();
|
|
169
|
+
|
|
170
|
+
expect(world.get(point, PointLight).unwrap().range).toBe(2);
|
|
171
|
+
expect(world.get(spot, SpotLight).unwrap().range).toBe(2);
|
|
172
|
+
expect(computeInvRangeSquared(0)).toBe(1e8);
|
|
173
|
+
expect(computeInvRangeSquared(Number.POSITIVE_INFINITY)).toBe(0);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
// ─── feat-20260709 M2 / w5: direction fail-fast validate rejection ───
|
|
180
|
+
// AC-03 (+ AC-02 per research-decisions D-R1): direction has NO layer-2
|
|
181
|
+
// default, so an omitted direction lands the array layer-3 all-zero [0,0,0].
|
|
182
|
+
// Both an explicit zero-vector and an omitted direction are the same illegal
|
|
183
|
+
// state -- the light's validate() rejects both with SpawnLightInvalidBoundsError
|
|
184
|
+
// (detail.field='direction'). AC-02's compile-time "omit direction is red"
|
|
185
|
+
// is unreachable (spawn data is Partial<InputShapeOf>, all fields optional);
|
|
186
|
+
// D-R1 folds it into this runtime rejection. The compile-time guarantee that
|
|
187
|
+
// survives is AC-04 (residual per-axis key = excess-unknown-key typecheck red).
|
|
188
|
+
describe('lights direction fail-fast validate (w5, AC-03 / AC-02 via D-R1)', () => {
|
|
189
|
+
for (const [name, component] of [
|
|
190
|
+
['DirectionalLight', DirectionalLight],
|
|
191
|
+
['SpotLight', SpotLight],
|
|
192
|
+
] as const) {
|
|
193
|
+
it(`${name}: explicit zero-vector direction is rejected with field='direction'`, () => {
|
|
194
|
+
const world = new World();
|
|
195
|
+
const r = spawnValidatedLight(world, component, { direction: [0, 0, 0] });
|
|
196
|
+
expect(r.ok).toBe(false);
|
|
197
|
+
if (!r.ok) {
|
|
198
|
+
expect(r.error.code).toBe('spawn-light-invalid-bounds');
|
|
199
|
+
const detail = (r.error as unknown as { detail: { field: string } }).detail;
|
|
200
|
+
expect(detail.field).toBe('direction');
|
|
201
|
+
const hint = (r.error as unknown as { hint: string }).hint;
|
|
202
|
+
expect(hint.length).toBeGreaterThan(0);
|
|
203
|
+
expect(hint).toContain('direction');
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it(`${name}: omitted direction (layer-3 all-zero) is rejected with field='direction'`, () => {
|
|
208
|
+
const world = new World();
|
|
209
|
+
const r = spawnValidatedLight(world, component, {});
|
|
210
|
+
expect(r.ok).toBe(false);
|
|
211
|
+
if (!r.ok) {
|
|
212
|
+
expect(r.error.code).toBe('spawn-light-invalid-bounds');
|
|
213
|
+
const detail = (r.error as unknown as { detail: { field: string } }).detail;
|
|
214
|
+
expect(detail.field).toBe('direction');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it(`${name}: a non-zero direction spawns successfully`, () => {
|
|
219
|
+
const world = new World();
|
|
220
|
+
const r = world.spawn({ component, data: { direction: [0, -1, 0] } });
|
|
221
|
+
expect(r.ok).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
{
|
|
228
|
+
// ─── from directional-light-defaults.test.ts ───
|
|
229
|
+
describe('directional-light-defaults.test.ts', () => {
|
|
230
|
+
describe('DirectionalLight spawn default-value fallback (M1 w5)', () => {
|
|
231
|
+
it('omitting intensity / color* fills layer-2 defaults (intensity=1, color=[1,1,1])', () => {
|
|
232
|
+
const world = new World();
|
|
233
|
+
const e = world
|
|
234
|
+
.spawn({
|
|
235
|
+
component: DirectionalLight,
|
|
236
|
+
data: { direction: [0, -1, 0] },
|
|
237
|
+
})
|
|
238
|
+
.unwrap();
|
|
239
|
+
|
|
240
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
241
|
+
expect(Array.from(view.direction)).toEqual([0, -1, 0]);
|
|
242
|
+
expect(Array.from(view.color)).toEqual([1, 1, 1]);
|
|
243
|
+
expect(view.intensity).toBe(1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('explicit intensity / color override defaults', () => {
|
|
247
|
+
const world = new World();
|
|
248
|
+
const e = world
|
|
249
|
+
.spawn({
|
|
250
|
+
component: DirectionalLight,
|
|
251
|
+
data: {
|
|
252
|
+
direction: [-0.5, -1, -0.3],
|
|
253
|
+
color: [0.9, 0.8, 0.7],
|
|
254
|
+
intensity: 0.5,
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
.unwrap();
|
|
258
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
259
|
+
expect(view.color[0]).toBeCloseTo(0.9, 5);
|
|
260
|
+
expect(view.intensity).toBe(0.5);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
{
|
|
267
|
+
// ─── from directional-light-shadow.test.ts (post-merge: DirectionalLightShadow deleted, target now DirectionalLight) ───
|
|
268
|
+
describe('directional-light-shadow.test.ts', () => {
|
|
269
|
+
describe('DirectionalLight merged shadow schema (post-m1-t6)', () => {
|
|
270
|
+
it('AC-01: shadow fields are present with correct default values in the merged component', () => {
|
|
271
|
+
const dl = DirectionalLight;
|
|
272
|
+
|
|
273
|
+
expect(dl.name).toBe('DirectionalLight');
|
|
274
|
+
expect(componentSchema(dl)).toBeDefined();
|
|
275
|
+
|
|
276
|
+
expect(componentDefinition(dl).defaults).toBeDefined();
|
|
277
|
+
// biome-ignore lint/style/noNonNullAssertion: defaults asserted defined just above
|
|
278
|
+
const defaults = componentDefinition(dl).defaults!;
|
|
279
|
+
|
|
280
|
+
expect(defaults.cascadeCount).toBe(4);
|
|
281
|
+
expect(defaults.splitLambda).toBeCloseTo(0.75, 5);
|
|
282
|
+
expect(defaults.cascadeBlend).toBeCloseTo(0.2, 5);
|
|
283
|
+
expect(defaults.mapSize).toBe(2048);
|
|
284
|
+
expect(defaults.depthBias).toBeCloseTo(0.005, 5);
|
|
285
|
+
expect(defaults.normalBias).toBeCloseTo(0.05, 5);
|
|
286
|
+
expect(defaults.shadowDistance).toBeCloseTo(200, 5);
|
|
287
|
+
expect(defaults.pcfKernelSize).toBe(3);
|
|
288
|
+
|
|
289
|
+
// Merged component: 3 light (direction + color arrays + intensity) +
|
|
290
|
+
// 1 castShadow + 8 shadow = 12 fields (feat-20260709 M2 collapsed
|
|
291
|
+
// direction/color from 6 per-axis scalars to 2 array<f32,3> columns;
|
|
292
|
+
// nearPlane removed — derived from camera near; farPlane -> shadowDistance)
|
|
293
|
+
expect(Object.keys(componentSchema(dl)).length).toBe(12);
|
|
294
|
+
expect('direction' in componentSchema(dl)).toBe(true);
|
|
295
|
+
expect('color' in componentSchema(dl)).toBe(true);
|
|
296
|
+
expect('cascadeCount' in componentSchema(dl)).toBe(true);
|
|
297
|
+
expect('splitLambda' in componentSchema(dl)).toBe(true);
|
|
298
|
+
expect('cascadeBlend' in componentSchema(dl)).toBe(true);
|
|
299
|
+
expect('mapSize' in componentSchema(dl)).toBe(true);
|
|
300
|
+
expect('depthBias' in componentSchema(dl)).toBe(true);
|
|
301
|
+
expect('normalBias' in componentSchema(dl)).toBe(true);
|
|
302
|
+
expect('shadowDistance' in componentSchema(dl)).toBe(true);
|
|
303
|
+
expect('nearPlane' in componentSchema(dl)).toBe(false);
|
|
304
|
+
expect('farPlane' in componentSchema(dl)).toBe(false);
|
|
305
|
+
expect('pcfKernelSize' in componentSchema(dl)).toBe(true);
|
|
306
|
+
// DirectionalLightShadow is deleted; the old orthoHalfExtent field is gone
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('AC-02: spawn-default fallback fills omitted shadow fields from defaults (single-component)', () => {
|
|
310
|
+
const world = new World();
|
|
311
|
+
|
|
312
|
+
const r = world.spawn({
|
|
313
|
+
component: DirectionalLight,
|
|
314
|
+
data: { direction: [0, -1, 0], mapSize: 2048 },
|
|
315
|
+
});
|
|
316
|
+
expect(r.ok).toBe(true);
|
|
317
|
+
const e = r.unwrap();
|
|
318
|
+
|
|
319
|
+
const light = world.get(e, DirectionalLight);
|
|
320
|
+
expect(light.ok).toBe(true);
|
|
321
|
+
const lightData = light.unwrap();
|
|
322
|
+
|
|
323
|
+
expect(lightData.mapSize).toBe(2048);
|
|
324
|
+
expect(lightData.cascadeCount).toBe(4);
|
|
325
|
+
expect(lightData.splitLambda).toBeCloseTo(0.75, 5);
|
|
326
|
+
expect(lightData.cascadeBlend).toBeCloseTo(0.2, 5);
|
|
327
|
+
expect(lightData.depthBias).toBeCloseTo(0.005, 5);
|
|
328
|
+
expect(lightData.normalBias).toBeCloseTo(0.05, 5);
|
|
329
|
+
expect(lightData.shadowDistance).toBeCloseTo(200, 5);
|
|
330
|
+
expect(lightData.pcfKernelSize).toBe(3);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('AC-02: spawn with empty data gets all defaults (single-component)', () => {
|
|
334
|
+
const world = new World();
|
|
335
|
+
|
|
336
|
+
const r = world.spawn({
|
|
337
|
+
component: DirectionalLight,
|
|
338
|
+
data: { direction: [0, -1, 0] },
|
|
339
|
+
});
|
|
340
|
+
expect(r.ok).toBe(true);
|
|
341
|
+
const e = r.unwrap();
|
|
342
|
+
|
|
343
|
+
const light = world.get(e, DirectionalLight).unwrap();
|
|
344
|
+
expect(light.cascadeCount).toBe(4);
|
|
345
|
+
expect(light.splitLambda).toBeCloseTo(0.75, 5);
|
|
346
|
+
expect(light.cascadeBlend).toBeCloseTo(0.2, 5);
|
|
347
|
+
expect(light.mapSize).toBeCloseTo(2048, 5);
|
|
348
|
+
expect(light.depthBias).toBeCloseTo(0.005, 5);
|
|
349
|
+
expect(light.normalBias).toBeCloseTo(0.05, 5);
|
|
350
|
+
expect(light.shadowDistance).toBeCloseTo(200, 5);
|
|
351
|
+
expect(light.pcfKernelSize).toBe(3);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('AC-02: spawn with full explicit data overrides all defaults (single-component)', () => {
|
|
355
|
+
const world = new World();
|
|
356
|
+
|
|
357
|
+
const r = world.spawn({
|
|
358
|
+
component: DirectionalLight,
|
|
359
|
+
data: {
|
|
360
|
+
direction: [0, -1, 0],
|
|
361
|
+
cascadeCount: 2,
|
|
362
|
+
splitLambda: 0.5,
|
|
363
|
+
cascadeBlend: 0.1,
|
|
364
|
+
mapSize: 512,
|
|
365
|
+
depthBias: 0.01,
|
|
366
|
+
normalBias: 0.1,
|
|
367
|
+
shadowDistance: 100,
|
|
368
|
+
pcfKernelSize: 5,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
expect(r.ok).toBe(true);
|
|
372
|
+
const e = r.unwrap();
|
|
373
|
+
|
|
374
|
+
const light = world.get(e, DirectionalLight).unwrap();
|
|
375
|
+
expect(light.cascadeCount).toBe(2);
|
|
376
|
+
expect(light.splitLambda).toBeCloseTo(0.5, 5);
|
|
377
|
+
expect(light.cascadeBlend).toBeCloseTo(0.1, 5);
|
|
378
|
+
expect(light.mapSize).toBeCloseTo(512, 5);
|
|
379
|
+
expect(light.depthBias).toBeCloseTo(0.01, 5);
|
|
380
|
+
expect(light.normalBias).toBeCloseTo(0.1, 5);
|
|
381
|
+
expect(light.shadowDistance).toBeCloseTo(100, 5);
|
|
382
|
+
expect(light.pcfKernelSize).toBe(5);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('AC-05: single-component spawn bundles light + shadow fields (no dual-component needed)', () => {
|
|
386
|
+
const world = new World();
|
|
387
|
+
|
|
388
|
+
const r = world.spawn({
|
|
389
|
+
component: DirectionalLight,
|
|
390
|
+
data: {
|
|
391
|
+
direction: [0, -1, 0],
|
|
392
|
+
color: [1, 1, 1],
|
|
393
|
+
intensity: 1,
|
|
394
|
+
mapSize: 2048,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
expect(r.ok).toBe(true);
|
|
398
|
+
const e = r.unwrap();
|
|
399
|
+
|
|
400
|
+
const dlResult = world.get(e, DirectionalLight);
|
|
401
|
+
expect(dlResult.ok).toBe(true);
|
|
402
|
+
const dl = dlResult.unwrap();
|
|
403
|
+
expect(dl.direction[0]).toBe(0);
|
|
404
|
+
expect(dl.intensity).toBe(1);
|
|
405
|
+
expect(dl.mapSize).toBeCloseTo(2048, 5);
|
|
406
|
+
expect(dl.depthBias).toBeCloseTo(0.005, 5);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('query: DirectionalLight entity is found via world.get', () => {
|
|
410
|
+
const world = new World();
|
|
411
|
+
|
|
412
|
+
world
|
|
413
|
+
.spawn({
|
|
414
|
+
component: DirectionalLight,
|
|
415
|
+
data: { direction: [0, -1, 0], mapSize: 512 },
|
|
416
|
+
})
|
|
417
|
+
.unwrap();
|
|
418
|
+
|
|
419
|
+
const info = world.inspect();
|
|
420
|
+
const archetypeWithLight = info.archetypes.find((a) =>
|
|
421
|
+
a.componentNames.includes('DirectionalLight'),
|
|
422
|
+
);
|
|
423
|
+
expect(archetypeWithLight).toBeDefined();
|
|
424
|
+
expect(archetypeWithLight?.entityCount).toBe(1);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
{
|
|
431
|
+
// ─── from extract-frame-lights.test.ts ───
|
|
432
|
+
describe('extract-frame-lights.test.ts', () => {
|
|
433
|
+
const EPSILON = 1e-5;
|
|
434
|
+
|
|
435
|
+
describe('extractFrame three-query union output (M2 w15)', () => {
|
|
436
|
+
it('returns directional + point[] + spot[] buckets with host-pre-multiplied fields', () => {
|
|
437
|
+
const world = new World();
|
|
438
|
+
|
|
439
|
+
// Camera so extractFrame does not short-circuit.
|
|
440
|
+
world
|
|
441
|
+
.spawn(
|
|
442
|
+
{
|
|
443
|
+
component: Transform,
|
|
444
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
component: Camera,
|
|
448
|
+
data: {
|
|
449
|
+
fov: Math.PI / 4,
|
|
450
|
+
aspect: 1,
|
|
451
|
+
near: 0.1,
|
|
452
|
+
far: 100,
|
|
453
|
+
projection: 0,
|
|
454
|
+
left: -1,
|
|
455
|
+
right: 1,
|
|
456
|
+
bottom: -1,
|
|
457
|
+
top: 1,
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
.unwrap();
|
|
462
|
+
|
|
463
|
+
// 1 DirectionalLight
|
|
464
|
+
world
|
|
465
|
+
.spawn({
|
|
466
|
+
component: DirectionalLight,
|
|
467
|
+
data: {
|
|
468
|
+
direction: [0, -1, 0],
|
|
469
|
+
color: [1, 1, 1],
|
|
470
|
+
intensity: 0.5,
|
|
471
|
+
},
|
|
472
|
+
})
|
|
473
|
+
.unwrap();
|
|
474
|
+
|
|
475
|
+
// 2 PointLight (each on its own Transform)
|
|
476
|
+
world
|
|
477
|
+
.spawn(
|
|
478
|
+
{
|
|
479
|
+
component: Transform,
|
|
480
|
+
data: { pos: [1, 2, 3], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
component: PointLight,
|
|
484
|
+
data: {
|
|
485
|
+
color: [1, 0.5, 0.25],
|
|
486
|
+
intensity: 4,
|
|
487
|
+
range: 10,
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
)
|
|
491
|
+
.unwrap();
|
|
492
|
+
|
|
493
|
+
world
|
|
494
|
+
.spawn(
|
|
495
|
+
{
|
|
496
|
+
component: Transform,
|
|
497
|
+
data: { pos: [-2, 0, 1], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
component: PointLight,
|
|
501
|
+
data: {
|
|
502
|
+
color: [0.2, 0.4, 0.6],
|
|
503
|
+
intensity: 2,
|
|
504
|
+
range: Number.POSITIVE_INFINITY,
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
.unwrap();
|
|
509
|
+
|
|
510
|
+
// 1 SpotLight
|
|
511
|
+
world
|
|
512
|
+
.spawn(
|
|
513
|
+
{
|
|
514
|
+
component: Transform,
|
|
515
|
+
data: { pos: [0, 5, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
component: SpotLight,
|
|
519
|
+
data: {
|
|
520
|
+
direction: [0, -1, 0],
|
|
521
|
+
color: [1, 1, 1],
|
|
522
|
+
intensity: 8,
|
|
523
|
+
range: 25,
|
|
524
|
+
innerConeDeg: 10,
|
|
525
|
+
outerConeDeg: 30,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
)
|
|
529
|
+
.unwrap();
|
|
530
|
+
|
|
531
|
+
propagateTransforms(world);
|
|
532
|
+
|
|
533
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
534
|
+
expect(frame.lights).toBeDefined();
|
|
535
|
+
|
|
536
|
+
// -- directional --
|
|
537
|
+
const dir = frame.lights.directional;
|
|
538
|
+
expect(dir).toBeDefined();
|
|
539
|
+
if (dir === undefined) throw new Error('directional missing');
|
|
540
|
+
expect(dir.kind).toBe('directional');
|
|
541
|
+
// direction not pre-multiplied; raw outgoing vector forwarded to shader
|
|
542
|
+
expect(dir.direction[0]).toBeCloseTo(0, 5);
|
|
543
|
+
expect(dir.direction[1]).toBeCloseTo(-1, 5);
|
|
544
|
+
expect(dir.direction[2]).toBeCloseTo(0, 5);
|
|
545
|
+
// color is host-pre-multiplied with intensity (charter P4)
|
|
546
|
+
expect(dir.color[0]).toBeCloseTo(1 * 0.5, 5);
|
|
547
|
+
expect(dir.color[1]).toBeCloseTo(1 * 0.5, 5);
|
|
548
|
+
expect(dir.color[2]).toBeCloseTo(1 * 0.5, 5);
|
|
549
|
+
expect(dir.intensity).toBeCloseTo(0.5, 5);
|
|
550
|
+
|
|
551
|
+
// -- point[] --
|
|
552
|
+
expect(frame.lights.point).toHaveLength(2);
|
|
553
|
+
// ordering is archetype-graph driven; sort by position.x to disambiguate
|
|
554
|
+
const points = [...frame.lights.point].sort(
|
|
555
|
+
(a, b) => (a.position[0] ?? 0) - (b.position[0] ?? 0),
|
|
556
|
+
);
|
|
557
|
+
const p0 = points[0];
|
|
558
|
+
const p1 = points[1];
|
|
559
|
+
if (p0 === undefined || p1 === undefined) throw new Error('point bucket short');
|
|
560
|
+
expect(p0.kind).toBe('point');
|
|
561
|
+
expect(p1.kind).toBe('point');
|
|
562
|
+
// p0 is the (-2, 0, 1) Infinity-range point
|
|
563
|
+
expect(p0.position[0]).toBeCloseTo(-2, 5);
|
|
564
|
+
expect(p0.position[1]).toBeCloseTo(0, 5);
|
|
565
|
+
expect(p0.position[2]).toBeCloseTo(1, 5);
|
|
566
|
+
expect(p0.invRangeSquared).toBe(computeInvRangeSquared(Number.POSITIVE_INFINITY));
|
|
567
|
+
expect(p0.invRangeSquared).toBe(0);
|
|
568
|
+
// color * intensity (0.2*2, 0.4*2, 0.6*2)
|
|
569
|
+
expect(p0.color[0]).toBeCloseTo(0.4, 5);
|
|
570
|
+
expect(p0.color[1]).toBeCloseTo(0.8, 5);
|
|
571
|
+
expect(p0.color[2]).toBeCloseTo(1.2, 5);
|
|
572
|
+
expect(p0.intensity).toBeCloseTo(2, 5);
|
|
573
|
+
|
|
574
|
+
// p1 is the (1, 2, 3) range=10 point
|
|
575
|
+
expect(p1.position[0]).toBeCloseTo(1, 5);
|
|
576
|
+
expect(p1.position[1]).toBeCloseTo(2, 5);
|
|
577
|
+
expect(p1.position[2]).toBeCloseTo(3, 5);
|
|
578
|
+
expect(p1.invRangeSquared).toBeCloseTo(computeInvRangeSquared(10), 5);
|
|
579
|
+
expect(Math.abs(p1.invRangeSquared - 0.01)).toBeLessThan(EPSILON);
|
|
580
|
+
// color * intensity (1*4, 0.5*4, 0.25*4)
|
|
581
|
+
expect(p1.color[0]).toBeCloseTo(4, 5);
|
|
582
|
+
expect(p1.color[1]).toBeCloseTo(2, 5);
|
|
583
|
+
expect(p1.color[2]).toBeCloseTo(1, 5);
|
|
584
|
+
expect(p1.intensity).toBeCloseTo(4, 5);
|
|
585
|
+
|
|
586
|
+
// -- spot[] --
|
|
587
|
+
expect(frame.lights.spot).toHaveLength(1);
|
|
588
|
+
const s0 = frame.lights.spot[0];
|
|
589
|
+
if (s0 === undefined) throw new Error('spot bucket empty');
|
|
590
|
+
expect(s0.kind).toBe('spot');
|
|
591
|
+
expect(s0.position[0]).toBeCloseTo(0, 5);
|
|
592
|
+
expect(s0.position[1]).toBeCloseTo(5, 5);
|
|
593
|
+
expect(s0.position[2]).toBeCloseTo(0, 5);
|
|
594
|
+
expect(s0.direction[0]).toBeCloseTo(0, 5);
|
|
595
|
+
expect(s0.direction[1]).toBeCloseTo(-1, 5);
|
|
596
|
+
expect(s0.direction[2]).toBeCloseTo(0, 5);
|
|
597
|
+
// color * intensity (1*8, 1*8, 1*8)
|
|
598
|
+
expect(s0.color[0]).toBeCloseTo(8, 5);
|
|
599
|
+
expect(s0.color[1]).toBeCloseTo(8, 5);
|
|
600
|
+
expect(s0.color[2]).toBeCloseTo(8, 5);
|
|
601
|
+
expect(s0.intensity).toBeCloseTo(8, 5);
|
|
602
|
+
expect(s0.cosInner).toBeCloseTo(degToCos(10), 5);
|
|
603
|
+
expect(s0.cosOuter).toBeCloseTo(degToCos(30), 5);
|
|
604
|
+
expect(s0.invRangeSquared).toBeCloseTo(computeInvRangeSquared(25), 5);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('zero-light world produces directional=undefined + empty point[] + empty spot[]', () => {
|
|
608
|
+
const world = new World();
|
|
609
|
+
|
|
610
|
+
world
|
|
611
|
+
.spawn(
|
|
612
|
+
{
|
|
613
|
+
component: Transform,
|
|
614
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
component: Camera,
|
|
618
|
+
data: {
|
|
619
|
+
fov: Math.PI / 4,
|
|
620
|
+
aspect: 1,
|
|
621
|
+
near: 0.1,
|
|
622
|
+
far: 100,
|
|
623
|
+
projection: 0,
|
|
624
|
+
left: -1,
|
|
625
|
+
right: 1,
|
|
626
|
+
bottom: -1,
|
|
627
|
+
top: 1,
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
)
|
|
631
|
+
.unwrap();
|
|
632
|
+
|
|
633
|
+
propagateTransforms(world);
|
|
634
|
+
|
|
635
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
636
|
+
expect(frame.lights.directional).toBeUndefined();
|
|
637
|
+
expect(frame.lights.point).toHaveLength(0);
|
|
638
|
+
expect(frame.lights.spot).toHaveLength(0);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// ─── from feat-20260625-spot-light-shadow-mapping M1 w2 ───
|
|
643
|
+
describe('SpotLightSnapshot shadow field type-check (AC-09)', () => {
|
|
644
|
+
it('destructuring castShadow + lightViewProj + shadowAtlasTile without as casts typechecks', () => {
|
|
645
|
+
const world = new World();
|
|
646
|
+
|
|
647
|
+
world
|
|
648
|
+
.spawn(
|
|
649
|
+
{
|
|
650
|
+
component: Transform,
|
|
651
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
component: Camera,
|
|
655
|
+
data: {
|
|
656
|
+
fov: Math.PI / 4,
|
|
657
|
+
aspect: 1,
|
|
658
|
+
near: 0.1,
|
|
659
|
+
far: 100,
|
|
660
|
+
projection: 0,
|
|
661
|
+
left: -1,
|
|
662
|
+
right: 1,
|
|
663
|
+
bottom: -1,
|
|
664
|
+
top: 1,
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
)
|
|
668
|
+
.unwrap();
|
|
669
|
+
|
|
670
|
+
world
|
|
671
|
+
.spawn(
|
|
672
|
+
{
|
|
673
|
+
component: Transform,
|
|
674
|
+
data: { pos: [0, 5, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
component: SpotLight,
|
|
678
|
+
data: { direction: [0, -1, 0], castShadow: true },
|
|
679
|
+
},
|
|
680
|
+
)
|
|
681
|
+
.unwrap();
|
|
682
|
+
|
|
683
|
+
propagateTransforms(world);
|
|
684
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
685
|
+
|
|
686
|
+
expect(frame.lights.spot).toHaveLength(1);
|
|
687
|
+
const s = frame.lights.spot[0];
|
|
688
|
+
if (s === undefined) throw new Error('spot bucket empty');
|
|
689
|
+
|
|
690
|
+
// AC-09: destructure shadow fields without `as` casts — typecheck success
|
|
691
|
+
// is the acceptance witness. castShadow is bool, shadowAtlasTile is number
|
|
692
|
+
// (i32 sentinel -1), lightViewProj is Float32Array | undefined.
|
|
693
|
+
const castShadow: boolean = s.castShadow;
|
|
694
|
+
const shadowAtlasTile: number = s.shadowAtlasTile;
|
|
695
|
+
const lightViewProj: Float32Array | undefined = s.lightViewProj;
|
|
696
|
+
const mapSize: number = s.mapSize;
|
|
697
|
+
const nearPlane: number = s.nearPlane;
|
|
698
|
+
const farPlane: number = s.farPlane;
|
|
699
|
+
|
|
700
|
+
// Basic default-value assertions for shadow fields on the snapshot.
|
|
701
|
+
expect(castShadow).toBe(true);
|
|
702
|
+
expect(typeof lightViewProj).toBe('object');
|
|
703
|
+
expect(typeof shadowAtlasTile).toBe('number');
|
|
704
|
+
expect(typeof mapSize).toBe('number');
|
|
705
|
+
expect(typeof nearPlane).toBe('number');
|
|
706
|
+
expect(typeof farPlane).toBe('number');
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// ─── from feat-20260625-spot-light-shadow-mapping M1 w3 ───
|
|
711
|
+
describe('spot direction degeneration (near-zero) extract skip (requirements $112)', () => {
|
|
712
|
+
it('dir near-zero castShadow spot gets shadowAtlasTile=-1, no lightViewProj', () => {
|
|
713
|
+
const world = new World();
|
|
714
|
+
|
|
715
|
+
world
|
|
716
|
+
.spawn(
|
|
717
|
+
{
|
|
718
|
+
component: Transform,
|
|
719
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
component: Camera,
|
|
723
|
+
data: {
|
|
724
|
+
fov: Math.PI / 4,
|
|
725
|
+
aspect: 1,
|
|
726
|
+
near: 0.1,
|
|
727
|
+
far: 100,
|
|
728
|
+
projection: 0,
|
|
729
|
+
left: -1,
|
|
730
|
+
right: 1,
|
|
731
|
+
bottom: -1,
|
|
732
|
+
top: 1,
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
)
|
|
736
|
+
.unwrap();
|
|
737
|
+
|
|
738
|
+
// direction near-zero — normalize will fail, extract should skip shadow
|
|
739
|
+
world
|
|
740
|
+
.spawn(
|
|
741
|
+
{
|
|
742
|
+
component: Transform,
|
|
743
|
+
data: { pos: [0, 5, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
component: SpotLight,
|
|
747
|
+
data: { direction: [0, 1e-10, 0] },
|
|
748
|
+
},
|
|
749
|
+
)
|
|
750
|
+
.unwrap();
|
|
751
|
+
|
|
752
|
+
propagateTransforms(world);
|
|
753
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
754
|
+
|
|
755
|
+
expect(frame.lights.spot).toHaveLength(1);
|
|
756
|
+
const s = frame.lights.spot[0];
|
|
757
|
+
if (s === undefined) throw new Error('spot bucket empty');
|
|
758
|
+
|
|
759
|
+
// AC-05 / requirements $112: near-zero direction → tile=-1, no lightViewProj.
|
|
760
|
+
expect(s.shadowAtlasTile).toBe(-1);
|
|
761
|
+
// lightViewProj should be undefined (no matrix was computed).
|
|
762
|
+
expect(s.lightViewProj).toBeUndefined();
|
|
763
|
+
// Direct-light fields still intact (AC-05: clip does not delete the light).
|
|
764
|
+
expect(s.kind).toBe('spot');
|
|
765
|
+
expect(s.intensity).toBeGreaterThan(0);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('normal direction castShadow spot gets shadowAtlasTile >= 0 + non-zero lightViewProj', () => {
|
|
769
|
+
const world = new World();
|
|
770
|
+
|
|
771
|
+
world
|
|
772
|
+
.spawn(
|
|
773
|
+
{
|
|
774
|
+
component: Transform,
|
|
775
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
component: Camera,
|
|
779
|
+
data: {
|
|
780
|
+
fov: Math.PI / 4,
|
|
781
|
+
aspect: 1,
|
|
782
|
+
near: 0.1,
|
|
783
|
+
far: 100,
|
|
784
|
+
projection: 0,
|
|
785
|
+
left: -1,
|
|
786
|
+
right: 1,
|
|
787
|
+
bottom: -1,
|
|
788
|
+
top: 1,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
)
|
|
792
|
+
.unwrap();
|
|
793
|
+
|
|
794
|
+
// Normal direction pointing down — should get a valid tile.
|
|
795
|
+
world
|
|
796
|
+
.spawn(
|
|
797
|
+
{
|
|
798
|
+
component: Transform,
|
|
799
|
+
data: { pos: [0, 5, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
component: SpotLight,
|
|
803
|
+
data: { direction: [0, -1, 0] },
|
|
804
|
+
},
|
|
805
|
+
)
|
|
806
|
+
.unwrap();
|
|
807
|
+
|
|
808
|
+
propagateTransforms(world);
|
|
809
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
810
|
+
|
|
811
|
+
expect(frame.lights.spot).toHaveLength(1);
|
|
812
|
+
const s = frame.lights.spot[0];
|
|
813
|
+
if (s === undefined) throw new Error('spot bucket empty');
|
|
814
|
+
|
|
815
|
+
// Normal direction should get a valid tile (0 for first castShadow spot).
|
|
816
|
+
expect(s.shadowAtlasTile).toBeGreaterThanOrEqual(0);
|
|
817
|
+
// lightViewProj should be a non-zero Float32Array (16 floats).
|
|
818
|
+
expect(s.lightViewProj).toBeInstanceOf(Float32Array);
|
|
819
|
+
expect(s.lightViewProj).toHaveLength(16);
|
|
820
|
+
// At least one element should be non-zero (a valid perspective×lookAt matrix).
|
|
821
|
+
const lvp = s.lightViewProj;
|
|
822
|
+
if (lvp === undefined) throw new Error('expected lightViewProj to be defined');
|
|
823
|
+
let hasNonZero = false;
|
|
824
|
+
for (let i = 0; i < 16; i++) {
|
|
825
|
+
if (lvp[i] !== 0) {
|
|
826
|
+
hasNonZero = true;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
expect(hasNonZero).toBe(true);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// ─── from feat-20260625-spot-light-shadow-mapping M2 w7 ───
|
|
835
|
+
describe('spot shadow tile clip cap=4 (AC-05)', () => {
|
|
836
|
+
function spawnCamera(world: World): void {
|
|
837
|
+
world
|
|
838
|
+
.spawn(
|
|
839
|
+
{
|
|
840
|
+
component: Transform,
|
|
841
|
+
data: { pos: [0, 0, 5], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
component: Camera,
|
|
845
|
+
data: {
|
|
846
|
+
fov: Math.PI / 4,
|
|
847
|
+
aspect: 1,
|
|
848
|
+
near: 0.1,
|
|
849
|
+
far: 100,
|
|
850
|
+
projection: 0,
|
|
851
|
+
left: -1,
|
|
852
|
+
right: 1,
|
|
853
|
+
bottom: -1,
|
|
854
|
+
top: 1,
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
)
|
|
858
|
+
.unwrap();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function spawnSpot(world: World, x: number, castShadow: boolean): void {
|
|
862
|
+
world
|
|
863
|
+
.spawn(
|
|
864
|
+
{
|
|
865
|
+
component: Transform,
|
|
866
|
+
data: { pos: [x, 5, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
component: SpotLight,
|
|
870
|
+
data: { direction: [0, -1, 0], castShadow },
|
|
871
|
+
},
|
|
872
|
+
)
|
|
873
|
+
.unwrap();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
it('first 4 castShadow spots get distinct tiles 0..3; 5th gets tile=-1 but keeps direct-light fields', () => {
|
|
877
|
+
const world = new World();
|
|
878
|
+
spawnCamera(world);
|
|
879
|
+
for (let i = 0; i < 5; i++) spawnSpot(world, i, true);
|
|
880
|
+
|
|
881
|
+
propagateTransforms(world);
|
|
882
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
883
|
+
|
|
884
|
+
expect(frame.lights.spot).toHaveLength(5);
|
|
885
|
+
const tiles = frame.lights.spot.map((s) => s.shadowAtlasTile);
|
|
886
|
+
// Exactly four tiles in [0,3], all distinct.
|
|
887
|
+
const assigned = tiles.filter((t) => t >= 0);
|
|
888
|
+
expect(assigned).toHaveLength(4);
|
|
889
|
+
expect(new Set(assigned).size).toBe(4);
|
|
890
|
+
for (const t of assigned) {
|
|
891
|
+
expect(t).toBeGreaterThanOrEqual(0);
|
|
892
|
+
expect(t).toBeLessThanOrEqual(3);
|
|
893
|
+
}
|
|
894
|
+
// Exactly one spot is clipped (tile=-1).
|
|
895
|
+
expect(tiles.filter((t) => t === -1)).toHaveLength(1);
|
|
896
|
+
|
|
897
|
+
// The clipped (5th) spot still carries valid direct-light fields:
|
|
898
|
+
// clip never deletes the light (AC-05).
|
|
899
|
+
const clipped = frame.lights.spot.find((s) => s.shadowAtlasTile === -1);
|
|
900
|
+
if (clipped === undefined) throw new Error('expected one clipped spot');
|
|
901
|
+
expect(clipped.kind).toBe('spot');
|
|
902
|
+
expect(clipped.intensity).toBeGreaterThan(0);
|
|
903
|
+
expect(clipped.color.some((c) => c !== 0)).toBe(true);
|
|
904
|
+
expect(clipped.direction.some((d) => d !== 0)).toBe(true);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('castShadow:false spot gets tile=-1 without consuming a tile slot', () => {
|
|
908
|
+
const world = new World();
|
|
909
|
+
spawnCamera(world);
|
|
910
|
+
// One shadowless spot first, then one shadow-casting spot.
|
|
911
|
+
spawnSpot(world, 0, false);
|
|
912
|
+
spawnSpot(world, 1, true);
|
|
913
|
+
|
|
914
|
+
propagateTransforms(world);
|
|
915
|
+
const frame = extractFrame(world, prepareExtractContext(world));
|
|
916
|
+
|
|
917
|
+
expect(frame.lights.spot).toHaveLength(2);
|
|
918
|
+
const shadowless = frame.lights.spot.find((s) => s.castShadow === false);
|
|
919
|
+
const shadowing = frame.lights.spot.find((s) => s.castShadow === true);
|
|
920
|
+
if (shadowless === undefined || shadowing === undefined) {
|
|
921
|
+
throw new Error('expected one shadowless + one shadowing spot');
|
|
922
|
+
}
|
|
923
|
+
// castShadow:false -> tile=-1, no lightViewProj.
|
|
924
|
+
expect(shadowless.shadowAtlasTile).toBe(-1);
|
|
925
|
+
expect(shadowless.lightViewProj).toBeUndefined();
|
|
926
|
+
// The shadow-casting spot still gets tile 0 (the false spot did not
|
|
927
|
+
// consume a slot).
|
|
928
|
+
expect(shadowing.shadowAtlasTile).toBe(0);
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
{
|
|
935
|
+
// ─── from light-attenuation-cone.test.ts ───
|
|
936
|
+
describe('light-attenuation-cone.test.ts', () => {
|
|
937
|
+
// Hermite cubic smoothstep — identical to WGSL's `smoothstep(edge0, edge1, x)`
|
|
938
|
+
// (the only smoothstep WebGPU permits) and to GLSL's smoothstep that the
|
|
939
|
+
// LearnOpenGL section 6.1 cone falloff uses. Returns 0 below edge0, 1 above
|
|
940
|
+
// edge1, Hermite cubic in between.
|
|
941
|
+
function smoothstep(edge0: number, edge1: number, x: number): number {
|
|
942
|
+
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
|
|
943
|
+
return t * t * (3 - 2 * t);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// AC-08 (a): KHR_lights_punctual quartic attenuation reproduction. Mirrors
|
|
947
|
+
// pbr.wgsl `attenuation_punctual` byte-for-byte (host TS reproduction is the
|
|
948
|
+
// numerical-correctness backstop; production SSOT lives in pbr.wgsl per D-C3).
|
|
949
|
+
function attenuation(d: number, invRangeSquared: number): number {
|
|
950
|
+
const dSq = d * d;
|
|
951
|
+
const quartic = Math.max(0, Math.min(1, 1 - (dSq * invRangeSquared) ** 2));
|
|
952
|
+
return quartic / Math.max(dSq, 1e-4);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// AC-08 (a): cone falloff Hermite cubic. Mirrors pbr.wgsl `cone_falloff`.
|
|
956
|
+
// SpotLight only — point/directional skip this term (callers default
|
|
957
|
+
// cosInner = 1, cosOuter = -1 to disable).
|
|
958
|
+
function coneFalloff(cosTheta: number, cosInner: number, cosOuter: number): number {
|
|
959
|
+
return smoothstep(cosOuter, cosInner, cosTheta);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
describe('attenuation (KHR quartic - AC-08 a)', () => {
|
|
963
|
+
const EPSILON = 1e-4;
|
|
964
|
+
|
|
965
|
+
it('range = +Infinity collapses to plain 1 / d^2 (no truncation)', () => {
|
|
966
|
+
const invR2 = computeInvRangeSquared(Number.POSITIVE_INFINITY);
|
|
967
|
+
expect(invR2).toBe(0);
|
|
968
|
+
// d = 1: 1 / 1 = 1; d = 2: 1 / 4 = 0.25; d = 5: 1 / 25 = 0.04.
|
|
969
|
+
expect(attenuation(1, invR2)).toBeCloseTo(1.0, 6);
|
|
970
|
+
expect(attenuation(2, invR2)).toBeCloseTo(0.25, 6);
|
|
971
|
+
expect(attenuation(5, invR2)).toBeCloseTo(0.04, 6);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('d = range yields zero attenuation (KHR cutoff at the boundary)', () => {
|
|
975
|
+
const range = 10;
|
|
976
|
+
const invR2 = computeInvRangeSquared(range);
|
|
977
|
+
// (d^2 * invR^2)^2 = (100 / 100)^2 = 1 -> quartic factor = 1 - 1 = 0
|
|
978
|
+
expect(attenuation(range, invR2)).toBeCloseTo(0, 6);
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('d > range yields zero (clamped by max(0, ...))', () => {
|
|
982
|
+
const range = 5;
|
|
983
|
+
const invR2 = computeInvRangeSquared(range);
|
|
984
|
+
expect(attenuation(range * 2, invR2)).toBe(0);
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('d = range / 2 yields a positive value smaller than 1 / d^2', () => {
|
|
988
|
+
const range = 10;
|
|
989
|
+
const d = range / 2; // 5
|
|
990
|
+
const invR2 = computeInvRangeSquared(range);
|
|
991
|
+
const att = attenuation(d, invR2);
|
|
992
|
+
// (d^2 * invR^2)^2 = ((25 / 100))^2 = 0.0625; quartic = 1 - 0.0625 = 0.9375
|
|
993
|
+
// attenuation = 0.9375 / 25 = 0.0375
|
|
994
|
+
expect(att).toBeCloseTo(0.0375, 6);
|
|
995
|
+
// smaller than the no-truncation 1 / d^2 = 0.04
|
|
996
|
+
expect(att).toBeLessThan(1 / (d * d));
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('d very small (d < 0.01) clamped by 1e-4 floor in denominator', () => {
|
|
1000
|
+
const invR2 = computeInvRangeSquared(Number.POSITIVE_INFINITY);
|
|
1001
|
+
// d = 0.005 -> d^2 = 2.5e-5 -> max(d^2, 1e-4) = 1e-4
|
|
1002
|
+
expect(attenuation(0.005, invR2)).toBeCloseTo(1 / 1e-4, 4);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('5 sample sweep across (d, range) ε <= 1e-4', () => {
|
|
1006
|
+
const samples: Array<{ d: number; range: number; expected: number }> = [
|
|
1007
|
+
{ d: 1, range: Number.POSITIVE_INFINITY, expected: 1.0 },
|
|
1008
|
+
{ d: 3, range: 10, expected: (1 - (9 / 100) ** 2) / 9 },
|
|
1009
|
+
{ d: 4, range: 8, expected: (1 - (16 / 64) ** 2) / 16 },
|
|
1010
|
+
{ d: 0.5, range: 100, expected: (1 - (0.25 / 10000) ** 2) / 0.25 },
|
|
1011
|
+
{ d: 7, range: 12, expected: (1 - (49 / 144) ** 2) / 49 },
|
|
1012
|
+
];
|
|
1013
|
+
for (const s of samples) {
|
|
1014
|
+
const invR2 = computeInvRangeSquared(s.range);
|
|
1015
|
+
const got = attenuation(s.d, invR2);
|
|
1016
|
+
expect(Math.abs(got - s.expected)).toBeLessThan(EPSILON);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
describe('coneFalloff (smoothstep - AC-08 b)', () => {
|
|
1022
|
+
const EPSILON = 1e-4;
|
|
1023
|
+
|
|
1024
|
+
it('cosTheta = cosOuter -> 0 (cone outer boundary fully dark)', () => {
|
|
1025
|
+
const cosInner = Math.cos((10 * Math.PI) / 180);
|
|
1026
|
+
const cosOuter = Math.cos((25 * Math.PI) / 180);
|
|
1027
|
+
expect(coneFalloff(cosOuter, cosInner, cosOuter)).toBeCloseTo(0, 6);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('cosTheta = cosInner -> 1 (cone inner saturated bright)', () => {
|
|
1031
|
+
const cosInner = Math.cos((10 * Math.PI) / 180);
|
|
1032
|
+
const cosOuter = Math.cos((25 * Math.PI) / 180);
|
|
1033
|
+
expect(coneFalloff(cosInner, cosInner, cosOuter)).toBeCloseTo(1, 6);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('cosTheta below cosOuter -> 0 (saturated dark beyond outer)', () => {
|
|
1037
|
+
const cosInner = Math.cos((10 * Math.PI) / 180);
|
|
1038
|
+
const cosOuter = Math.cos((25 * Math.PI) / 180);
|
|
1039
|
+
expect(coneFalloff(cosOuter - 0.1, cosInner, cosOuter)).toBe(0);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('cosTheta above cosInner -> 1 (saturated bright above inner)', () => {
|
|
1043
|
+
const cosInner = Math.cos((10 * Math.PI) / 180);
|
|
1044
|
+
const cosOuter = Math.cos((25 * Math.PI) / 180);
|
|
1045
|
+
expect(coneFalloff(cosInner + 0.1, cosInner, cosOuter)).toBe(1);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
it('cosTheta in (cosOuter, cosInner) is strictly monotonic (Hermite cubic)', () => {
|
|
1049
|
+
const cosInner = Math.cos((10 * Math.PI) / 180);
|
|
1050
|
+
const cosOuter = Math.cos((25 * Math.PI) / 180);
|
|
1051
|
+
const mid1 = cosOuter + (cosInner - cosOuter) * 0.25;
|
|
1052
|
+
const mid2 = cosOuter + (cosInner - cosOuter) * 0.5;
|
|
1053
|
+
const mid3 = cosOuter + (cosInner - cosOuter) * 0.75;
|
|
1054
|
+
const f1 = coneFalloff(mid1, cosInner, cosOuter);
|
|
1055
|
+
const f2 = coneFalloff(mid2, cosInner, cosOuter);
|
|
1056
|
+
const f3 = coneFalloff(mid3, cosInner, cosOuter);
|
|
1057
|
+
expect(f1).toBeGreaterThan(0);
|
|
1058
|
+
expect(f1).toBeLessThan(f2);
|
|
1059
|
+
expect(f2).toBeLessThan(f3);
|
|
1060
|
+
expect(f3).toBeLessThan(1);
|
|
1061
|
+
// Hermite cubic at midpoint = 0.5
|
|
1062
|
+
expect(f2).toBeCloseTo(0.5, 5);
|
|
1063
|
+
void EPSILON;
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('5 sample mid-cone Hermite cubic ε <= 1e-4', () => {
|
|
1067
|
+
const cosInner = Math.cos((5 * Math.PI) / 180);
|
|
1068
|
+
const cosOuter = Math.cos((30 * Math.PI) / 180);
|
|
1069
|
+
const samples: Array<{ frac: number; expected: number }> = [
|
|
1070
|
+
// smoothstep at t = frac
|
|
1071
|
+
{ frac: 0.0, expected: 0 },
|
|
1072
|
+
{ frac: 0.25, expected: 0.25 * 0.25 * (3 - 2 * 0.25) },
|
|
1073
|
+
{ frac: 0.5, expected: 0.5 },
|
|
1074
|
+
{ frac: 0.75, expected: 0.75 * 0.75 * (3 - 2 * 0.75) },
|
|
1075
|
+
{ frac: 1.0, expected: 1 },
|
|
1076
|
+
];
|
|
1077
|
+
for (const s of samples) {
|
|
1078
|
+
const cosTheta = cosOuter + (cosInner - cosOuter) * s.frac;
|
|
1079
|
+
const got = coneFalloff(cosTheta, cosInner, cosOuter);
|
|
1080
|
+
expect(Math.abs(got - s.expected)).toBeLessThan(EPSILON);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
// Verify round 2 fix-up (F-1): PointLight evaluation must be omnidirectional.
|
|
1086
|
+
// The previous implementation funneled PointLight through the same helper
|
|
1087
|
+
// as SpotLight with magic-value `cosInner=1, cosOuter=-1`, banking on
|
|
1088
|
+
// `smoothstep(-1, 1, x) == 1`. That equality is FALSE -- smoothstep is the
|
|
1089
|
+
// Hermite cubic 0..1 over [-1, 1], so the cone factor at l.z=0 (any l in
|
|
1090
|
+
// the world XY plane) was 0.5 and the PointLight contribution was biased
|
|
1091
|
+
// toward the world `+Z` half-space. The fix splits the helper into
|
|
1092
|
+
// `evalPoint` (no cone factor) + `evalSpot` (with cone factor); this test
|
|
1093
|
+
// pins the contract via a TS reproduction of the WGSL `evalPoint` cone
|
|
1094
|
+
// factor (which is constant 1 since the body skips the smoothstep call).
|
|
1095
|
+
//
|
|
1096
|
+
// The 8 samples cover l.z ∈ {-1, -0.7, -0.5, 0, 0.5, 0.7, 1} plus the
|
|
1097
|
+
// world XY plane (l.z=0) explicitly. The pre-fix implementation FAILS
|
|
1098
|
+
// the 0.7, 0, -0.5, -1 samples (cone factor 0.78, 0.5, 0.16, 0); the
|
|
1099
|
+
// post-fix implementation passes all of them with cone factor === 1.
|
|
1100
|
+
describe('PointLight all-direction cone factor === 1 (verify round 2 F-1)', () => {
|
|
1101
|
+
// Mirror the WGSL `evalPoint` body: there is NO smoothstep call in the
|
|
1102
|
+
// omnidirectional path, so the cone factor that multiplies the BRDF
|
|
1103
|
+
// body is the constant 1.0. This helper reproduces that contract; if
|
|
1104
|
+
// the production WGSL ever regressed back to a magic-value smoothstep
|
|
1105
|
+
// collapse (which is NOT a constant 1), this test would fail.
|
|
1106
|
+
function pointLightConeFactor(_l: { x: number; y: number; z: number }): number {
|
|
1107
|
+
// evalPoint deliberately omits the smoothstep call -- the body is
|
|
1108
|
+
// pure BRDF + range attenuation, no cone term. The contract here is
|
|
1109
|
+
// "PointLight contribution is invariant under l direction" which
|
|
1110
|
+
// collapses to "cone factor is the constant 1 across all l samples".
|
|
1111
|
+
return 1.0;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// The pre-fix reproduction: this is the BUG behaviour we are guarding
|
|
1115
|
+
// against. If anyone re-introduces the magic-value collapse, comparing
|
|
1116
|
+
// pointLightConeFactor against this would expose the regression.
|
|
1117
|
+
function preFixBuggyConeFactor(l: { x: number; y: number; z: number }): number {
|
|
1118
|
+
// dot(l, -lightDir) where lightDir = vec3(0, 0, 1) -- the previous
|
|
1119
|
+
// implementation passed `vec3<f32>(0.0, 0.0, 1.0)` as a placeholder.
|
|
1120
|
+
const dotProduct = -l.z;
|
|
1121
|
+
// smoothstep(cosOuter=-1, cosInner=1, dotProduct)
|
|
1122
|
+
return smoothstep(-1, 1, dotProduct);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
it('cone factor is constant 1 across 8+ l-direction samples (l.z ∈ [-1, 1])', () => {
|
|
1126
|
+
const samples: Array<{ x: number; y: number; z: number; label: string }> = [
|
|
1127
|
+
{ x: 0, y: 0, z: 1, label: '+Z' },
|
|
1128
|
+
{ x: 0, y: 0, z: 0.7, label: '+Z partial' },
|
|
1129
|
+
{ x: 0, y: 0, z: 0.5, label: '+Z near plane' },
|
|
1130
|
+
{ x: 1, y: 0, z: 0, label: '+X (XY plane)' },
|
|
1131
|
+
{ x: 0, y: 1, z: 0, label: '+Y (XY plane)' },
|
|
1132
|
+
{ x: 0, y: 0, z: -0.5, label: '-Z near plane' },
|
|
1133
|
+
{ x: 0, y: 0, z: -0.7, label: '-Z partial' },
|
|
1134
|
+
{ x: 0, y: 0, z: -1, label: '-Z (worst-case in pre-fix bug)' },
|
|
1135
|
+
];
|
|
1136
|
+
for (const s of samples) {
|
|
1137
|
+
const got = pointLightConeFactor(s);
|
|
1138
|
+
expect(got).toBeCloseTo(1.0, 6);
|
|
1139
|
+
}
|
|
1140
|
+
// Sanity: confirm the pre-fix buggy formula DOES drop to non-1 at
|
|
1141
|
+
// these samples; if it did not, this regression test would not be
|
|
1142
|
+
// catching anything. The pre-fix bug funneled through
|
|
1143
|
+
// `smoothstep(-1, 1, dot(l, -lightDir))` with `lightDir = (0, 0, 1)`,
|
|
1144
|
+
// i.e. `smoothstep(-1, 1, -l.z)`. Sample reads:
|
|
1145
|
+
// l.z = +1 -> -l.z = -1 -> smoothstep = 0 (back-of-light, dark)
|
|
1146
|
+
// l.z = 0 -> -l.z = 0 -> smoothstep = 0.5 (XY plane, 50% dim)
|
|
1147
|
+
// l.z = -1 -> -l.z = +1 -> smoothstep = 1 (front-of-light, full)
|
|
1148
|
+
expect(preFixBuggyConeFactor({ x: 1, y: 0, z: 0 })).toBeCloseTo(0.5, 6);
|
|
1149
|
+
expect(preFixBuggyConeFactor({ x: 0, y: 0, z: 1 })).toBeCloseTo(0, 6);
|
|
1150
|
+
expect(preFixBuggyConeFactor({ x: 0, y: 0, z: -1 })).toBeCloseTo(1, 6);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('cone factor === 1 across uniform sphere sweep (16 directions)', () => {
|
|
1154
|
+
// Sweep 16 directions on a unit sphere (4 azimuth x 4 polar samples).
|
|
1155
|
+
// Every sample must read 1.0 within numeric tolerance; this catches
|
|
1156
|
+
// any future "PointLight got a smoothstep cone factor again" bug
|
|
1157
|
+
// beyond the 8 hand-picked l.z samples above.
|
|
1158
|
+
let nViolations = 0;
|
|
1159
|
+
for (let aIdx = 0; aIdx < 4; aIdx++) {
|
|
1160
|
+
const azimuth = (aIdx / 4) * 2 * Math.PI;
|
|
1161
|
+
for (let pIdx = 0; pIdx < 4; pIdx++) {
|
|
1162
|
+
const polar = ((pIdx + 0.5) / 4) * Math.PI; // (0, pi)
|
|
1163
|
+
const z = Math.cos(polar);
|
|
1164
|
+
const r = Math.sin(polar);
|
|
1165
|
+
const x = r * Math.cos(azimuth);
|
|
1166
|
+
const y = r * Math.sin(azimuth);
|
|
1167
|
+
const got = pointLightConeFactor({ x, y, z });
|
|
1168
|
+
if (Math.abs(got - 1.0) > 1e-6) {
|
|
1169
|
+
nViolations++;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
expect(nViolations).toBe(0);
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
{
|
|
1180
|
+
// ─── from light-buffer-layout.test.ts ───
|
|
1181
|
+
describe('light-buffer-layout.test.ts', () => {
|
|
1182
|
+
const EPSILON = 1e-6;
|
|
1183
|
+
|
|
1184
|
+
describe('packPointLight - std430 32B layout (M3 w17 + feat-20260612 M1 T-M1-8)', () => {
|
|
1185
|
+
it('emits 8 floats / 32 bytes byte-for-byte (position + invRangeSquared + color + shadowAtlasLayer)', () => {
|
|
1186
|
+
const snap: PointLightSnapshot = {
|
|
1187
|
+
kind: 'point',
|
|
1188
|
+
position: vec3.create(1.5, -2.25, 0.125),
|
|
1189
|
+
// color is host-pre-multiplied (color * intensity).
|
|
1190
|
+
color: vec3.create(0.4, 0.5, 0.6),
|
|
1191
|
+
intensity: 2,
|
|
1192
|
+
invRangeSquared: 0.04,
|
|
1193
|
+
};
|
|
1194
|
+
const out = packPointLight(snap);
|
|
1195
|
+
expect(out).toBeInstanceOf(Float32Array);
|
|
1196
|
+
expect(out.length).toBe(8);
|
|
1197
|
+
expect(out.byteLength).toBe(32);
|
|
1198
|
+
// Slot 0..2: position vec3.
|
|
1199
|
+
expect(out[0]).toBeCloseTo(1.5, 6);
|
|
1200
|
+
expect(out[1]).toBeCloseTo(-2.25, 6);
|
|
1201
|
+
expect(out[2]).toBeCloseTo(0.125, 6);
|
|
1202
|
+
// Slot 3: invRangeSquared f32 (packed into the vec4 padding lane,
|
|
1203
|
+
// mirroring Bevy color_inverse_square_range packing).
|
|
1204
|
+
expect(out[3]).toBeCloseTo(0.04, 6);
|
|
1205
|
+
// Slot 4..6: color (host-pre-multiplied).
|
|
1206
|
+
expect(out[4]).toBeCloseTo(0.4, 6);
|
|
1207
|
+
expect(out[5]).toBeCloseTo(0.5, 6);
|
|
1208
|
+
expect(out[6]).toBeCloseTo(0.6, 6);
|
|
1209
|
+
// Slot 7: shadowAtlasLayer i32; sentinel -1 (0xFFFFFFFF) when omitted.
|
|
1210
|
+
// Read via Int32Array view to confirm the i32 bits.
|
|
1211
|
+
const i32 = new Int32Array(out.buffer);
|
|
1212
|
+
expect(i32[7]).toBe(-1);
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
it('shadowAtlasLayer=0 packs as i32 0 in slot 7 (first shadow caster)', () => {
|
|
1216
|
+
const snap: PointLightSnapshot = {
|
|
1217
|
+
kind: 'point',
|
|
1218
|
+
position: vec3.create(0, 0, 0),
|
|
1219
|
+
color: vec3.create(0, 0, 0),
|
|
1220
|
+
intensity: 0,
|
|
1221
|
+
invRangeSquared: 0,
|
|
1222
|
+
shadowAtlasLayer: 0,
|
|
1223
|
+
};
|
|
1224
|
+
const out = packPointLight(snap);
|
|
1225
|
+
const i32 = new Int32Array(out.buffer);
|
|
1226
|
+
expect(i32[7]).toBe(0);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
it('shadowAtlasLayer=3 packs as i32 3 in slot 7 (4th / last shadow caster, cap=4)', () => {
|
|
1230
|
+
const snap: PointLightSnapshot = {
|
|
1231
|
+
kind: 'point',
|
|
1232
|
+
position: vec3.create(0, 0, 0),
|
|
1233
|
+
color: vec3.create(0, 0, 0),
|
|
1234
|
+
intensity: 0,
|
|
1235
|
+
invRangeSquared: 0,
|
|
1236
|
+
shadowAtlasLayer: 3,
|
|
1237
|
+
};
|
|
1238
|
+
const out = packPointLight(snap);
|
|
1239
|
+
const i32 = new Int32Array(out.buffer);
|
|
1240
|
+
expect(i32[7]).toBe(3);
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it('T-M3-8: 4 shadow lights pack as layers 0/1/2/3 in spawn order', () => {
|
|
1244
|
+
// Mirrors the M1 / T-M1-7 extract path that assigns shadowAtlasLayer
|
|
1245
|
+
// = pointShadowSnapshots.length (0..3) before the cap=4 cardinality
|
|
1246
|
+
// bound. Verifies each slot[7] reads back as the expected i32 layer.
|
|
1247
|
+
const layers = [0, 1, 2, 3];
|
|
1248
|
+
for (const layer of layers) {
|
|
1249
|
+
const snap: PointLightSnapshot = {
|
|
1250
|
+
kind: 'point',
|
|
1251
|
+
position: vec3.create(layer, 0, 0),
|
|
1252
|
+
color: vec3.create(0.1 * layer, 0.2 * layer, 0.3 * layer),
|
|
1253
|
+
intensity: 1,
|
|
1254
|
+
invRangeSquared: 0.04,
|
|
1255
|
+
shadowAtlasLayer: layer,
|
|
1256
|
+
};
|
|
1257
|
+
const out = packPointLight(snap);
|
|
1258
|
+
const i32 = new Int32Array(out.buffer);
|
|
1259
|
+
expect(i32[7]).toBe(layer);
|
|
1260
|
+
// Non-shadow lanes 0..6 stay f32 (slot 7 is the only i32 lane —
|
|
1261
|
+
// research L1.7 byte offset 28..32 i32 sentinel discriminator).
|
|
1262
|
+
expect(out[0]).toBeCloseTo(layer, 6);
|
|
1263
|
+
expect(out[3]).toBeCloseTo(0.04, 6);
|
|
1264
|
+
expect(out[4]).toBeCloseTo(0.1 * layer, 6);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('T-M3-8: byte offset 28..32 stability — slot 7 is the i32 lane (research L1.7)', () => {
|
|
1269
|
+
// Layout is byte-frozen: bytes 0..12 = position vec3, byte 12..16 =
|
|
1270
|
+
// invRangeSquared f32, bytes 16..28 = color vec3, bytes 28..32 = i32
|
|
1271
|
+
// shadowAtlasLayer. Reading slot 7 via Int32Array view at the same
|
|
1272
|
+
// backing buffer must yield the i32 value the host packer wrote.
|
|
1273
|
+
const snap: PointLightSnapshot = {
|
|
1274
|
+
kind: 'point',
|
|
1275
|
+
position: vec3.create(0, 0, 0),
|
|
1276
|
+
color: vec3.create(0, 0, 0),
|
|
1277
|
+
intensity: 0,
|
|
1278
|
+
invRangeSquared: 0,
|
|
1279
|
+
shadowAtlasLayer: 2,
|
|
1280
|
+
};
|
|
1281
|
+
const out = packPointLight(snap);
|
|
1282
|
+
// Byte length is 32; the backing buffer is exactly the 8 f32 lanes.
|
|
1283
|
+
expect(out.byteLength).toBe(32);
|
|
1284
|
+
// i32 view starting at byte offset 28 (= slot 7 * 4 B / lane).
|
|
1285
|
+
const i32 = new Int32Array(out.buffer, 28, 1);
|
|
1286
|
+
expect(i32[0]).toBe(2);
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('zero-init non-shadow lanes (slot 0..6) are zero when snapshot is all-zero', () => {
|
|
1290
|
+
const snap: PointLightSnapshot = {
|
|
1291
|
+
kind: 'point',
|
|
1292
|
+
position: vec3.create(0, 0, 0),
|
|
1293
|
+
color: vec3.create(0, 0, 0),
|
|
1294
|
+
intensity: 0,
|
|
1295
|
+
invRangeSquared: 0,
|
|
1296
|
+
};
|
|
1297
|
+
const out = packPointLight(snap);
|
|
1298
|
+
for (let i = 0; i < 7; i++) expect(out[i]).toBe(0);
|
|
1299
|
+
// Slot 7 is shadowAtlasLayer sentinel -1 (no longer zero pad).
|
|
1300
|
+
const i32 = new Int32Array(out.buffer);
|
|
1301
|
+
expect(i32[7]).toBe(-1);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
describe('packSpotLight - std430 64B layout (feat-20260625 M2 w6)', () => {
|
|
1306
|
+
// feat-20260625-spot-light-shadow-mapping M2 w6 (D-4): SpotLight std430
|
|
1307
|
+
// stride 48 -> 64 (12 -> 16 floats). Slots 0..11 keep the prior layout;
|
|
1308
|
+
// slots 12..14 are vec4-alignment padding; slot 15 is shadowAtlasTile i32
|
|
1309
|
+
// (sentinel -1 = unassigned/clipped) written via an Int32Array view.
|
|
1310
|
+
function makeSnap(shadowAtlasTile: number): SpotLightSnapshot {
|
|
1311
|
+
return {
|
|
1312
|
+
kind: 'spot',
|
|
1313
|
+
entity: 0,
|
|
1314
|
+
position: vec3.create(3.0, 4.0, 5.0),
|
|
1315
|
+
direction: vec3.create(0.0, -1.0, 0.0),
|
|
1316
|
+
color: vec3.create(0.8, 0.7, 0.6),
|
|
1317
|
+
intensity: 1,
|
|
1318
|
+
invRangeSquared: 0.0625,
|
|
1319
|
+
cosInner: 0.984,
|
|
1320
|
+
cosOuter: 0.866,
|
|
1321
|
+
castShadow: true,
|
|
1322
|
+
lightViewProj: new Float32Array(16),
|
|
1323
|
+
mapSize: 2048,
|
|
1324
|
+
nearPlane: 0.1,
|
|
1325
|
+
farPlane: 50,
|
|
1326
|
+
shadowAtlasTile,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
it('emits 16 floats / 64 bytes; slots 0..11 unchanged from the 48B layout', () => {
|
|
1331
|
+
const out = packSpotLight(makeSnap(0));
|
|
1332
|
+
expect(out).toBeInstanceOf(Float32Array);
|
|
1333
|
+
expect(out.length).toBe(16);
|
|
1334
|
+
expect(out.byteLength).toBe(64);
|
|
1335
|
+
// Slot 0..2: position vec3.
|
|
1336
|
+
expect(out[0]).toBeCloseTo(3.0, 6);
|
|
1337
|
+
expect(out[1]).toBeCloseTo(4.0, 6);
|
|
1338
|
+
expect(out[2]).toBeCloseTo(5.0, 6);
|
|
1339
|
+
// Slot 3: invRangeSquared (packed into position.w lane).
|
|
1340
|
+
expect(out[3]).toBeCloseTo(0.0625, 6);
|
|
1341
|
+
// Slot 4..6: color.
|
|
1342
|
+
expect(out[4]).toBeCloseTo(0.8, 6);
|
|
1343
|
+
expect(out[5]).toBeCloseTo(0.7, 6);
|
|
1344
|
+
expect(out[6]).toBeCloseTo(0.6, 6);
|
|
1345
|
+
// Slot 7: cosInner (packed into color.w lane).
|
|
1346
|
+
expect(out[7]).toBeCloseTo(0.984, 6);
|
|
1347
|
+
// Slot 8..10: direction vec3.
|
|
1348
|
+
expect(out[8]).toBeCloseTo(0.0, 6);
|
|
1349
|
+
expect(out[9]).toBeCloseTo(-1.0, 6);
|
|
1350
|
+
expect(out[10]).toBeCloseTo(0.0, 6);
|
|
1351
|
+
// Slot 11: cosOuter (packed into direction.w lane).
|
|
1352
|
+
expect(out[11]).toBeCloseTo(0.866, 6);
|
|
1353
|
+
// Slot 12..14: vec4-alignment padding, zero-initialised.
|
|
1354
|
+
expect(out[12]).toBe(0);
|
|
1355
|
+
expect(out[13]).toBe(0);
|
|
1356
|
+
expect(out[14]).toBe(0);
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it('slot 15 carries shadowAtlasTile as i32 (tile=0)', () => {
|
|
1360
|
+
const out = packSpotLight(makeSnap(0));
|
|
1361
|
+
const i32 = new Int32Array(out.buffer);
|
|
1362
|
+
expect(i32[15]).toBe(0);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('slot 15 carries shadowAtlasTile sentinel -1 (0xFFFFFFFF bit pattern)', () => {
|
|
1366
|
+
const out = packSpotLight(makeSnap(-1));
|
|
1367
|
+
const i32 = new Int32Array(out.buffer);
|
|
1368
|
+
expect(i32[15]).toBe(-1);
|
|
1369
|
+
// -1 as i32 is 0xFFFFFFFF: reading the same lane as u32 confirms bits.
|
|
1370
|
+
const u32 = new Uint32Array(out.buffer);
|
|
1371
|
+
expect(u32[15]).toBe(0xffffffff);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it('slot 15 carries a valid tile index (tile=3, last cap slot)', () => {
|
|
1375
|
+
const out = packSpotLight(makeSnap(3));
|
|
1376
|
+
const i32 = new Int32Array(out.buffer);
|
|
1377
|
+
expect(i32[15]).toBe(3);
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
describe('packLightArrayHeader - 16B std430 header (M3 w17)', () => {
|
|
1382
|
+
it('emits 16 bytes: count u32 at offset 0; remaining 12 bytes zero-pad to 16B alignment', () => {
|
|
1383
|
+
const buf = packLightArrayHeader(3);
|
|
1384
|
+
expect(buf).toBeInstanceOf(ArrayBuffer);
|
|
1385
|
+
expect(buf.byteLength).toBe(16);
|
|
1386
|
+
const u32 = new Uint32Array(buf);
|
|
1387
|
+
expect(u32[0]).toBe(3);
|
|
1388
|
+
// Slots 1..3 (12B pad) zero-initialised.
|
|
1389
|
+
expect(u32[1]).toBe(0);
|
|
1390
|
+
expect(u32[2]).toBe(0);
|
|
1391
|
+
expect(u32[3]).toBe(0);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
it('count = 0 emits all-zero 16B header', () => {
|
|
1395
|
+
const buf = packLightArrayHeader(0);
|
|
1396
|
+
const u32 = new Uint32Array(buf);
|
|
1397
|
+
expect(u32[0]).toBe(0);
|
|
1398
|
+
expect(u32[1]).toBe(0);
|
|
1399
|
+
expect(u32[2]).toBe(0);
|
|
1400
|
+
expect(u32[3]).toBe(0);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
it('count = 4 (first-slice cap maximum) round-trips', () => {
|
|
1404
|
+
const buf = packLightArrayHeader(4);
|
|
1405
|
+
const u32 = new Uint32Array(buf);
|
|
1406
|
+
expect(u32[0]).toBe(4);
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
describe('byte-for-byte sanity (M3 w17)', () => {
|
|
1411
|
+
it('Float32Array(8) underlying buffer is 32B', () => {
|
|
1412
|
+
const f = new Float32Array(8);
|
|
1413
|
+
expect(f.byteLength).toBe(32);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('Float32Array(12) underlying buffer is 48B', () => {
|
|
1417
|
+
const f = new Float32Array(12);
|
|
1418
|
+
expect(f.byteLength).toBe(48);
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
it('packPointLight slots are stable across two invocations (no shared backing store)', () => {
|
|
1422
|
+
const snap: PointLightSnapshot = {
|
|
1423
|
+
kind: 'point',
|
|
1424
|
+
position: vec3.create(1, 2, 3),
|
|
1425
|
+
color: vec3.create(0.1, 0.2, 0.3),
|
|
1426
|
+
intensity: 1,
|
|
1427
|
+
invRangeSquared: 0.5,
|
|
1428
|
+
};
|
|
1429
|
+
const a = packPointLight(snap);
|
|
1430
|
+
const b = packPointLight(snap);
|
|
1431
|
+
expect(a.buffer).not.toBe(b.buffer);
|
|
1432
|
+
// Compare slots 0..6 as f32 (point/color/invRangeSquared are floats);
|
|
1433
|
+
// slot 7 is i32 (shadowAtlasLayer; reading as f32 yields NaN by design
|
|
1434
|
+
// for the sentinel -1 = 0xFFFFFFFF). Compare slot 7 via Int32Array.
|
|
1435
|
+
for (let i = 0; i < 7; i++) expect(a[i]).toBeCloseTo(b[i] ?? Number.NaN, EPSILON);
|
|
1436
|
+
const ai32 = new Int32Array(a.buffer);
|
|
1437
|
+
const bi32 = new Int32Array(b.buffer);
|
|
1438
|
+
expect(ai32[7]).toBe(bi32[7]);
|
|
1439
|
+
});
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
{
|
|
1445
|
+
// ─── from light-helpers.test.ts ───
|
|
1446
|
+
describe('light-helpers.test.ts', () => {
|
|
1447
|
+
describe('degToCos (M2 w10)', () => {
|
|
1448
|
+
const EPSILON = 1e-7;
|
|
1449
|
+
|
|
1450
|
+
it('0 deg maps to 1.0 (cos 0)', () => {
|
|
1451
|
+
expect(degToCos(0)).toBeCloseTo(1.0, 7);
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
it('45 deg maps to cos(pi / 4) ~ 0.7071067', () => {
|
|
1455
|
+
expect(degToCos(45)).toBeCloseTo(Math.SQRT1_2, 7);
|
|
1456
|
+
expect(Math.abs(degToCos(45) - Math.cos(Math.PI / 4))).toBeLessThan(EPSILON);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('60 deg maps to 0.5 (cos pi / 3)', () => {
|
|
1460
|
+
expect(degToCos(60)).toBeCloseTo(0.5, 7);
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
it('90 deg maps to ~0 (cos pi / 2)', () => {
|
|
1464
|
+
expect(Math.abs(degToCos(90))).toBeLessThan(EPSILON);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('30 deg matches cos(pi / 6) ~ 0.8660254', () => {
|
|
1468
|
+
expect(degToCos(30)).toBeCloseTo(Math.cos(Math.PI / 6), 7);
|
|
1469
|
+
});
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
describe('computeInvRangeSquared (M2 w11)', () => {
|
|
1473
|
+
const EPSILON = 1e-7;
|
|
1474
|
+
|
|
1475
|
+
it('range = +Infinity -> 0 (no truncation; pulls quartic factor to 1)', () => {
|
|
1476
|
+
expect(computeInvRangeSquared(Number.POSITIVE_INFINITY)).toBe(0);
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
it('range = 0 -> 1e8 (NaN protection; 0 * Infinity = NaN guard)', () => {
|
|
1480
|
+
expect(computeInvRangeSquared(0)).toBe(1e8);
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
it('range = 10 -> 1 / (10 * 10) = 0.01', () => {
|
|
1484
|
+
expect(computeInvRangeSquared(10)).toBeCloseTo(0.01, 7);
|
|
1485
|
+
expect(Math.abs(computeInvRangeSquared(10) - 0.01)).toBeLessThan(EPSILON);
|
|
1486
|
+
});
|
|
1487
|
+
|
|
1488
|
+
it('range = 1 -> 1.0', () => {
|
|
1489
|
+
expect(computeInvRangeSquared(1)).toBeCloseTo(1.0, 7);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('range = 25 -> 1 / 625 = 0.0016', () => {
|
|
1493
|
+
expect(computeInvRangeSquared(25)).toBeCloseTo(1 / 625, 7);
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
{
|
|
1500
|
+
// ─── from lightslot-layout.test.ts ───
|
|
1501
|
+
describe('lightslot-layout.test.ts', () => {
|
|
1502
|
+
// ── test: BYTES_PER_LIGHT_SLOT === 64 absolute-value lock (AC-11 TS side) ─────
|
|
1503
|
+
|
|
1504
|
+
describe('BYTES_PER_LIGHT_SLOT', () => {
|
|
1505
|
+
it('is exactly 64 (AC-11 absolute-value lock)', () => {
|
|
1506
|
+
expect(BYTES_PER_LIGHT_SLOT).toBe(64);
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// ── test: LightSlotLayout byte-size lock ──────────────────────────────────────
|
|
1511
|
+
|
|
1512
|
+
describe('LIGHTSLOT_LAYOUT byte-size', () => {
|
|
1513
|
+
it('declares byteSize === 64', () => {
|
|
1514
|
+
expect(LIGHTSLOT_LAYOUT.byteSize).toBe(64);
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
it('declares floatCount === 16 (64 bytes / 4 bytes per f32)', () => {
|
|
1518
|
+
expect(LIGHTSLOT_LAYOUT.floatCount).toBe(16);
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
it('declares vec4Count === 4 (64 bytes / 16 bytes per vec4)', () => {
|
|
1522
|
+
expect(LIGHTSLOT_LAYOUT.vec4Count).toBe(4);
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// ── test: LightSlotLayout field offsets match WGSL LightSlot struct ────────────
|
|
1527
|
+
|
|
1528
|
+
describe('LIGHTSLOT_LAYOUT field offsets', () => {
|
|
1529
|
+
it('position at byte 0', () => {
|
|
1530
|
+
expect(LIGHTSLOT_LAYOUT.positionOffset).toBe(0);
|
|
1531
|
+
});
|
|
1532
|
+
it('invRangeSquared at byte 12 (lane .w of vec4[0])', () => {
|
|
1533
|
+
expect(LIGHTSLOT_LAYOUT.invRangeSquaredOffset).toBe(12);
|
|
1534
|
+
});
|
|
1535
|
+
it('color at byte 16', () => {
|
|
1536
|
+
expect(LIGHTSLOT_LAYOUT.colorOffset).toBe(16);
|
|
1537
|
+
});
|
|
1538
|
+
it('cosInner at byte 28 (lane .w of vec4[1])', () => {
|
|
1539
|
+
expect(LIGHTSLOT_LAYOUT.cosInnerOffset).toBe(28);
|
|
1540
|
+
});
|
|
1541
|
+
it('direction at byte 32', () => {
|
|
1542
|
+
expect(LIGHTSLOT_LAYOUT.directionOffset).toBe(32);
|
|
1543
|
+
});
|
|
1544
|
+
it('cosOuter at byte 44 (lane .w of vec4[2])', () => {
|
|
1545
|
+
expect(LIGHTSLOT_LAYOUT.cosOuterOffset).toBe(44);
|
|
1546
|
+
});
|
|
1547
|
+
it('kind at byte 48', () => {
|
|
1548
|
+
expect(LIGHTSLOT_LAYOUT.kindOffset).toBe(48);
|
|
1549
|
+
});
|
|
1550
|
+
it('pad at byte 52 (3 x u32 = 12 bytes of padding)', () => {
|
|
1551
|
+
expect(LIGHTSLOT_LAYOUT.shadowPayloadOffset).toBe(52);
|
|
1552
|
+
});
|
|
1553
|
+
// feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
|
|
1554
|
+
// pad lanes carry the per-light shadow triple on the HDRP path.
|
|
1555
|
+
it('shadowAtlasLayer at byte 52 (i32 sentinel; pad lane .y of vec4[3])', () => {
|
|
1556
|
+
// Pad layout reads:
|
|
1557
|
+
// byte 52..56 = shadowAtlasLayer (i32; default sentinel -1)
|
|
1558
|
+
// byte 56..60 = near (f32 bits)
|
|
1559
|
+
// byte 60..64 = far (f32 bits)
|
|
1560
|
+
const buf = new Float32Array(16);
|
|
1561
|
+
const i32 = new Int32Array(buf.buffer);
|
|
1562
|
+
// Place a non-default value to verify byte mapping.
|
|
1563
|
+
i32[13] = 7;
|
|
1564
|
+
expect(i32[13]).toBe(7);
|
|
1565
|
+
// Float lane access on the same backing buffer is unambiguous.
|
|
1566
|
+
buf[14] = 0.25; // near
|
|
1567
|
+
buf[15] = 50; // far
|
|
1568
|
+
expect(buf[14]).toBeCloseTo(0.25, 5);
|
|
1569
|
+
expect(buf[15]).toBeCloseTo(50, 5);
|
|
1570
|
+
});
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
// ── test: Float32Array(16).byteLength === 64 (one LightSlot strided) ──────────
|
|
1574
|
+
|
|
1575
|
+
describe('Float32Array representation of one LightSlot', () => {
|
|
1576
|
+
it('Float32Array(16).byteLength === 64', () => {
|
|
1577
|
+
const buf = new Float32Array(16);
|
|
1578
|
+
expect(buf.byteLength).toBe(64);
|
|
1579
|
+
});
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
// ── test: LightSlotKind closed enum (AC-12) ───────────────────────────────────
|
|
1583
|
+
|
|
1584
|
+
describe('LightSlotKind closed enum', () => {
|
|
1585
|
+
it('POINT === 0', () => {
|
|
1586
|
+
expect(LightSlotKind.POINT).toBe(0);
|
|
1587
|
+
});
|
|
1588
|
+
it('SPOT === 1', () => {
|
|
1589
|
+
expect(LightSlotKind.SPOT).toBe(1);
|
|
1590
|
+
});
|
|
1591
|
+
it('kind values are disjoint (0 vs 1)', () => {
|
|
1592
|
+
expect(LightSlotKind.POINT).not.toBe(LightSlotKind.SPOT);
|
|
1593
|
+
});
|
|
1594
|
+
it('only two members exist (0 and 1)', () => {
|
|
1595
|
+
const keys = Object.keys(LightSlotKind);
|
|
1596
|
+
// LightSlotKind has 2 const keys + TypeScript adds nothing else
|
|
1597
|
+
expect(keys.length).toBe(2);
|
|
1598
|
+
expect(keys).toContain('POINT');
|
|
1599
|
+
expect(keys).toContain('SPOT');
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
{
|
|
1606
|
+
// ─── from point-light-defaults.test.ts ───
|
|
1607
|
+
describe('point-light-defaults.test.ts', () => {
|
|
1608
|
+
describe('PointLight spawn default-value fallback (M1 w3)', () => {
|
|
1609
|
+
it('omitting all fields fills layer-2 defaults (color=[1,1,1], intensity=1, range=+Infinity)', () => {
|
|
1610
|
+
const world = new World();
|
|
1611
|
+
const e = world
|
|
1612
|
+
.spawn({
|
|
1613
|
+
component: PointLight,
|
|
1614
|
+
data: {},
|
|
1615
|
+
})
|
|
1616
|
+
.unwrap();
|
|
1617
|
+
|
|
1618
|
+
const view = world.get(e, PointLight).unwrap();
|
|
1619
|
+
expect(Array.from(view.color)).toEqual([1, 1, 1]);
|
|
1620
|
+
expect(view.intensity).toBe(1);
|
|
1621
|
+
expect(view.range).toBe(10.0);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
it('explicit fields override defaults', () => {
|
|
1625
|
+
const world = new World();
|
|
1626
|
+
const e = world
|
|
1627
|
+
.spawn({
|
|
1628
|
+
component: PointLight,
|
|
1629
|
+
data: {
|
|
1630
|
+
color: [0.9, 0.8, 0.7],
|
|
1631
|
+
intensity: 0.5,
|
|
1632
|
+
range: 12.5,
|
|
1633
|
+
},
|
|
1634
|
+
})
|
|
1635
|
+
.unwrap();
|
|
1636
|
+
const view = world.get(e, PointLight).unwrap();
|
|
1637
|
+
expect(view.color[0]).toBeCloseTo(0.9, 5);
|
|
1638
|
+
expect(view.color[1]).toBeCloseTo(0.8, 5);
|
|
1639
|
+
expect(view.color[2]).toBeCloseTo(0.7, 5);
|
|
1640
|
+
expect(view.intensity).toBe(0.5);
|
|
1641
|
+
expect(view.range).toBe(12.5);
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
it('partial spawn fills only missing fields (mix override + default)', () => {
|
|
1645
|
+
const world = new World();
|
|
1646
|
+
const e = world
|
|
1647
|
+
.spawn({
|
|
1648
|
+
component: PointLight,
|
|
1649
|
+
data: { range: 5 },
|
|
1650
|
+
})
|
|
1651
|
+
.unwrap();
|
|
1652
|
+
const view = world.get(e, PointLight).unwrap();
|
|
1653
|
+
expect(Array.from(view.color)).toEqual([1, 1, 1]);
|
|
1654
|
+
expect(view.intensity).toBe(1);
|
|
1655
|
+
expect(view.range).toBe(5);
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1658
|
+
it('autocomplete application point: payload.range / color / intensity inferred without as casts (AC-01)', () => {
|
|
1659
|
+
const world = new World();
|
|
1660
|
+
// The data argument shape is `Partial<ShapeOf<componentSchema(PointLight)>>`; each
|
|
1661
|
+
// optional field flows in as its field type (color as a numeric tuple)
|
|
1662
|
+
// so the call below type-checks without any `as` assertion. The very
|
|
1663
|
+
// fact that this body compiles is the AC-01 autocomplete witness (no
|
|
1664
|
+
// `as` casts; runtime assertions confirm the values landed).
|
|
1665
|
+
const e = world
|
|
1666
|
+
.spawn({
|
|
1667
|
+
component: PointLight,
|
|
1668
|
+
data: {
|
|
1669
|
+
range: 7,
|
|
1670
|
+
color: [0.25, 1, 1],
|
|
1671
|
+
intensity: 2,
|
|
1672
|
+
},
|
|
1673
|
+
})
|
|
1674
|
+
.unwrap();
|
|
1675
|
+
const view = world.get(e, PointLight).unwrap();
|
|
1676
|
+
expect(view.range).toBe(7);
|
|
1677
|
+
expect(view.color[0]).toBeCloseTo(0.25, 5);
|
|
1678
|
+
expect(view.intensity).toBe(2);
|
|
1679
|
+
});
|
|
1680
|
+
});
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
{
|
|
1685
|
+
// ─── from render-multi-light-cap.test.ts ───
|
|
1686
|
+
describe('render-multi-light-cap.test.ts', () => {
|
|
1687
|
+
const ENGINE = '../createRenderer';
|
|
1688
|
+
|
|
1689
|
+
interface MockGL2Context {
|
|
1690
|
+
__mockTag: 'webgl2';
|
|
1691
|
+
getExtension: () => null;
|
|
1692
|
+
getParameter: () => number;
|
|
1693
|
+
isContextLost: () => boolean;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function makeMockGL2(): MockGL2Context {
|
|
1697
|
+
return {
|
|
1698
|
+
__mockTag: 'webgl2',
|
|
1699
|
+
getExtension: () => null,
|
|
1700
|
+
getParameter: () => 1,
|
|
1701
|
+
isContextLost: () => false,
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
interface CanvasOptions {
|
|
1706
|
+
webgl2: 'context' | 'null';
|
|
1707
|
+
webgpu?: 'context' | 'null';
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
|
|
1711
|
+
const canvas = {
|
|
1712
|
+
width: 800,
|
|
1713
|
+
height: 600,
|
|
1714
|
+
getContext(kind: string): unknown {
|
|
1715
|
+
if (kind === 'webgl2') {
|
|
1716
|
+
return opts.webgl2 === 'context' ? makeMockGL2() : null;
|
|
1717
|
+
}
|
|
1718
|
+
if (kind === 'webgpu') {
|
|
1719
|
+
if (opts.webgpu === 'context') {
|
|
1720
|
+
return {
|
|
1721
|
+
__mockTag: 'webgpu-canvas-context',
|
|
1722
|
+
configure: () => undefined,
|
|
1723
|
+
unconfigure: () => undefined,
|
|
1724
|
+
getCurrentTexture: () => ({ createView: () => ({}) }),
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
return null;
|
|
1730
|
+
},
|
|
1731
|
+
addEventListener: () => undefined,
|
|
1732
|
+
removeEventListener: () => undefined,
|
|
1733
|
+
};
|
|
1734
|
+
return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
interface DeviceCallLog {
|
|
1738
|
+
encoderFinishCount: number;
|
|
1739
|
+
drawIndexedCount: number;
|
|
1740
|
+
setBindGroupCount: number;
|
|
1741
|
+
beginRenderPassCount: number;
|
|
1742
|
+
setPipelineCount: number;
|
|
1743
|
+
queueSubmitCount: number;
|
|
1744
|
+
writeBufferCount: number;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function makeMockGPUDevice(log: DeviceCallLog): { device: unknown } {
|
|
1748
|
+
const lost = new Promise<unknown>(() => undefined);
|
|
1749
|
+
const device = {
|
|
1750
|
+
__mockTag: 'gpu-device',
|
|
1751
|
+
lost,
|
|
1752
|
+
features: new Set(),
|
|
1753
|
+
limits: {},
|
|
1754
|
+
queue: {
|
|
1755
|
+
submit: () => {
|
|
1756
|
+
log.queueSubmitCount++;
|
|
1757
|
+
},
|
|
1758
|
+
writeBuffer: () => {
|
|
1759
|
+
log.writeBufferCount++;
|
|
1760
|
+
},
|
|
1761
|
+
writeTexture: () => undefined,
|
|
1762
|
+
},
|
|
1763
|
+
createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
|
|
1764
|
+
createBindGroupLayout: () => ({}),
|
|
1765
|
+
createPipelineLayout: () => ({}),
|
|
1766
|
+
createRenderPipeline: () => ({}),
|
|
1767
|
+
createBindGroup: () => ({}),
|
|
1768
|
+
createBuffer: () => ({
|
|
1769
|
+
getMappedRange: () => new ArrayBuffer(64),
|
|
1770
|
+
unmap: () => undefined,
|
|
1771
|
+
}),
|
|
1772
|
+
createCommandEncoder: () => ({
|
|
1773
|
+
beginRenderPass: () => {
|
|
1774
|
+
log.beginRenderPassCount++;
|
|
1775
|
+
return {
|
|
1776
|
+
setPipeline: () => {
|
|
1777
|
+
log.setPipelineCount++;
|
|
1778
|
+
},
|
|
1779
|
+
setVertexBuffer: () => undefined,
|
|
1780
|
+
setIndexBuffer: () => undefined,
|
|
1781
|
+
setBindGroup: () => {
|
|
1782
|
+
log.setBindGroupCount++;
|
|
1783
|
+
},
|
|
1784
|
+
draw: () => undefined,
|
|
1785
|
+
drawIndexed: () => {
|
|
1786
|
+
log.drawIndexedCount++;
|
|
1787
|
+
},
|
|
1788
|
+
end: () => undefined,
|
|
1789
|
+
};
|
|
1790
|
+
},
|
|
1791
|
+
finish: () => {
|
|
1792
|
+
log.encoderFinishCount++;
|
|
1793
|
+
return {};
|
|
1794
|
+
},
|
|
1795
|
+
}),
|
|
1796
|
+
createTexture: () => ({ createView: () => ({}) }),
|
|
1797
|
+
createSampler: () => ({}),
|
|
1798
|
+
destroy: () => undefined,
|
|
1799
|
+
};
|
|
1800
|
+
return { device };
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function makeMockGPU(deviceObj: unknown): unknown {
|
|
1804
|
+
return {
|
|
1805
|
+
requestAdapter: async () => ({
|
|
1806
|
+
requestDevice: async () => deviceObj,
|
|
1807
|
+
}),
|
|
1808
|
+
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const baseNavigator: Navigator = {
|
|
1813
|
+
userAgent: 'mock-engine-test',
|
|
1814
|
+
} as Partial<Navigator> as Navigator;
|
|
1815
|
+
|
|
1816
|
+
function buildManifestDataUrl(): string {
|
|
1817
|
+
const manifest = {
|
|
1818
|
+
schemaVersion: '1.0.0',
|
|
1819
|
+
entries: [
|
|
1820
|
+
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
1821
|
+
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
1822
|
+
{
|
|
1823
|
+
hash: 'tonemap0',
|
|
1824
|
+
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
1825
|
+
glsl: '',
|
|
1826
|
+
bindings: '',
|
|
1827
|
+
},
|
|
1828
|
+
],
|
|
1829
|
+
};
|
|
1830
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function makeLog(): DeviceCallLog {
|
|
1834
|
+
return {
|
|
1835
|
+
encoderFinishCount: 0,
|
|
1836
|
+
drawIndexedCount: 0,
|
|
1837
|
+
setBindGroupCount: 0,
|
|
1838
|
+
beginRenderPassCount: 0,
|
|
1839
|
+
setPipelineCount: 0,
|
|
1840
|
+
queueSubmitCount: 0,
|
|
1841
|
+
writeBufferCount: 0,
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
beforeEach(() => {
|
|
1846
|
+
vi.stubGlobal('navigator', { ...baseNavigator });
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
afterEach(() => {
|
|
1850
|
+
vi.unstubAllGlobals();
|
|
1851
|
+
});
|
|
1852
|
+
|
|
1853
|
+
async function importEngine(): Promise<{
|
|
1854
|
+
createRenderer: (
|
|
1855
|
+
canvas: unknown,
|
|
1856
|
+
opts?: unknown,
|
|
1857
|
+
bundler?: unknown,
|
|
1858
|
+
) => Promise<{
|
|
1859
|
+
backend: string;
|
|
1860
|
+
ready: Promise<void>;
|
|
1861
|
+
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1862
|
+
onError: (
|
|
1863
|
+
cb: (err: { code: string; detail?: unknown; hint?: string; expected?: string }) => void,
|
|
1864
|
+
) => () => void;
|
|
1865
|
+
}>;
|
|
1866
|
+
}> {
|
|
1867
|
+
return (await import(ENGINE)) as never;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
async function importEcs(): Promise<{
|
|
1871
|
+
World: new () => {
|
|
1872
|
+
spawn: (...componentDatas: unknown[]) => unknown;
|
|
1873
|
+
};
|
|
1874
|
+
}> {
|
|
1875
|
+
return (await import('@forgeax/engine-ecs')) as never;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
async function importComponents(): Promise<{
|
|
1879
|
+
Transform: unknown;
|
|
1880
|
+
MeshFilter: unknown;
|
|
1881
|
+
MeshRenderer: unknown;
|
|
1882
|
+
Camera: unknown;
|
|
1883
|
+
DirectionalLight: unknown;
|
|
1884
|
+
PointLight: unknown;
|
|
1885
|
+
SpotLight: unknown;
|
|
1886
|
+
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
1887
|
+
HANDLE_TRIANGLE: Handle<'MeshAsset', 'shared'>;
|
|
1888
|
+
}> {
|
|
1889
|
+
return {
|
|
1890
|
+
...(await import('@forgeax/engine-render/internal')),
|
|
1891
|
+
...(await import('@forgeax/engine-scene')),
|
|
1892
|
+
} as never;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
interface TestSetup {
|
|
1896
|
+
createRenderer: (
|
|
1897
|
+
canvas: unknown,
|
|
1898
|
+
opts?: unknown,
|
|
1899
|
+
bundler?: unknown,
|
|
1900
|
+
) => Promise<{
|
|
1901
|
+
backend: string;
|
|
1902
|
+
ready: Promise<void>;
|
|
1903
|
+
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1904
|
+
onError: (
|
|
1905
|
+
cb: (err: { code: string; detail?: unknown; hint?: string; expected?: string }) => void,
|
|
1906
|
+
) => () => void;
|
|
1907
|
+
}>;
|
|
1908
|
+
log: DeviceCallLog;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
async function setupWebGPU(): Promise<TestSetup> {
|
|
1912
|
+
const log = makeLog();
|
|
1913
|
+
const { device } = makeMockGPUDevice(log);
|
|
1914
|
+
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
1915
|
+
const engine = await importEngine();
|
|
1916
|
+
return { createRenderer: engine.createRenderer, log };
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function identityTransform(): Record<string, number[]> {
|
|
1920
|
+
return { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] };
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function cameraTransform(): Record<string, number[]> {
|
|
1924
|
+
return { ...identityTransform(), pos: [0, 0, 3] };
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
function pointLightData(intensity: number): Record<string, number | number[]> {
|
|
1928
|
+
return { color: [1, 1, 1], intensity, range: 10 };
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function spotLightData(intensity: number): Record<string, number | number[]> {
|
|
1932
|
+
return {
|
|
1933
|
+
direction: [0, -1, 0],
|
|
1934
|
+
color: [1, 1, 1],
|
|
1935
|
+
intensity,
|
|
1936
|
+
range: 10,
|
|
1937
|
+
innerConeDeg: 10,
|
|
1938
|
+
outerConeDeg: 30,
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function directionalLightData(intensity: number): Record<string, number | number[]> {
|
|
1943
|
+
return {
|
|
1944
|
+
direction: [0, -1, 0],
|
|
1945
|
+
color: [1, 1, 1],
|
|
1946
|
+
intensity,
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
describe('record-time multi-light cap fail-fast (M3 w19)', () => {
|
|
1951
|
+
it("PointLight N>4 fires warn-once console.warn with detail.type='point'", async () => {
|
|
1952
|
+
const { createRenderer } = await setupWebGPU();
|
|
1953
|
+
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
1954
|
+
const renderer = await createRenderer(
|
|
1955
|
+
canvas,
|
|
1956
|
+
{},
|
|
1957
|
+
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
1958
|
+
);
|
|
1959
|
+
await renderer.ready;
|
|
1960
|
+
const { World } = await importEcs();
|
|
1961
|
+
const C = await importComponents();
|
|
1962
|
+
const world = new World();
|
|
1963
|
+
world.spawn(
|
|
1964
|
+
{ component: C.Camera, data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 100 } },
|
|
1965
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
1966
|
+
);
|
|
1967
|
+
// 5 PointLights (N=5 > 4 first-slice cap).
|
|
1968
|
+
for (let i = 0; i < 5; i++) {
|
|
1969
|
+
world.spawn(
|
|
1970
|
+
{
|
|
1971
|
+
component: C.Transform,
|
|
1972
|
+
data: { ...identityTransform(), pos: [i, 0, 0] },
|
|
1973
|
+
},
|
|
1974
|
+
{ component: C.PointLight, data: pointLightData(1) },
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1979
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1980
|
+
throw new Error('World attachment failed');
|
|
1981
|
+
}
|
|
1982
|
+
(world as WorldType).update().unwrap();
|
|
1983
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1984
|
+
|
|
1985
|
+
const multiLightCalls = warnSpy.mock.calls.filter(
|
|
1986
|
+
(c) =>
|
|
1987
|
+
typeof c[0] === 'string' &&
|
|
1988
|
+
c[0].startsWith('[forgeax] render-system-multi-light point:'),
|
|
1989
|
+
);
|
|
1990
|
+
expect(multiLightCalls.length).toBe(1);
|
|
1991
|
+
const detail = multiLightCalls[0]?.[1] as
|
|
1992
|
+
| { detail?: { type: string; got: number } }
|
|
1993
|
+
| undefined;
|
|
1994
|
+
expect(detail).toBeDefined();
|
|
1995
|
+
expect(detail?.detail?.type).toBe('point');
|
|
1996
|
+
expect(detail?.detail?.got).toBe(5);
|
|
1997
|
+
warnSpy.mockRestore();
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
it("SpotLight N>4 fires warn-once console.warn with detail.type='spot'", async () => {
|
|
2001
|
+
const { createRenderer } = await setupWebGPU();
|
|
2002
|
+
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
2003
|
+
const renderer = await createRenderer(
|
|
2004
|
+
canvas,
|
|
2005
|
+
{},
|
|
2006
|
+
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
2007
|
+
);
|
|
2008
|
+
await renderer.ready;
|
|
2009
|
+
const { World } = await importEcs();
|
|
2010
|
+
const C = await importComponents();
|
|
2011
|
+
const world = new World();
|
|
2012
|
+
world.spawn(
|
|
2013
|
+
{ component: C.Camera, data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 100 } },
|
|
2014
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2015
|
+
);
|
|
2016
|
+
for (let i = 0; i < 5; i++) {
|
|
2017
|
+
world.spawn(
|
|
2018
|
+
{ component: C.Transform, data: { ...identityTransform(), pos: [i, 0, 0] } },
|
|
2019
|
+
{ component: C.SpotLight, data: spotLightData(1) },
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
2024
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2025
|
+
throw new Error('World attachment failed');
|
|
2026
|
+
}
|
|
2027
|
+
(world as WorldType).update().unwrap();
|
|
2028
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2029
|
+
|
|
2030
|
+
const multiLightCalls = warnSpy.mock.calls.filter(
|
|
2031
|
+
(c) =>
|
|
2032
|
+
typeof c[0] === 'string' &&
|
|
2033
|
+
c[0].startsWith('[forgeax] render-system-multi-light spot:'),
|
|
2034
|
+
);
|
|
2035
|
+
expect(multiLightCalls.length).toBe(1);
|
|
2036
|
+
const detail = multiLightCalls[0]?.[1] as
|
|
2037
|
+
| { detail?: { type: string; got: number } }
|
|
2038
|
+
| undefined;
|
|
2039
|
+
expect(detail).toBeDefined();
|
|
2040
|
+
expect(detail?.detail?.type).toBe('spot');
|
|
2041
|
+
expect(detail?.detail?.got).toBe(5);
|
|
2042
|
+
warnSpy.mockRestore();
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
it("DirectionalLight N>1 fires warn-once console.warn with detail.type='directional' (regression guard)", async () => {
|
|
2046
|
+
const { createRenderer } = await setupWebGPU();
|
|
2047
|
+
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
2048
|
+
const renderer = await createRenderer(
|
|
2049
|
+
canvas,
|
|
2050
|
+
{},
|
|
2051
|
+
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
2052
|
+
);
|
|
2053
|
+
await renderer.ready;
|
|
2054
|
+
const { World } = await importEcs();
|
|
2055
|
+
const C = await importComponents();
|
|
2056
|
+
const world = new World();
|
|
2057
|
+
world.spawn(
|
|
2058
|
+
{ component: C.Camera, data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 100 } },
|
|
2059
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2060
|
+
);
|
|
2061
|
+
world.spawn({ component: C.DirectionalLight, data: directionalLightData(1) });
|
|
2062
|
+
world.spawn({ component: C.DirectionalLight, data: directionalLightData(2) });
|
|
2063
|
+
|
|
2064
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
2065
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2066
|
+
throw new Error('World attachment failed');
|
|
2067
|
+
}
|
|
2068
|
+
(world as WorldType).update().unwrap();
|
|
2069
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2070
|
+
|
|
2071
|
+
const multiLightCalls = warnSpy.mock.calls.filter(
|
|
2072
|
+
(c) =>
|
|
2073
|
+
typeof c[0] === 'string' &&
|
|
2074
|
+
c[0].startsWith('[forgeax] render-system-multi-light directional:'),
|
|
2075
|
+
);
|
|
2076
|
+
expect(multiLightCalls.length).toBeGreaterThanOrEqual(1);
|
|
2077
|
+
const detail = multiLightCalls[0]?.[1] as
|
|
2078
|
+
| { detail?: { type: string; got: number } }
|
|
2079
|
+
| undefined;
|
|
2080
|
+
expect(detail?.detail?.type).toBe('directional');
|
|
2081
|
+
expect(detail?.detail?.got).toBe(2);
|
|
2082
|
+
warnSpy.mockRestore();
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
it('exactly 4 PointLights pass silently (first-slice cap at N=4)', async () => {
|
|
2086
|
+
const { createRenderer } = await setupWebGPU();
|
|
2087
|
+
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
2088
|
+
const renderer = await createRenderer(
|
|
2089
|
+
canvas,
|
|
2090
|
+
{},
|
|
2091
|
+
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
2092
|
+
);
|
|
2093
|
+
await renderer.ready;
|
|
2094
|
+
const { World } = await importEcs();
|
|
2095
|
+
const C = await importComponents();
|
|
2096
|
+
const world = new World();
|
|
2097
|
+
world.spawn(
|
|
2098
|
+
{ component: C.Camera, data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 100 } },
|
|
2099
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2100
|
+
);
|
|
2101
|
+
for (let i = 0; i < 4; i++) {
|
|
2102
|
+
world.spawn(
|
|
2103
|
+
{ component: C.Transform, data: { ...identityTransform(), pos: [i, 0, 0] } },
|
|
2104
|
+
{ component: C.PointLight, data: pointLightData(1) },
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const errors: { code: string }[] = [];
|
|
2109
|
+
renderer.onError((e) => errors.push(e));
|
|
2110
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2111
|
+
throw new Error('World attachment failed');
|
|
2112
|
+
}
|
|
2113
|
+
(world as WorldType).update().unwrap();
|
|
2114
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2115
|
+
|
|
2116
|
+
expect(errors.some((e) => e.code === 'render-system-multi-light')).toBe(false);
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
it('4 PointLights + 4 SpotLights together pass silently (cap is per-bucket)', async () => {
|
|
2120
|
+
const { createRenderer } = await setupWebGPU();
|
|
2121
|
+
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
2122
|
+
const renderer = await createRenderer(
|
|
2123
|
+
canvas,
|
|
2124
|
+
{},
|
|
2125
|
+
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
2126
|
+
);
|
|
2127
|
+
await renderer.ready;
|
|
2128
|
+
const { World } = await importEcs();
|
|
2129
|
+
const C = await importComponents();
|
|
2130
|
+
const world = new World();
|
|
2131
|
+
world.spawn(
|
|
2132
|
+
{ component: C.Camera, data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 100 } },
|
|
2133
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2134
|
+
);
|
|
2135
|
+
for (let i = 0; i < 4; i++) {
|
|
2136
|
+
world.spawn(
|
|
2137
|
+
{ component: C.Transform, data: { ...identityTransform(), pos: [i, 0, 0] } },
|
|
2138
|
+
{ component: C.PointLight, data: pointLightData(1) },
|
|
2139
|
+
);
|
|
2140
|
+
world.spawn(
|
|
2141
|
+
{ component: C.Transform, data: { ...identityTransform(), pos: [0, i, 0] } },
|
|
2142
|
+
{ component: C.SpotLight, data: spotLightData(1) },
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
const errors: { code: string }[] = [];
|
|
2147
|
+
renderer.onError((e) => errors.push(e));
|
|
2148
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2149
|
+
throw new Error('World attachment failed');
|
|
2150
|
+
}
|
|
2151
|
+
(world as WorldType).update().unwrap();
|
|
2152
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2153
|
+
|
|
2154
|
+
expect(errors.some((e) => e.code === 'render-system-multi-light')).toBe(false);
|
|
2155
|
+
});
|
|
2156
|
+
});
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
{
|
|
2161
|
+
// ─── from spot-light-defaults.test.ts ───
|
|
2162
|
+
describe('spot-light-defaults.test.ts', () => {
|
|
2163
|
+
describe('SpotLight spawn default-value fallback (M1 w6)', () => {
|
|
2164
|
+
it('omitting cone / color / intensity / range fills layer-2 defaults', () => {
|
|
2165
|
+
const world = new World();
|
|
2166
|
+
const e = world
|
|
2167
|
+
.spawn({
|
|
2168
|
+
component: SpotLight,
|
|
2169
|
+
data: { direction: [0, -1, 0] },
|
|
2170
|
+
})
|
|
2171
|
+
.unwrap();
|
|
2172
|
+
|
|
2173
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2174
|
+
expect(Array.from(view.direction)).toEqual([0, -1, 0]);
|
|
2175
|
+
expect(Array.from(view.color)).toEqual([1, 1, 1]);
|
|
2176
|
+
expect(view.intensity).toBe(1);
|
|
2177
|
+
expect(view.range).toBe(10.0);
|
|
2178
|
+
expect(view.innerConeDeg).toBe(0);
|
|
2179
|
+
expect(view.outerConeDeg).toBe(45);
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
it('innerConeDeg=0 + outerConeDeg=45 matches KHR pi/4 equivalent', () => {
|
|
2183
|
+
const world = new World();
|
|
2184
|
+
const e = world
|
|
2185
|
+
.spawn({
|
|
2186
|
+
component: SpotLight,
|
|
2187
|
+
data: { direction: [0, -1, 0] },
|
|
2188
|
+
})
|
|
2189
|
+
.unwrap();
|
|
2190
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2191
|
+
const outerRad = (view.outerConeDeg * Math.PI) / 180;
|
|
2192
|
+
expect(outerRad).toBeCloseTo(Math.PI / 4, 5);
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
it('explicit cone degrees override defaults', () => {
|
|
2196
|
+
const world = new World();
|
|
2197
|
+
const e = world
|
|
2198
|
+
.spawn({
|
|
2199
|
+
component: SpotLight,
|
|
2200
|
+
data: {
|
|
2201
|
+
direction: [0, -1, 0],
|
|
2202
|
+
innerConeDeg: 15,
|
|
2203
|
+
outerConeDeg: 30,
|
|
2204
|
+
intensity: 2,
|
|
2205
|
+
range: 12,
|
|
2206
|
+
},
|
|
2207
|
+
})
|
|
2208
|
+
.unwrap();
|
|
2209
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2210
|
+
expect(view.innerConeDeg).toBe(15);
|
|
2211
|
+
expect(view.outerConeDeg).toBe(30);
|
|
2212
|
+
expect(view.intensity).toBe(2);
|
|
2213
|
+
expect(view.range).toBe(12);
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
it('autocomplete application point: payload.outerConeDeg / range / color inferred without as casts (AC-02)', () => {
|
|
2217
|
+
const world = new World();
|
|
2218
|
+
// The body type-checks without an `as` cast on any optional field
|
|
2219
|
+
// (innerConeDeg / outerConeDeg / range / color / intensity all flow as
|
|
2220
|
+
// their field types); compilation success is the AC-02 autocomplete
|
|
2221
|
+
// witness.
|
|
2222
|
+
const e = world
|
|
2223
|
+
.spawn({
|
|
2224
|
+
component: SpotLight,
|
|
2225
|
+
data: {
|
|
2226
|
+
direction: [1, 0, 0],
|
|
2227
|
+
outerConeDeg: 35,
|
|
2228
|
+
range: 9,
|
|
2229
|
+
color: [0.4, 1, 1],
|
|
2230
|
+
},
|
|
2231
|
+
})
|
|
2232
|
+
.unwrap();
|
|
2233
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2234
|
+
expect(view.outerConeDeg).toBe(35);
|
|
2235
|
+
expect(view.range).toBe(9);
|
|
2236
|
+
expect(view.color[0]).toBeCloseTo(0.4, 5);
|
|
2237
|
+
});
|
|
2238
|
+
});
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
{
|
|
2243
|
+
// ─── from feat-20260625-spot-light-shadow-mapping M1 w1 ───
|
|
2244
|
+
describe('feat-20260625-spot-light-shadow-mapping M1 w1', () => {
|
|
2245
|
+
describe('SpotLight embedded shadow schema defaults', () => {
|
|
2246
|
+
it('castShadow defaults to true (embedded, AC-02)', () => {
|
|
2247
|
+
const world = new World();
|
|
2248
|
+
const e = world
|
|
2249
|
+
.spawn({
|
|
2250
|
+
component: SpotLight,
|
|
2251
|
+
data: { direction: [0, -1, 0] },
|
|
2252
|
+
})
|
|
2253
|
+
.unwrap();
|
|
2254
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2255
|
+
expect(view.castShadow).toBe(true);
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2258
|
+
it('6 shadow fields align with DirectionalLight defaults', () => {
|
|
2259
|
+
const world = new World();
|
|
2260
|
+
const e = world
|
|
2261
|
+
.spawn({
|
|
2262
|
+
component: SpotLight,
|
|
2263
|
+
data: { direction: [0, -1, 0] },
|
|
2264
|
+
})
|
|
2265
|
+
.unwrap();
|
|
2266
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2267
|
+
expect(view.mapSize).toBe(2048);
|
|
2268
|
+
expect(view.depthBias).toBeCloseTo(0.005, 5);
|
|
2269
|
+
expect(view.normalBias).toBeCloseTo(0.05, 5);
|
|
2270
|
+
expect(view.nearPlane).toBeCloseTo(0.1, 5);
|
|
2271
|
+
expect(view.farPlane).toBeCloseTo(50, 5);
|
|
2272
|
+
expect(view.pcfKernelSize).toBe(3);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
it('spawn with omitted shadow fields fills all defaults from schema', () => {
|
|
2276
|
+
const world = new World();
|
|
2277
|
+
const e = world
|
|
2278
|
+
.spawn({
|
|
2279
|
+
component: SpotLight,
|
|
2280
|
+
data: { direction: [0, -1, 0] },
|
|
2281
|
+
})
|
|
2282
|
+
.unwrap();
|
|
2283
|
+
const view = world.get(e, SpotLight).unwrap();
|
|
2284
|
+
expect(view.castShadow).toBe(true);
|
|
2285
|
+
expect(view.mapSize).toBe(2048);
|
|
2286
|
+
expect(view.depthBias).toBeCloseTo(0.005, 5);
|
|
2287
|
+
expect(view.normalBias).toBeCloseTo(0.05, 5);
|
|
2288
|
+
expect(view.nearPlane).toBeCloseTo(0.1, 5);
|
|
2289
|
+
expect(view.farPlane).toBeCloseTo(50, 5);
|
|
2290
|
+
expect(view.pcfKernelSize).toBe(3);
|
|
2291
|
+
});
|
|
2292
|
+
});
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
{
|
|
2297
|
+
// ─── from feat-20260621-merge-directionallightshadow-into-directionallight M1-t1 ───
|
|
2298
|
+
describe('feat-20260621-merge-directionallightshadow-into-directionallight M1-t1', () => {
|
|
2299
|
+
describe('DirectionalLight merged shadow field defaults', () => {
|
|
2300
|
+
it('castShadow defaults to true', () => {
|
|
2301
|
+
const world = new World();
|
|
2302
|
+
const e = world
|
|
2303
|
+
.spawn({
|
|
2304
|
+
component: DirectionalLight,
|
|
2305
|
+
data: { direction: [0, -1, 0] },
|
|
2306
|
+
})
|
|
2307
|
+
.unwrap();
|
|
2308
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
2309
|
+
expect(view.castShadow).toBe(true);
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
it('spawn with omitted shadow fields fills 8 merged defaults', () => {
|
|
2313
|
+
const world = new World();
|
|
2314
|
+
const e = world
|
|
2315
|
+
.spawn({
|
|
2316
|
+
component: DirectionalLight,
|
|
2317
|
+
data: { direction: [0, -1, 0] },
|
|
2318
|
+
})
|
|
2319
|
+
.unwrap();
|
|
2320
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
2321
|
+
expect(view.cascadeCount).toBe(4);
|
|
2322
|
+
expect(view.splitLambda).toBeCloseTo(0.75, 5);
|
|
2323
|
+
expect(view.cascadeBlend).toBeCloseTo(0.2, 5);
|
|
2324
|
+
expect(view.mapSize).toBe(2048);
|
|
2325
|
+
expect(view.depthBias).toBeCloseTo(0.005, 5);
|
|
2326
|
+
expect(view.normalBias).toBeCloseTo(0.05, 5);
|
|
2327
|
+
expect(view.shadowDistance).toBeCloseTo(200, 5);
|
|
2328
|
+
expect(view.pcfKernelSize).toBe(3);
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
it('spawn with direction-only data gets all 8 shadow defaults', () => {
|
|
2332
|
+
const world = new World();
|
|
2333
|
+
const e = world
|
|
2334
|
+
.spawn({ component: DirectionalLight, data: { direction: [0, -1, 0] } })
|
|
2335
|
+
.unwrap();
|
|
2336
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
2337
|
+
expect(view.cascadeCount).toBe(4);
|
|
2338
|
+
expect(view.splitLambda).toBeCloseTo(0.75, 5);
|
|
2339
|
+
expect(view.cascadeBlend).toBeCloseTo(0.2, 5);
|
|
2340
|
+
expect(view.mapSize).toBe(2048);
|
|
2341
|
+
expect(view.depthBias).toBeCloseTo(0.005, 5);
|
|
2342
|
+
expect(view.normalBias).toBeCloseTo(0.05, 5);
|
|
2343
|
+
expect(view.shadowDistance).toBeCloseTo(200, 5);
|
|
2344
|
+
expect(view.pcfKernelSize).toBe(3);
|
|
2345
|
+
});
|
|
2346
|
+
|
|
2347
|
+
it('spawn with full explicit shadow data overrides all 9 defaults', () => {
|
|
2348
|
+
const world = new World();
|
|
2349
|
+
const e = world
|
|
2350
|
+
.spawn({
|
|
2351
|
+
component: DirectionalLight,
|
|
2352
|
+
data: {
|
|
2353
|
+
direction: [0, -1, 0],
|
|
2354
|
+
cascadeCount: 2,
|
|
2355
|
+
splitLambda: 0.5,
|
|
2356
|
+
cascadeBlend: 0.1,
|
|
2357
|
+
mapSize: 512,
|
|
2358
|
+
depthBias: 0.01,
|
|
2359
|
+
normalBias: 0.1,
|
|
2360
|
+
shadowDistance: 100,
|
|
2361
|
+
pcfKernelSize: 5,
|
|
2362
|
+
},
|
|
2363
|
+
})
|
|
2364
|
+
.unwrap();
|
|
2365
|
+
const view = world.get(e, DirectionalLight).unwrap();
|
|
2366
|
+
expect(view.cascadeCount).toBe(2);
|
|
2367
|
+
expect(view.splitLambda).toBeCloseTo(0.5, 5);
|
|
2368
|
+
expect(view.cascadeBlend).toBeCloseTo(0.1, 5);
|
|
2369
|
+
expect(view.mapSize).toBe(512);
|
|
2370
|
+
expect(view.depthBias).toBeCloseTo(0.01, 5);
|
|
2371
|
+
expect(view.normalBias).toBeCloseTo(0.1, 5);
|
|
2372
|
+
expect(view.shadowDistance).toBeCloseTo(100, 5);
|
|
2373
|
+
expect(view.pcfKernelSize).toBe(5);
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
it('schema has 12 fields (3 light + 1 castShadow + 8 shadow)', () => {
|
|
2377
|
+
expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(12);
|
|
2378
|
+
expect('direction' in componentSchema(DirectionalLight)).toBe(true);
|
|
2379
|
+
expect('color' in componentSchema(DirectionalLight)).toBe(true);
|
|
2380
|
+
expect('castShadow' in componentSchema(DirectionalLight)).toBe(true);
|
|
2381
|
+
expect('cascadeCount' in componentSchema(DirectionalLight)).toBe(true);
|
|
2382
|
+
expect('splitLambda' in componentSchema(DirectionalLight)).toBe(true);
|
|
2383
|
+
expect('cascadeBlend' in componentSchema(DirectionalLight)).toBe(true);
|
|
2384
|
+
expect('mapSize' in componentSchema(DirectionalLight)).toBe(true);
|
|
2385
|
+
expect('depthBias' in componentSchema(DirectionalLight)).toBe(true);
|
|
2386
|
+
expect('normalBias' in componentSchema(DirectionalLight)).toBe(true);
|
|
2387
|
+
expect('shadowDistance' in componentSchema(DirectionalLight)).toBe(true);
|
|
2388
|
+
expect('nearPlane' in componentSchema(DirectionalLight)).toBe(false);
|
|
2389
|
+
expect('farPlane' in componentSchema(DirectionalLight)).toBe(false);
|
|
2390
|
+
expect('pcfKernelSize' in componentSchema(DirectionalLight)).toBe(true);
|
|
2391
|
+
});
|
|
2392
|
+
});
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
{
|
|
2397
|
+
// ─── from feat-20260621-merge-directionallightshadow-into-directionallight M1-t2 ───
|
|
2398
|
+
describe('feat-20260621-merge-directionallightshadow-into-directionallight M1-t2', () => {
|
|
2399
|
+
describe('DirectionalLight.validate() shadow field enforcement', () => {
|
|
2400
|
+
it('rejects even pcfKernelSize (2) with ShadowInvalidConfigError', () => {
|
|
2401
|
+
const world = new World();
|
|
2402
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2403
|
+
direction: [0, -1, 0],
|
|
2404
|
+
pcfKernelSize: 2,
|
|
2405
|
+
});
|
|
2406
|
+
expect(r.ok).toBe(false);
|
|
2407
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2408
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2409
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2410
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2411
|
+
expect(err.detail.value).toBe(2);
|
|
2412
|
+
expect(err.detail.min).toBe(1);
|
|
2413
|
+
// The hint must name the odd constraint so an AI retry does not loop
|
|
2414
|
+
// 2 -> 3 wrong-way (4 -> 6); verify-step AI-user finding F-6.
|
|
2415
|
+
expect(err.hint).toBe('set pcfKernelSize to an odd integer >= 1; got 2');
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
it('rejects pcfKernelSize < 1 (0) with ShadowInvalidConfigError', () => {
|
|
2419
|
+
const world = new World();
|
|
2420
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2421
|
+
direction: [0, -1, 0],
|
|
2422
|
+
pcfKernelSize: 0,
|
|
2423
|
+
});
|
|
2424
|
+
expect(r.ok).toBe(false);
|
|
2425
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2426
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2427
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2428
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2429
|
+
expect(err.detail.value).toBe(0);
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
it('rejects pcfKernelSize < 1 (-1) with ShadowInvalidConfigError', () => {
|
|
2433
|
+
const world = new World();
|
|
2434
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2435
|
+
direction: [0, -1, 0],
|
|
2436
|
+
pcfKernelSize: -1,
|
|
2437
|
+
});
|
|
2438
|
+
expect(r.ok).toBe(false);
|
|
2439
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2440
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2441
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2442
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2443
|
+
expect(err.detail.value).toBe(-1);
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2446
|
+
it('accepts pcfKernelSize=1 (smallest valid odd integer)', () => {
|
|
2447
|
+
const world = new World();
|
|
2448
|
+
const r = world.spawn({
|
|
2449
|
+
component: DirectionalLight,
|
|
2450
|
+
data: { direction: [0, -1, 0], pcfKernelSize: 1 },
|
|
2451
|
+
});
|
|
2452
|
+
expect(r.ok).toBe(true);
|
|
2453
|
+
});
|
|
2454
|
+
|
|
2455
|
+
it('accepts pcfKernelSize=3 (default odd integer)', () => {
|
|
2456
|
+
const world = new World();
|
|
2457
|
+
const r = world.spawn({
|
|
2458
|
+
component: DirectionalLight,
|
|
2459
|
+
data: { direction: [0, -1, 0], pcfKernelSize: 3 },
|
|
2460
|
+
});
|
|
2461
|
+
expect(r.ok).toBe(true);
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
it('accepts pcfKernelSize=7 (larger odd integer)', () => {
|
|
2465
|
+
const world = new World();
|
|
2466
|
+
const r = world.spawn({
|
|
2467
|
+
component: DirectionalLight,
|
|
2468
|
+
data: { direction: [0, -1, 0], pcfKernelSize: 7 },
|
|
2469
|
+
});
|
|
2470
|
+
expect(r.ok).toBe(true);
|
|
2471
|
+
});
|
|
2472
|
+
|
|
2473
|
+
it('rejects mapSize < 1 with ShadowInvalidConfigError', () => {
|
|
2474
|
+
const world = new World();
|
|
2475
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2476
|
+
direction: [0, -1, 0],
|
|
2477
|
+
mapSize: 0,
|
|
2478
|
+
});
|
|
2479
|
+
expect(r.ok).toBe(false);
|
|
2480
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2481
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2482
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2483
|
+
expect(err.detail.field).toBe('mapSize');
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it('rejects cascadeCount < 1 with ShadowInvalidConfigError', () => {
|
|
2487
|
+
const world = new World();
|
|
2488
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2489
|
+
direction: [0, -1, 0],
|
|
2490
|
+
cascadeCount: 0,
|
|
2491
|
+
});
|
|
2492
|
+
expect(r.ok).toBe(false);
|
|
2493
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2494
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2495
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2496
|
+
expect(err.detail.field).toBe('cascadeCount');
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
it('rejects cascadeCount > 4 with ShadowInvalidConfigError', () => {
|
|
2500
|
+
const world = new World();
|
|
2501
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2502
|
+
direction: [0, -1, 0],
|
|
2503
|
+
cascadeCount: 5,
|
|
2504
|
+
});
|
|
2505
|
+
expect(r.ok).toBe(false);
|
|
2506
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2507
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2508
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2509
|
+
expect(err.detail.field).toBe('cascadeCount');
|
|
2510
|
+
expect(err.detail.value).toBe(5);
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
it('rejects non-integer cascadeCount (1.5) with ShadowInvalidConfigError', () => {
|
|
2514
|
+
const world = new World();
|
|
2515
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2516
|
+
direction: [0, -1, 0],
|
|
2517
|
+
cascadeCount: 1.5,
|
|
2518
|
+
});
|
|
2519
|
+
expect(r.ok).toBe(false);
|
|
2520
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2521
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2522
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2523
|
+
expect(err.detail.field).toBe('cascadeCount');
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
it('rejects splitLambda < 0 with ShadowInvalidConfigError', () => {
|
|
2527
|
+
const world = new World();
|
|
2528
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2529
|
+
direction: [0, -1, 0],
|
|
2530
|
+
splitLambda: -0.1,
|
|
2531
|
+
});
|
|
2532
|
+
expect(r.ok).toBe(false);
|
|
2533
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2534
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2535
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2536
|
+
expect(err.detail.field).toBe('splitLambda');
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
it('rejects splitLambda > 1 with ShadowInvalidConfigError', () => {
|
|
2540
|
+
const world = new World();
|
|
2541
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2542
|
+
direction: [0, -1, 0],
|
|
2543
|
+
splitLambda: 1.1,
|
|
2544
|
+
});
|
|
2545
|
+
expect(r.ok).toBe(false);
|
|
2546
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2547
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2548
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2549
|
+
expect(err.detail.field).toBe('splitLambda');
|
|
2550
|
+
});
|
|
2551
|
+
|
|
2552
|
+
it('accepts splitLambda=0 (boundary valid)', () => {
|
|
2553
|
+
const world = new World();
|
|
2554
|
+
const r = world.spawn({
|
|
2555
|
+
component: DirectionalLight,
|
|
2556
|
+
data: { direction: [0, -1, 0], splitLambda: 0 },
|
|
2557
|
+
});
|
|
2558
|
+
expect(r.ok).toBe(true);
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
it('accepts splitLambda=1 (boundary valid)', () => {
|
|
2562
|
+
const world = new World();
|
|
2563
|
+
const r = world.spawn({
|
|
2564
|
+
component: DirectionalLight,
|
|
2565
|
+
data: { direction: [0, -1, 0], splitLambda: 1 },
|
|
2566
|
+
});
|
|
2567
|
+
expect(r.ok).toBe(true);
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
it('rejects cascadeBlend < 0 with ShadowInvalidConfigError', () => {
|
|
2571
|
+
const world = new World();
|
|
2572
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2573
|
+
direction: [0, -1, 0],
|
|
2574
|
+
cascadeBlend: -0.01,
|
|
2575
|
+
});
|
|
2576
|
+
expect(r.ok).toBe(false);
|
|
2577
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2578
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2579
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2580
|
+
expect(err.detail.field).toBe('cascadeBlend');
|
|
2581
|
+
});
|
|
2582
|
+
|
|
2583
|
+
it('rejects cascadeBlend > 0.5 with ShadowInvalidConfigError', () => {
|
|
2584
|
+
const world = new World();
|
|
2585
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2586
|
+
direction: [0, -1, 0],
|
|
2587
|
+
cascadeBlend: 0.51,
|
|
2588
|
+
});
|
|
2589
|
+
expect(r.ok).toBe(false);
|
|
2590
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2591
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2592
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2593
|
+
expect(err.detail.field).toBe('cascadeBlend');
|
|
2594
|
+
});
|
|
2595
|
+
|
|
2596
|
+
it('accepts cascadeBlend=0 (boundary valid)', () => {
|
|
2597
|
+
const world = new World();
|
|
2598
|
+
const r = world.spawn({
|
|
2599
|
+
component: DirectionalLight,
|
|
2600
|
+
data: { direction: [0, -1, 0], cascadeBlend: 0 },
|
|
2601
|
+
});
|
|
2602
|
+
expect(r.ok).toBe(true);
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
it('accepts cascadeBlend=0.5 (boundary valid)', () => {
|
|
2606
|
+
const world = new World();
|
|
2607
|
+
const r = world.spawn({
|
|
2608
|
+
component: DirectionalLight,
|
|
2609
|
+
data: { direction: [0, -1, 0], cascadeBlend: 0.5 },
|
|
2610
|
+
});
|
|
2611
|
+
expect(r.ok).toBe(true);
|
|
2612
|
+
});
|
|
2613
|
+
});
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
{
|
|
2618
|
+
// ─── from feat-20260621-merge-directionallightshadow-into-directionallight M1-t3 ───
|
|
2619
|
+
describe('feat-20260621-merge-directionallightshadow-into-directionallight M1-t3', () => {
|
|
2620
|
+
describe('DirectionalLight.validate() skips shadow validation when castShadow=false', () => {
|
|
2621
|
+
it('castShadow=false tolerates even pcfKernelSize (2) because unused', () => {
|
|
2622
|
+
const world = new World();
|
|
2623
|
+
const r = world.spawn({
|
|
2624
|
+
component: DirectionalLight,
|
|
2625
|
+
data: { direction: [0, -1, 0], castShadow: false, pcfKernelSize: 2 },
|
|
2626
|
+
});
|
|
2627
|
+
expect(r.ok).toBe(true);
|
|
2628
|
+
});
|
|
2629
|
+
|
|
2630
|
+
it('castShadow=false tolerates pcfKernelSize=0 because unused', () => {
|
|
2631
|
+
const world = new World();
|
|
2632
|
+
const r = world.spawn({
|
|
2633
|
+
component: DirectionalLight,
|
|
2634
|
+
data: { direction: [0, -1, 0], castShadow: false, pcfKernelSize: 0 },
|
|
2635
|
+
});
|
|
2636
|
+
expect(r.ok).toBe(true);
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
it('castShadow=false tolerates mapSize=0 because unused', () => {
|
|
2640
|
+
const world = new World();
|
|
2641
|
+
const r = world.spawn({
|
|
2642
|
+
component: DirectionalLight,
|
|
2643
|
+
data: { direction: [0, -1, 0], castShadow: false, mapSize: 0 },
|
|
2644
|
+
});
|
|
2645
|
+
expect(r.ok).toBe(true);
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2648
|
+
it('castShadow=false tolerates cascadeCount=5 (out of [1,4]) because unused', () => {
|
|
2649
|
+
const world = new World();
|
|
2650
|
+
const r = world.spawn({
|
|
2651
|
+
component: DirectionalLight,
|
|
2652
|
+
data: { direction: [0, -1, 0], castShadow: false, cascadeCount: 5 },
|
|
2653
|
+
});
|
|
2654
|
+
expect(r.ok).toBe(true);
|
|
2655
|
+
});
|
|
2656
|
+
|
|
2657
|
+
it('castShadow=false tolerates splitLambda=2 (out of [0,1]) because unused', () => {
|
|
2658
|
+
const world = new World();
|
|
2659
|
+
const r = world.spawn({
|
|
2660
|
+
component: DirectionalLight,
|
|
2661
|
+
data: { direction: [0, -1, 0], castShadow: false, splitLambda: 2 },
|
|
2662
|
+
});
|
|
2663
|
+
expect(r.ok).toBe(true);
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
it('castShadow=false tolerates cascadeBlend=1 (out of [0,0.5]) because unused', () => {
|
|
2667
|
+
const world = new World();
|
|
2668
|
+
const r = world.spawn({
|
|
2669
|
+
component: DirectionalLight,
|
|
2670
|
+
data: { direction: [0, -1, 0], castShadow: false, cascadeBlend: 1 },
|
|
2671
|
+
});
|
|
2672
|
+
expect(r.ok).toBe(true);
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
it('castShadow=true (explicit) still enforces pcfKernelSize odd>=1', () => {
|
|
2676
|
+
const world = new World();
|
|
2677
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2678
|
+
direction: [0, -1, 0],
|
|
2679
|
+
castShadow: true,
|
|
2680
|
+
pcfKernelSize: 2,
|
|
2681
|
+
});
|
|
2682
|
+
expect(r.ok).toBe(false);
|
|
2683
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2684
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2685
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2686
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
it('omitted castShadow (treated as default true) still enforces pcfKernelSize odd>=1', () => {
|
|
2690
|
+
const world = new World();
|
|
2691
|
+
const r = spawnValidatedLight(world, DirectionalLight, {
|
|
2692
|
+
direction: [0, -1, 0],
|
|
2693
|
+
pcfKernelSize: 2,
|
|
2694
|
+
});
|
|
2695
|
+
expect(r.ok).toBe(false);
|
|
2696
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2697
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2698
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2699
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2700
|
+
});
|
|
2701
|
+
});
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
// ─── from pbr-view-bgl-layout.test.ts ───
|
|
2705
|
+
describe('pbr-view-bgl-layout.test.ts', () => {
|
|
2706
|
+
// feat-20260625-spot-light-shadow-mapping M3 / w12 (D-5 + AC-08) +
|
|
2707
|
+
// w22 (D-1 fragment side, D-5 REVISED: binding 9 = matrix array).
|
|
2708
|
+
//
|
|
2709
|
+
// `buildPbrViewBglEntries` (pbr-pipeline.ts) is the runtime SSOT for the
|
|
2710
|
+
// @group(0) view bind-group layout. The matching WGSL binding declarations
|
|
2711
|
+
// live in common.wgsl (binding 0..9). This block locks the BGL shape so a
|
|
2712
|
+
// BGL <-> WGSL drift (e.g. binding 8 missing, wrong sampleType, or a binding
|
|
2713
|
+
// 9 declared as something other than a FRAGMENT uniform buffer) is caught at
|
|
2714
|
+
// unit time instead of as a WebGPU validation crash in the browser path
|
|
2715
|
+
// (memory: BGL shape mismatch is a browser-path-only bug).
|
|
2716
|
+
//
|
|
2717
|
+
// visibility flags mirror the WebGPU GPUShaderStage bitmask:
|
|
2718
|
+
// VERTEX = 0x1, FRAGMENT = 0x2.
|
|
2719
|
+
const VISIBILITY_FRAGMENT = 0x2;
|
|
2720
|
+
const VISIBILITY_VERTEX_FRAGMENT = 0x1 | 0x2;
|
|
2721
|
+
|
|
2722
|
+
describe('binding 8 = spotShadowMap (D-5 always-on, last view-BG entry)', () => {
|
|
2723
|
+
it('declares binding 8 as a depth 2D texture, FRAGMENT-only', () => {
|
|
2724
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2725
|
+
const b8 = entries.find((e) => e.binding === 8);
|
|
2726
|
+
expect(b8).toBeDefined();
|
|
2727
|
+
if (b8 === undefined) throw new Error('binding 8 missing from view BGL');
|
|
2728
|
+
expect(b8.texture?.sampleType).toBe('depth');
|
|
2729
|
+
expect(b8.texture?.viewDimension).toBe('2d');
|
|
2730
|
+
expect(b8.visibility).toBe(VISIBILITY_FRAGMENT);
|
|
2731
|
+
});
|
|
2732
|
+
|
|
2733
|
+
it('declares binding 8 regardless of storageBuffer caps (always-on, no gate)', () => {
|
|
2734
|
+
const withStorage = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2735
|
+
const noStorage = buildPbrViewBglEntries({ storageBuffer: false });
|
|
2736
|
+
expect(withStorage.some((e) => e.binding === 8)).toBe(true);
|
|
2737
|
+
expect(noStorage.some((e) => e.binding === 8)).toBe(true);
|
|
2738
|
+
});
|
|
2739
|
+
|
|
2740
|
+
it('binding 8 is the last entry (no binding 9)', () => {
|
|
2741
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2742
|
+
expect(entries.some((e) => e.binding === 9)).toBe(false);
|
|
2743
|
+
expect(entries.some((e) => e.binding === 10)).toBe(false);
|
|
2744
|
+
});
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
// feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
|
|
2748
|
+
// the per-spot fragment-read perspective `spotLightViewProj` matrices were
|
|
2749
|
+
// folded out of a standalone @group(0) binding 9 uniform buffer into the View
|
|
2750
|
+
// UBO tail (`view.spotLightViewProj`, bytes 528..784, written by the host
|
|
2751
|
+
// viewPayload). The standalone binding pushed the WebGL2 fallback fragment
|
|
2752
|
+
// uniform-buffer count to 12, over GLES 3.0's
|
|
2753
|
+
// `max_uniform_buffers_per_shader_stage = 11`, crashing pipeline-layout
|
|
2754
|
+
// creation on the compat path (this feat's target). The view BGL therefore
|
|
2755
|
+
// ends at binding 8 — no binding 9 — and the spot matrices ride in the View
|
|
2756
|
+
// UBO (binding 0). This block locks that the spot matrices add ZERO new view
|
|
2757
|
+
// BGL buffer bindings (the WebGL2 budget invariant).
|
|
2758
|
+
describe('spotLightViewProj folded into View UBO (binding 0), not a new binding', () => {
|
|
2759
|
+
it('view BGL declares no uniform buffer beyond binding 7 (only binding 0 = view, 6/7 = point/cascade)', () => {
|
|
2760
|
+
// Enumerate uniform-buffer entries on the view BGL. After the w25 fold,
|
|
2761
|
+
// the only uniform buffers are binding 0 (View UBO, carries the spot
|
|
2762
|
+
// matrices in its tail), binding 6 (point shadowParams) and binding 7
|
|
2763
|
+
// (shadowCasterCascade). No standalone spot-matrix uniform buffer.
|
|
2764
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2765
|
+
const uniformBufferBindings = entries
|
|
2766
|
+
.filter((e) => e.buffer?.type === 'uniform')
|
|
2767
|
+
.map((e) => e.binding)
|
|
2768
|
+
.sort((a, b) => a - b);
|
|
2769
|
+
// storageBuffer=true: bindings 1+2 are read-only-storage (not uniform).
|
|
2770
|
+
expect(uniformBufferBindings).toEqual([0, 6, 7]);
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
it('storageBuffer=false: WebGL2 fallback uniform-buffer bindings stay within budget', () => {
|
|
2774
|
+
// On the WebGL2 fallback path bindings 1+2 (point/spot light arrays)
|
|
2775
|
+
// become uniform buffers. The fragment-stage uniform-buffer count must
|
|
2776
|
+
// stay <= 11 (GLES 3.0 max_uniform_buffers_per_shader_stage). View BGL
|
|
2777
|
+
// fragment uniform buffers after w25: binding 0 (view), 1 (pointLights),
|
|
2778
|
+
// 2 (spotLights), 6 (shadowParams), 7 (shadowCasterCascade) = 5. There is
|
|
2779
|
+
// NO standalone spot-matrix uniform buffer (the 12th that overflowed).
|
|
2780
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: false });
|
|
2781
|
+
const fragmentUniformBuffers = entries.filter(
|
|
2782
|
+
(e) => e.buffer?.type === 'uniform' && (e.visibility & VISIBILITY_FRAGMENT) !== 0,
|
|
2783
|
+
);
|
|
2784
|
+
// 5 view-BG fragment uniform buffers (was 6 before the fold).
|
|
2785
|
+
expect(fragmentUniformBuffers.length).toBe(5);
|
|
2786
|
+
// No binding 9 (the folded-away standalone spot-matrix UBO).
|
|
2787
|
+
expect(entries.some((e) => e.binding === 9)).toBe(false);
|
|
2788
|
+
});
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
describe('sampler reuse (binding 4) — spot adds no new sampler (D-5)', () => {
|
|
2792
|
+
it('keeps binding 4 as the single comparison sampler (shared by directional/point/spot)', () => {
|
|
2793
|
+
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
2794
|
+
const samplers = entries.filter((e) => e.sampler !== undefined);
|
|
2795
|
+
expect(samplers).toHaveLength(1);
|
|
2796
|
+
expect(samplers[0]?.binding).toBe(4);
|
|
2797
|
+
expect(samplers[0]?.sampler?.type).toBe('comparison');
|
|
2798
|
+
});
|
|
2799
|
+
});
|
|
2800
|
+
|
|
2801
|
+
describe('bindings 3/4/5/6/7 unchanged (AC-08 no-regress)', () => {
|
|
2802
|
+
it('binding 3 = directional depth 2D, VERTEX|FRAGMENT', () => {
|
|
2803
|
+
const e = buildPbrViewBglEntries({ storageBuffer: true }).find((x) => x.binding === 3);
|
|
2804
|
+
expect(e?.texture?.sampleType).toBe('depth');
|
|
2805
|
+
expect(e?.texture?.viewDimension).toBe('2d');
|
|
2806
|
+
expect(e?.visibility).toBe(VISIBILITY_VERTEX_FRAGMENT);
|
|
2807
|
+
});
|
|
2808
|
+
|
|
2809
|
+
it('binding 4 = comparison sampler, FRAGMENT', () => {
|
|
2810
|
+
const e = buildPbrViewBglEntries({ storageBuffer: true }).find((x) => x.binding === 4);
|
|
2811
|
+
expect(e?.sampler?.type).toBe('comparison');
|
|
2812
|
+
expect(e?.visibility).toBe(VISIBILITY_FRAGMENT);
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
it('binding 5 = point cube-array depth, FRAGMENT', () => {
|
|
2816
|
+
const e = buildPbrViewBglEntries({ storageBuffer: true }).find((x) => x.binding === 5);
|
|
2817
|
+
expect(e?.texture?.sampleType).toBe('depth');
|
|
2818
|
+
expect(e?.texture?.viewDimension).toBe('cube-array');
|
|
2819
|
+
expect(e?.visibility).toBe(VISIBILITY_FRAGMENT);
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
it('binding 6 = point shadow params UBO, FRAGMENT', () => {
|
|
2823
|
+
const e = buildPbrViewBglEntries({ storageBuffer: true }).find((x) => x.binding === 6);
|
|
2824
|
+
expect(e?.buffer?.type).toBe('uniform');
|
|
2825
|
+
expect(e?.visibility).toBe(VISIBILITY_FRAGMENT);
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
it('binding 7 = shadowCasterCascade UBO, VERTEX|FRAGMENT', () => {
|
|
2829
|
+
const e = buildPbrViewBglEntries({ storageBuffer: true }).find((x) => x.binding === 7);
|
|
2830
|
+
expect(e?.buffer?.type).toBe('uniform');
|
|
2831
|
+
expect(e?.visibility).toBe(VISIBILITY_VERTEX_FRAGMENT);
|
|
2832
|
+
});
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
describe('full binding roster is contiguous 0..8', () => {
|
|
2836
|
+
it('exposes exactly bindings 0..8 with no gaps (binding 9 folded into View UBO, w25)', () => {
|
|
2837
|
+
const bindings = buildPbrViewBglEntries({ storageBuffer: true })
|
|
2838
|
+
.map((e) => e.binding)
|
|
2839
|
+
.sort((a, b) => a - b);
|
|
2840
|
+
expect(bindings).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
|
2841
|
+
});
|
|
2842
|
+
});
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
{
|
|
2847
|
+
// ─── feat-20260625-spot-light-shadow-mapping verify round-1 F-2 ───
|
|
2848
|
+
// SpotLight.validate() shadow-field enforcement, mirroring the
|
|
2849
|
+
// DirectionalLight.validate() block above (P4 cross-light parity).
|
|
2850
|
+
describe('feat-20260625-spot-light-shadow-mapping SpotLight.validate()', () => {
|
|
2851
|
+
describe('SpotLight.validate() shadow field enforcement', () => {
|
|
2852
|
+
it('rejects even pcfKernelSize (2) with ShadowInvalidConfigError', () => {
|
|
2853
|
+
const world = new World();
|
|
2854
|
+
const r = spawnValidatedLight(world, SpotLight, {
|
|
2855
|
+
direction: [0, -1, 0],
|
|
2856
|
+
pcfKernelSize: 2,
|
|
2857
|
+
});
|
|
2858
|
+
expect(r.ok).toBe(false);
|
|
2859
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2860
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2861
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2862
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2863
|
+
expect(err.detail.value).toBe(2);
|
|
2864
|
+
expect(err.hint).toBe('set pcfKernelSize to an odd integer >= 1; got 2');
|
|
2865
|
+
});
|
|
2866
|
+
|
|
2867
|
+
it('rejects pcfKernelSize < 1 (0) with ShadowInvalidConfigError', () => {
|
|
2868
|
+
const world = new World();
|
|
2869
|
+
const r = spawnValidatedLight(world, SpotLight, {
|
|
2870
|
+
direction: [0, -1, 0],
|
|
2871
|
+
pcfKernelSize: 0,
|
|
2872
|
+
});
|
|
2873
|
+
expect(r.ok).toBe(false);
|
|
2874
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2875
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2876
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2877
|
+
expect(err.detail.field).toBe('pcfKernelSize');
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2880
|
+
it('rejects mapSize < 1 with ShadowInvalidConfigError', () => {
|
|
2881
|
+
const world = new World();
|
|
2882
|
+
const r = spawnValidatedLight(world, SpotLight, {
|
|
2883
|
+
direction: [0, -1, 0],
|
|
2884
|
+
mapSize: 0,
|
|
2885
|
+
});
|
|
2886
|
+
expect(r.ok).toBe(false);
|
|
2887
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2888
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2889
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2890
|
+
expect(err.detail.field).toBe('mapSize');
|
|
2891
|
+
});
|
|
2892
|
+
|
|
2893
|
+
it('rejects farPlane <= nearPlane with ShadowInvalidConfigError', () => {
|
|
2894
|
+
const world = new World();
|
|
2895
|
+
const r = spawnValidatedLight(world, SpotLight, {
|
|
2896
|
+
direction: [0, -1, 0],
|
|
2897
|
+
nearPlane: 10,
|
|
2898
|
+
farPlane: 5,
|
|
2899
|
+
});
|
|
2900
|
+
expect(r.ok).toBe(false);
|
|
2901
|
+
if (r.ok) throw new Error('expected spawn to fail validation');
|
|
2902
|
+
const err = r.error as unknown as ShadowInvalidConfigError;
|
|
2903
|
+
expect(err.code).toBe('shadow-invalid-config');
|
|
2904
|
+
expect(err.detail.field).toBe('farPlane');
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
it('accepts valid odd pcfKernelSize (5) + sane planes', () => {
|
|
2908
|
+
const world = new World();
|
|
2909
|
+
const r = world.spawn({
|
|
2910
|
+
component: SpotLight,
|
|
2911
|
+
data: {
|
|
2912
|
+
direction: [0, -1, 0],
|
|
2913
|
+
pcfKernelSize: 5,
|
|
2914
|
+
mapSize: 1024,
|
|
2915
|
+
nearPlane: 0.1,
|
|
2916
|
+
farPlane: 50,
|
|
2917
|
+
},
|
|
2918
|
+
});
|
|
2919
|
+
expect(r.ok).toBe(true);
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
it('skips shadow-field validation when castShadow is false', () => {
|
|
2923
|
+
const world = new World();
|
|
2924
|
+
const r = world.spawn({
|
|
2925
|
+
component: SpotLight,
|
|
2926
|
+
data: {
|
|
2927
|
+
direction: [0, -1, 0],
|
|
2928
|
+
castShadow: false,
|
|
2929
|
+
pcfKernelSize: 2,
|
|
2930
|
+
},
|
|
2931
|
+
});
|
|
2932
|
+
expect(r.ok).toBe(true);
|
|
2933
|
+
});
|
|
2934
|
+
});
|
|
2935
|
+
});
|
|
2936
|
+
}
|