@forgeax/engine-render 0.1.32 → 0.1.34
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/README.md +1374 -142
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/backend-contract.d.ts +2 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
- package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
- package/dist/assembly/canvas-draw-guard.d.ts +3 -0
- package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
- package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
- package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +3 -3
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
- package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
- package/dist/assembly/material-shader-policy.d.ts +16 -2
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
- package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
- package/dist/assembly/render-feature-post-processes.d.ts +6 -0
- package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
- package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-inspection.d.ts +11 -0
- package/dist/assembly/renderer-inspection.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
- package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +2 -2
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/barrel-distortion.d.ts +47 -0
- package/dist/barrel-distortion.d.ts.map +1 -0
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
- package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
- package/dist/chunk-HEB564WF.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
- package/dist/chunk-MKDIT2EM.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
- package/dist/chunk-ODZWR6JQ.mjs +4927 -0
- package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
- package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
- package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
- package/dist/chunk-UI5SFODX.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
- package/dist/chunk-UINHLCZA.mjs.map +1 -0
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
- package/dist/chunk-WEX45HKH.mjs.map +1 -0
- package/dist/cloud/density.d.ts +55 -0
- package/dist/cloud/density.d.ts.map +1 -0
- package/dist/cloud/extract.d.ts +25 -0
- package/dist/cloud/extract.d.ts.map +1 -0
- package/dist/cloud/feature.d.ts +57 -0
- package/dist/cloud/feature.d.ts.map +1 -0
- package/dist/cloud/frame-context.d.ts +8 -0
- package/dist/cloud/frame-context.d.ts.map +1 -0
- package/dist/cloud/inspection.d.ts +59 -0
- package/dist/cloud/inspection.d.ts.map +1 -0
- package/dist/cloud/optics.d.ts +56 -0
- package/dist/cloud/optics.d.ts.map +1 -0
- package/dist/cloud/parameters.d.ts +73 -0
- package/dist/cloud/parameters.d.ts.map +1 -0
- package/dist/cloud/resources.d.ts +60 -0
- package/dist/cloud/resources.d.ts.map +1 -0
- package/dist/cloud/shadow.d.ts +42 -0
- package/dist/cloud/shadow.d.ts.map +1 -0
- package/dist/cloud/temporal.d.ts +74 -0
- package/dist/cloud/temporal.d.ts.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/barrel-distortion.d.ts +13 -0
- package/dist/components/barrel-distortion.d.ts.map +1 -0
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/cloud-layer.d.ts +29 -0
- package/dist/components/cloud-layer.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/instances.d.ts +20 -6
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.d.ts +1 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +9751 -3528
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/dynamic-geometry.d.ts +5 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/cloud.d.ts +59 -0
- package/dist/errors/cloud.d.ts.map +1 -0
- package/dist/errors/render.d.ts +23 -5
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/examples/single-layer-medium-surface.d.ts +47 -0
- package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +12 -1
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/barrel-distortion.d.ts +29 -0
- package/dist/features/barrel-distortion.d.ts.map +1 -0
- package/dist/features/error-reporting.d.ts +17 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +15 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +15 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts +5 -0
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +1 -0
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/features/targets.d.ts +2 -0
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +44 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +15 -7
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-dirty-ranges.d.ts +11 -0
- package/dist/gpu-dirty-ranges.d.ts.map +1 -0
- package/dist/gpu-driven/batch-topology.d.ts +17 -0
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts +6 -0
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster-material.d.ts +178 -0
- package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
- package/dist/gpu-driven/production-raster.d.ts +50 -4
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/resource-allocation.d.ts +31 -0
- package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
- package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
- package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
- package/dist/gpu-driven/view-gpu.d.ts +35 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +11 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +2 -2
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +10 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +31 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +258 -8
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +178 -3
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instance-buffer-cache.d.ts +4 -4
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +49 -94
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +7 -7
- package/dist/pbr-pipeline.d.ts +19 -1
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
- package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
- package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
- package/dist/record/barrel-distortion-frame.d.ts +15 -0
- package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
- package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
- package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
- package/dist/record/fold-buckets.d.ts +95 -0
- package/dist/record/fold-buckets.d.ts.map +1 -0
- package/dist/record/fold-instance-buffer.d.ts +6 -0
- package/dist/record/fold-instance-buffer.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +40 -3
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +14 -62
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-draw-receipts.d.ts +11 -0
- package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
- package/dist/record/gpu-timing.d.ts +8 -0
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +21 -5
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +13 -2
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/motion-blur-receipt.d.ts +11 -0
- package/dist/record/motion-blur-receipt.d.ts.map +1 -0
- package/dist/record/probe-blend-buffer.d.ts +16 -0
- package/dist/record/probe-blend-buffer.d.ts.map +1 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +34 -31
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +2 -0
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +6 -4
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +22 -2
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +11 -3
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/record/volume-params.d.ts +9 -0
- package/dist/record/volume-params.d.ts.map +1 -0
- package/dist/recovery/render-system-candidate.d.ts +10 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts.map +1 -1
- package/dist/render-contract.d.ts +66 -8
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +106 -3
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +3 -2
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +24 -10
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system-presentation.d.ts +23 -0
- package/dist/render-system-presentation.d.ts.map +1 -0
- package/dist/render-system-producer-roots.d.ts +10 -0
- package/dist/render-system-producer-roots.d.ts.map +1 -0
- package/dist/render-system-residency.d.ts +12 -0
- package/dist/render-system-residency.d.ts.map +1 -0
- package/dist/render-system.d.ts +26 -15
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/probe-blend.d.ts +27 -0
- package/dist/scene/probe-blend.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +14 -5
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +9 -0
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/surface/dynamic-input.d.ts +133 -0
- package/dist/surface/dynamic-input.d.ts.map +1 -0
- package/dist/surface/gpu-driven.d.ts +41 -0
- package/dist/surface/gpu-driven.d.ts.map +1 -0
- package/dist/surface/submission-observation.d.ts +136 -0
- package/dist/surface/submission-observation.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/gpu.d.ts +13 -1
- package/dist/temporal/gpu.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/inspection.d.ts.map +1 -1
- package/dist/temporal/standard-scene-data.d.ts.map +1 -1
- package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/transmission/single-layer-medium.d.ts +89 -0
- package/dist/transmission/single-layer-medium.d.ts.map +1 -0
- package/dist/typed-render-graph-primitives.d.ts +24 -9
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +9 -5
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/extract.d.ts +1 -1
- package/dist/volume/extract.d.ts.map +1 -1
- package/dist/volume/inspection.d.ts +2 -0
- package/dist/volume/inspection.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +4 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/resources.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +22 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
- package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
- package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
- package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
- package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
- package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +626 -113
- package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
- package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
- package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
- package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
- package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
- package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
- package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
- package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +210 -9
- package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
- package/src/__tests__/instances-culling.integration.test.ts +10 -36
- package/src/__tests__/instances-store.unit.test.ts +105 -238
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
- package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
- package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
- package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
- package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
- package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
- package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/probe-bindings.unit.test.ts +582 -1
- package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
- package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
- package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
- package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
- package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
- package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
- package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
- package/src/__tests__/resource-allocation.unit.test.ts +123 -0
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
- package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
- package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
- package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
- package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
- package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
- package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
- package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
- package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
- package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
- package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
- package/src/__tests__/temporal-performance.integration.test.ts +3 -3
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/visibility-instances.integration.test.ts +7 -19
- package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
- package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
- package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/backend-contract.ts +30 -0
- package/src/assembly/barrel-distortion-inspection.ts +22 -0
- package/src/assembly/canvas-draw-guard.ts +11 -0
- package/src/assembly/cloud-shadow-entry.ts +22 -0
- package/src/assembly/dynamic-geometry-host.ts +65 -37
- package/src/assembly/dynamic-geometry-runtime.ts +7 -1
- package/src/assembly/factory.ts +2 -30
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/host-contract.ts +3 -3
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/material/assembly.ts +25 -13
- package/src/assembly/material/pipeline-helpers.ts +53 -12
- package/src/assembly/material-shader-policy.ts +55 -0
- package/src/assembly/material-shader-prewarm.ts +21 -10
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/recovery/device-loss-fanout.ts +13 -0
- package/src/assembly/recovery/renderer-recover.ts +41 -18
- package/src/assembly/render-feature-post-processes.ts +54 -0
- package/src/assembly/renderer-facade.ts +6 -1
- package/src/assembly/renderer-feature-inspection.ts +8 -2
- package/src/assembly/renderer-frame-transaction.ts +23 -0
- package/src/assembly/renderer-inspection.ts +28 -0
- package/src/assembly/skin-palette-owner.ts +1 -1
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-pbr-ready.ts +9 -1
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready-per-pass.ts +104 -0
- package/src/assembly/webgpu-ready.ts +296 -306
- package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
- package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
- package/src/assembly/webgpu-renderer.ts +228 -281
- package/src/assembly/webgpu-vertex-layouts.ts +1 -1
- package/src/barrel-distortion.ts +247 -0
- package/src/bloom-admission.ts +21 -0
- package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
- package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
- package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
- package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
- package/src/cloud/density.ts +600 -0
- package/src/cloud/extract.ts +110 -0
- package/src/cloud/feature.ts +1810 -0
- package/src/cloud/frame-context.ts +42 -0
- package/src/cloud/inspection.ts +130 -0
- package/src/cloud/optics.ts +285 -0
- package/src/cloud/parameters.ts +255 -0
- package/src/cloud/resources.ts +122 -0
- package/src/cloud/shadow.ts +167 -0
- package/src/cloud/temporal.ts +226 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/barrel-distortion.ts +46 -0
- package/src/components/camera.ts +66 -10
- package/src/components/cloud-layer.ts +47 -0
- package/src/components/index.ts +2 -0
- package/src/components/instances.ts +18 -45
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/components/sprite-instances.ts +1 -1
- package/src/construct-renderer.ts +1 -1
- package/src/device/gpu-residency.ts +56 -12
- package/src/dynamic-geometry.ts +12 -1
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/cloud.ts +103 -0
- package/src/errors/render.ts +58 -8
- package/src/examples/single-layer-medium-surface.ts +59 -0
- package/src/extract/camera.ts +35 -8
- package/src/extract/contracts.ts +12 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/barrel-distortion.ts +172 -0
- package/src/features/error-reporting.ts +98 -0
- package/src/features/host.ts +138 -24
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +33 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +11 -0
- package/src/features/render-graph-raster.ts +45 -3
- package/src/features/targets.ts +66 -0
- package/src/features/types.ts +47 -0
- package/src/fullscreen-post-process-pass.ts +41 -9
- package/src/gpu-dirty-ranges.ts +48 -0
- package/src/gpu-driven/batch-topology.ts +53 -0
- package/src/gpu-driven/material-artifacts.ts +35 -32
- package/src/gpu-driven/prepared-draw.ts +27 -14
- package/src/gpu-driven/production-raster-material.ts +1894 -0
- package/src/gpu-driven/production-raster.ts +904 -787
- package/src/gpu-driven/resource-allocation.ts +142 -0
- package/src/gpu-driven/shadow-ownership.ts +71 -0
- package/src/gpu-driven/view-gpu.ts +181 -10
- package/src/gpu-scene.ts +259 -101
- package/src/hdrp-buffers.ts +4 -3
- package/src/ibl/skylight-bind-group.ts +49 -7
- package/src/index.ts +228 -9
- package/src/inspection-types.ts +222 -2
- package/src/instance-buffer-cache.ts +4 -4
- package/src/instances.ts +151 -492
- package/src/pbr-pipeline.ts +219 -13
- package/src/pipeline/single-layer-medium-passes.ts +267 -0
- package/src/pipeline/standard-forward-lane.ts +313 -57
- package/src/pipeline/standard-output/graph.ts +8 -2
- package/src/pipeline/standard-output/types.ts +12 -1
- package/src/pipeline/standard-pipeline.ts +291 -18
- package/src/pipeline/standard-post.ts +397 -102
- package/src/pipeline-builder.ts +1 -1
- package/src/pipeline-spec.ts +5 -5
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/prepare/prepared-graphics-resolver.ts +2 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
- package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
- package/src/record/barrel-distortion-frame.ts +110 -0
- package/src/record/dynamic-geometry-consumption.ts +39 -9
- package/src/record/fold-buckets.ts +852 -0
- package/src/record/fold-instance-buffer.ts +51 -0
- package/src/record/frame-lighting.ts +36 -2
- package/src/record/frame-snapshot.ts +44 -4
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +373 -1099
- package/src/record/gpu-draw-receipts.ts +38 -0
- package/src/record/gpu-timing.ts +48 -5
- package/src/record/main-pass-geometry.ts +281 -83
- package/src/record/main-pass-material.ts +66 -4
- package/src/record/main-pass-sprite-draws.ts +13 -66
- package/src/record/main-pass.ts +156 -20
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/motion-blur-receipt.ts +91 -0
- package/src/record/probe-blend-buffer.ts +180 -0
- package/src/record/recovery-pipeline.ts +35 -3
- package/src/record/render-context.ts +51 -31
- package/src/record/shadow-pass.ts +36 -2
- package/src/record/skybox-post-pass.ts +150 -200
- package/src/record/typed-frame-graph.ts +824 -44
- package/src/record/view-ubo.ts +26 -3
- package/src/record/volume-params.ts +225 -0
- package/src/recovery/render-system-candidate.ts +257 -52
- package/src/reflection/record-owner.ts +7 -3
- package/src/render-contract.ts +71 -8
- package/src/render-graph-primitives.ts +7 -1
- package/src/render-pipeline.ts +136 -1
- package/src/render-system-extract-tail.ts +72 -60
- package/src/render-system-extract.ts +117 -87
- package/src/render-system-presentation.ts +56 -0
- package/src/render-system-producer-roots.ts +96 -0
- package/src/render-system-residency.ts +159 -0
- package/src/render-system.ts +601 -482
- package/src/scene/probe-blend.ts +96 -4
- package/src/scene/render-scene.ts +431 -281
- package/src/ssao-buffers.ts +95 -2
- package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
- package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
- package/src/surface/dynamic-input.ts +658 -0
- package/src/surface/gpu-driven.ts +147 -0
- package/src/surface/submission-observation.ts +517 -0
- package/src/systems/skin-palette-allocator.ts +4 -1
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/gpu.ts +101 -17
- package/src/temporal/inspection.ts +7 -1
- package/src/temporal/standard-scene-data.ts +5 -0
- package/src/temporal/target-coverage-attachment.ts +4 -0
- package/src/temporal/view.ts +13 -2
- package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
- package/src/transmission/single-layer-medium.ts +322 -0
- package/src/typed-render-graph-primitives.ts +191 -50
- package/src/volume/component.ts +10 -10
- package/src/volume/extract.ts +11 -5
- package/src/volume/inspection.ts +3 -0
- package/src/volume/passes.ts +95 -8
- package/src/volume/resources.ts +2 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
- package/dist/chunk-LULCA3OF.mjs +0 -1713
- package/dist/chunk-LULCA3OF.mjs.map +0 -1
- package/dist/chunk-PA5DRJNI.mjs.map +0 -1
|
@@ -0,0 +1,4927 @@
|
|
|
1
|
+
import { CLOUD_EXTINCTION_COEFFICIENT, CLOUD_QUALITY_PROFILES, cloudLayerFormationKey, CloudLayerCacheInvalidError, cloudShadowResolutionForQuality, PointShapeValue, pointShapeFromU32, cloudViewDistanceForQuality } from './chunk-UINHLCZA.mjs';
|
|
2
|
+
import { PointsLinesBudgetExceededError, createVisibilityBudget, PointsLinesStyleUnsupportedError, PointsLinesMaterialUnsupportedError, PointsLinesInvalidStyleError, PointsLinesTopologyMismatchError } from './chunk-WEX45HKH.mjs';
|
|
3
|
+
import { ok, err, deriveMaterialDynamicInputLayout } from '@forgeax/engine-types';
|
|
4
|
+
import { validateR32FloatReceipt } from '@forgeax/engine-rhi';
|
|
5
|
+
|
|
6
|
+
var BARREL_DISTORTION_POST_PROCESS_ID = "fullscreen.forgeax.barrel-distortion";
|
|
7
|
+
var BARREL_DISTORTION_FEATURE_IDENTITY = BARREL_DISTORTION_POST_PROCESS_ID;
|
|
8
|
+
var BARREL_DISTORTION_WGSL_COORDINATE = (
|
|
9
|
+
/* wgsl */
|
|
10
|
+
`
|
|
11
|
+
struct BarrelDistortionParams {
|
|
12
|
+
strength : f32,
|
|
13
|
+
centerX : f32,
|
|
14
|
+
centerY : f32,
|
|
15
|
+
radiusSquared : f32,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
fn barrel_distortion_display_to_scene_uv(
|
|
19
|
+
inputUv : vec2<f32>,
|
|
20
|
+
params : BarrelDistortionParams,
|
|
21
|
+
dimensions : vec2<f32>,
|
|
22
|
+
) -> vec2<f32> {
|
|
23
|
+
let aspect = dimensions.x / max(dimensions.y, 1.0);
|
|
24
|
+
let p = vec2<f32>(
|
|
25
|
+
2.0 * aspect * (inputUv.x - params.centerX),
|
|
26
|
+
2.0 * (inputUv.y - params.centerY),
|
|
27
|
+
);
|
|
28
|
+
let t = dot(p, p) / max(params.radiusSquared, 1e-6);
|
|
29
|
+
let scale = (1.0 - params.strength) / (1.0 - params.strength * t);
|
|
30
|
+
return clamp(
|
|
31
|
+
vec2<f32>(
|
|
32
|
+
params.centerX + (inputUv.x - params.centerX) * scale,
|
|
33
|
+
params.centerY + (inputUv.y - params.centerY) * scale,
|
|
34
|
+
),
|
|
35
|
+
vec2<f32>(0.0),
|
|
36
|
+
vec2<f32>(1.0),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
`
|
|
40
|
+
);
|
|
41
|
+
var BARREL_DISTORTION_WGSL = (
|
|
42
|
+
/* wgsl */
|
|
43
|
+
`${BARREL_DISTORTION_WGSL_COORDINATE}
|
|
44
|
+
struct FullscreenOutput {
|
|
45
|
+
@builtin(position) position : vec4<f32>,
|
|
46
|
+
@location(0) uv : vec2<f32>,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
fn fullscreen_triangle(vertex_index : u32) -> FullscreenOutput {
|
|
50
|
+
var x : f32 = -1.0;
|
|
51
|
+
var y : f32 = -1.0;
|
|
52
|
+
if (vertex_index == 1u) { x = 3.0; }
|
|
53
|
+
if (vertex_index == 2u) { y = 3.0; }
|
|
54
|
+
let u : f32 = (x + 1.0) * 0.5;
|
|
55
|
+
let v : f32 = 1.0 - (y + 1.0) * 0.5;
|
|
56
|
+
var out : FullscreenOutput;
|
|
57
|
+
out.position = vec4<f32>(x, y, 0.0, 1.0);
|
|
58
|
+
out.uv = vec2<f32>(u, v);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@group(1) @binding(0) var sceneColor : texture_2d<f32>;
|
|
63
|
+
@group(1) @binding(1) var sceneSampler : sampler;
|
|
64
|
+
|
|
65
|
+
@group(1) @binding(2) var<uniform> params : BarrelDistortionParams;
|
|
66
|
+
|
|
67
|
+
@vertex
|
|
68
|
+
fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
|
|
69
|
+
return fullscreen_triangle(vertex_index);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@fragment
|
|
73
|
+
fn fs_main(input : FullscreenOutput) -> @location(0) vec4<f32> {
|
|
74
|
+
let dimensions = vec2<f32>(textureDimensions(sceneColor));
|
|
75
|
+
let sampleUv = barrel_distortion_display_to_scene_uv(input.uv, params, dimensions);
|
|
76
|
+
return textureSampleLevel(sceneColor, sceneSampler, sampleUv, 0.0);
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
);
|
|
80
|
+
function createBarrelDistortionRenderFeature() {
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
identity: BARREL_DISTORTION_FEATURE_IDENTITY,
|
|
83
|
+
requiredCapabilities: ["rgba16floatRenderable"],
|
|
84
|
+
requiredFullscreenPostProcesses: [
|
|
85
|
+
{ identity: BARREL_DISTORTION_POST_PROCESS_ID, source: BARREL_DISTORTION_WGSL }
|
|
86
|
+
],
|
|
87
|
+
extract: (context) => ok({ active: (context.selectedCamera?.barrelDistortion?.strength ?? 0) > 0 }),
|
|
88
|
+
plan: (data, _context) => ok({
|
|
89
|
+
resources: data.active ? [
|
|
90
|
+
{
|
|
91
|
+
kind: "fullscreen-program",
|
|
92
|
+
name: BARREL_DISTORTION_POST_PROCESS_ID,
|
|
93
|
+
source: BARREL_DISTORTION_WGSL,
|
|
94
|
+
reads: ["ldrColor"],
|
|
95
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) }
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
kind: "graphics-program",
|
|
99
|
+
name: "barrel-distortion-pipeline",
|
|
100
|
+
program: {
|
|
101
|
+
shader: BARREL_DISTORTION_POST_PROCESS_ID,
|
|
102
|
+
vertexLayout: "none",
|
|
103
|
+
colorFormats: ["rgba16float"],
|
|
104
|
+
sampleCount: 1
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: "graphics-bindings",
|
|
109
|
+
name: "barrel-distortion-bindings",
|
|
110
|
+
program: "barrel-distortion-pipeline",
|
|
111
|
+
values: {
|
|
112
|
+
group: 1,
|
|
113
|
+
fullscreen: true,
|
|
114
|
+
shader: BARREL_DISTORTION_POST_PROCESS_ID,
|
|
115
|
+
input: "barrel-input"
|
|
116
|
+
},
|
|
117
|
+
logicalTargets: { input: "barrel-input" }
|
|
118
|
+
}
|
|
119
|
+
] : [],
|
|
120
|
+
passes: data.active ? [
|
|
121
|
+
{
|
|
122
|
+
kind: "raster",
|
|
123
|
+
name: "barrel-distortion",
|
|
124
|
+
colorAttachments: [
|
|
125
|
+
{ target: "barrel-output", loadOp: "clear", storeOp: "store" }
|
|
126
|
+
],
|
|
127
|
+
sampledTargets: ["barrel-input"],
|
|
128
|
+
draws: [
|
|
129
|
+
{
|
|
130
|
+
program: "barrel-distortion-pipeline",
|
|
131
|
+
bindings: ["barrel-distortion-bindings"],
|
|
132
|
+
vertexData: [],
|
|
133
|
+
vertexLayout: "none",
|
|
134
|
+
draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
] : []
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
var CLOUD_SHAPE_CELLS = 4;
|
|
143
|
+
var CLOUD_WEATHER_CELLS = 2;
|
|
144
|
+
var CLOUD_DETAIL_CELLS = CLOUD_SHAPE_CELLS * 3;
|
|
145
|
+
var CLOUD_VERTICAL_CELLS = 1;
|
|
146
|
+
var CLOUD_VERTICAL_NOISE_CELLS = 2;
|
|
147
|
+
var CLOUD_DETAIL_VERTICAL_CELLS = 3;
|
|
148
|
+
function saturate(value) {
|
|
149
|
+
return Math.min(1, Math.max(0, value));
|
|
150
|
+
}
|
|
151
|
+
function smoothstep(edge0, edge1, value) {
|
|
152
|
+
const t = saturate((value - edge0) / Math.max(1e-6, edge1 - edge0));
|
|
153
|
+
return t * t * (3 - 2 * t);
|
|
154
|
+
}
|
|
155
|
+
function fract(value) {
|
|
156
|
+
return value - Math.floor(value);
|
|
157
|
+
}
|
|
158
|
+
function wrapInteger(value, period) {
|
|
159
|
+
const wrapped2 = value % period;
|
|
160
|
+
return wrapped2 < 0 ? wrapped2 + period : wrapped2;
|
|
161
|
+
}
|
|
162
|
+
function hash3(x, y, z, seed) {
|
|
163
|
+
let value = Math.imul(x | 0, 73244475);
|
|
164
|
+
value = Math.imul(value ^ Math.imul(y | 0, 295559667), 73244475);
|
|
165
|
+
value = Math.imul(value ^ Math.imul(z | 0, 214175), 73244475);
|
|
166
|
+
value ^= seed | 0;
|
|
167
|
+
value = Math.imul(value ^ value >>> 16, 73244475);
|
|
168
|
+
value = Math.imul(value ^ value >>> 13, 668265261);
|
|
169
|
+
return ((value ^ value >>> 16) >>> 0) / 4294967296;
|
|
170
|
+
}
|
|
171
|
+
function valueNoise(x, y, z, seed, periodX = 0, periodY = 0, periodZ = 0) {
|
|
172
|
+
const ix = Math.floor(x);
|
|
173
|
+
const iy = Math.floor(y);
|
|
174
|
+
const iz = Math.floor(z);
|
|
175
|
+
const fx = x - ix;
|
|
176
|
+
const fy = y - iy;
|
|
177
|
+
const fz = z - iz;
|
|
178
|
+
const ux = fx * fx * (3 - 2 * fx);
|
|
179
|
+
const uy = fy * fy * (3 - 2 * fy);
|
|
180
|
+
const uz = fz * fz * (3 - 2 * fz);
|
|
181
|
+
const lattice = (cellX, cellY, cellZ) => hash3(
|
|
182
|
+
periodX > 0 ? wrapInteger(cellX, periodX) : cellX,
|
|
183
|
+
periodY > 0 ? wrapInteger(cellY, periodY) : cellY,
|
|
184
|
+
periodZ > 0 ? wrapInteger(cellZ, periodZ) : cellZ,
|
|
185
|
+
seed
|
|
186
|
+
);
|
|
187
|
+
const c000 = lattice(ix, iy, iz);
|
|
188
|
+
const c100 = lattice(ix + 1, iy, iz);
|
|
189
|
+
const c010 = lattice(ix, iy + 1, iz);
|
|
190
|
+
const c110 = lattice(ix + 1, iy + 1, iz);
|
|
191
|
+
const c001 = lattice(ix, iy, iz + 1);
|
|
192
|
+
const c101 = lattice(ix + 1, iy, iz + 1);
|
|
193
|
+
const c011 = lattice(ix, iy + 1, iz + 1);
|
|
194
|
+
const c111 = lattice(ix + 1, iy + 1, iz + 1);
|
|
195
|
+
const x00 = c000 + (c100 - c000) * ux;
|
|
196
|
+
const x10 = c010 + (c110 - c010) * ux;
|
|
197
|
+
const x01 = c001 + (c101 - c001) * ux;
|
|
198
|
+
const x11 = c011 + (c111 - c011) * ux;
|
|
199
|
+
return x00 + (x10 - x00) * uy + (x01 + (x11 - x01) * uy - (x00 + (x10 - x00) * uy)) * uz;
|
|
200
|
+
}
|
|
201
|
+
function cellularNoise(x, y, z, seed, periodXZ = CLOUD_DETAIL_CELLS, periodY = 0) {
|
|
202
|
+
const ix = Math.floor(x);
|
|
203
|
+
const iy = Math.floor(y);
|
|
204
|
+
const iz = Math.floor(z);
|
|
205
|
+
const fx = x - ix;
|
|
206
|
+
const fy = y - iy;
|
|
207
|
+
const fz = z - iz;
|
|
208
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
209
|
+
for (let dz = -1; dz <= 1; dz += 1) {
|
|
210
|
+
for (let dy = -1; dy <= 1; dy += 1) {
|
|
211
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
212
|
+
const random = hash3(
|
|
213
|
+
wrapInteger(ix + dx, periodXZ),
|
|
214
|
+
periodY > 0 ? wrapInteger(iy + dy, periodY) : iy + dy,
|
|
215
|
+
wrapInteger(iz + dz, periodXZ),
|
|
216
|
+
seed ^ 2654435769
|
|
217
|
+
);
|
|
218
|
+
const px = dx + fract(random * 17) - fx;
|
|
219
|
+
const py = dy + fract(random * 31) - fy;
|
|
220
|
+
const pz = dz + fract(random * 47) - fz;
|
|
221
|
+
nearest = Math.min(nearest, px * px + py * py + pz * pz);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return 1 - saturate(Math.sqrt(nearest) * 1.25);
|
|
226
|
+
}
|
|
227
|
+
function heightEnvelope(height, body, weather) {
|
|
228
|
+
const lowerEdge = 0.05 + (1 - body) * 0.1;
|
|
229
|
+
const crownEdge = Math.min(0.96, 0.6 + body * 0.3 + weather * 0.08);
|
|
230
|
+
return smoothstep(lowerEdge, Math.min(1, lowerEdge + 0.12), height) * (1 - smoothstep(crownEdge, Math.min(1, crownEdge + 0.14), height));
|
|
231
|
+
}
|
|
232
|
+
function composeCloudDensity(height, formation, coverage) {
|
|
233
|
+
const threshold = 1 - coverage * (0.45 + formation.weather * 0.55);
|
|
234
|
+
const covered = smoothstep(0, 0.4, (formation.body - threshold) / Math.max(1e-3, 1 - threshold));
|
|
235
|
+
const shaped = covered * heightEnvelope(height, formation.body, formation.weather);
|
|
236
|
+
const erosion = (1 - formation.erosion) * 0.18;
|
|
237
|
+
return saturate((shaped - erosion) / (1 - erosion));
|
|
238
|
+
}
|
|
239
|
+
function formationField(x, y, z, seed, detailRepeat = 1) {
|
|
240
|
+
const macroWarpX = valueNoise(
|
|
241
|
+
x * CLOUD_WEATHER_CELLS,
|
|
242
|
+
y * CLOUD_VERTICAL_NOISE_CELLS + 3,
|
|
243
|
+
z * CLOUD_WEATHER_CELLS,
|
|
244
|
+
seed + 41023,
|
|
245
|
+
CLOUD_WEATHER_CELLS,
|
|
246
|
+
2,
|
|
247
|
+
CLOUD_WEATHER_CELLS
|
|
248
|
+
) - 0.5;
|
|
249
|
+
const macroWarpZ = valueNoise(
|
|
250
|
+
x * CLOUD_WEATHER_CELLS + 5,
|
|
251
|
+
y * CLOUD_VERTICAL_NOISE_CELLS - 7,
|
|
252
|
+
z * CLOUD_WEATHER_CELLS,
|
|
253
|
+
seed + 41023,
|
|
254
|
+
CLOUD_WEATHER_CELLS,
|
|
255
|
+
2,
|
|
256
|
+
CLOUD_WEATHER_CELLS
|
|
257
|
+
) - 0.5;
|
|
258
|
+
const bodyX = x + macroWarpX * 0.3;
|
|
259
|
+
const bodyZ = z + macroWarpZ * 0.3;
|
|
260
|
+
let amplitude = 0.5;
|
|
261
|
+
let frequency = 1;
|
|
262
|
+
let sum = 0;
|
|
263
|
+
let weight = 0;
|
|
264
|
+
for (let octave = 0; octave < 4; octave += 1) {
|
|
265
|
+
const tilePeriod = CLOUD_SHAPE_CELLS * frequency;
|
|
266
|
+
sum += valueNoise(
|
|
267
|
+
bodyX * tilePeriod,
|
|
268
|
+
y * frequency * CLOUD_VERTICAL_NOISE_CELLS,
|
|
269
|
+
bodyZ * tilePeriod,
|
|
270
|
+
seed + octave * 1013,
|
|
271
|
+
tilePeriod,
|
|
272
|
+
frequency * CLOUD_VERTICAL_NOISE_CELLS,
|
|
273
|
+
tilePeriod
|
|
274
|
+
) * amplitude;
|
|
275
|
+
weight += amplitude;
|
|
276
|
+
amplitude *= 0.4;
|
|
277
|
+
frequency *= 2;
|
|
278
|
+
}
|
|
279
|
+
const broad = sum / weight;
|
|
280
|
+
const cells = cellularNoise(
|
|
281
|
+
bodyX * CLOUD_SHAPE_CELLS,
|
|
282
|
+
y * CLOUD_VERTICAL_NOISE_CELLS,
|
|
283
|
+
bodyZ * CLOUD_SHAPE_CELLS,
|
|
284
|
+
seed + 5011,
|
|
285
|
+
CLOUD_SHAPE_CELLS,
|
|
286
|
+
CLOUD_VERTICAL_NOISE_CELLS
|
|
287
|
+
);
|
|
288
|
+
const body = saturate(broad * 0.85 + cells * 0.15 + 0.15);
|
|
289
|
+
const weather = smoothstep(
|
|
290
|
+
0.34,
|
|
291
|
+
0.66,
|
|
292
|
+
valueNoise(
|
|
293
|
+
x * CLOUD_WEATHER_CELLS,
|
|
294
|
+
0.37,
|
|
295
|
+
z * CLOUD_WEATHER_CELLS,
|
|
296
|
+
seed + 17041,
|
|
297
|
+
CLOUD_WEATHER_CELLS,
|
|
298
|
+
1,
|
|
299
|
+
CLOUD_WEATHER_CELLS
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
const detailX = x * detailRepeat;
|
|
303
|
+
const detailY = y * detailRepeat;
|
|
304
|
+
const detailZ = z * detailRepeat;
|
|
305
|
+
const warpX = valueNoise(
|
|
306
|
+
detailX * CLOUD_WEATHER_CELLS,
|
|
307
|
+
detailY * 2 + 11,
|
|
308
|
+
detailZ * CLOUD_WEATHER_CELLS,
|
|
309
|
+
seed + 29011,
|
|
310
|
+
CLOUD_WEATHER_CELLS,
|
|
311
|
+
2,
|
|
312
|
+
CLOUD_WEATHER_CELLS
|
|
313
|
+
) - 0.5;
|
|
314
|
+
const warpZ = valueNoise(
|
|
315
|
+
detailX * CLOUD_WEATHER_CELLS + 7,
|
|
316
|
+
detailY * 2 - 5,
|
|
317
|
+
detailZ * CLOUD_WEATHER_CELLS,
|
|
318
|
+
seed + 29011,
|
|
319
|
+
CLOUD_WEATHER_CELLS,
|
|
320
|
+
2,
|
|
321
|
+
CLOUD_WEATHER_CELLS
|
|
322
|
+
) - 0.5;
|
|
323
|
+
const erosion = cellularNoise(
|
|
324
|
+
(detailX + warpX * 0.22) * CLOUD_DETAIL_CELLS,
|
|
325
|
+
detailY * CLOUD_DETAIL_VERTICAL_CELLS,
|
|
326
|
+
(detailZ + warpZ * 0.22) * CLOUD_DETAIL_CELLS,
|
|
327
|
+
seed + 7919,
|
|
328
|
+
CLOUD_DETAIL_CELLS,
|
|
329
|
+
CLOUD_DETAIL_VERTICAL_CELLS
|
|
330
|
+
);
|
|
331
|
+
const noise = body;
|
|
332
|
+
return { weather, body, erosion, noise };
|
|
333
|
+
}
|
|
334
|
+
function evaluateCloudFormation(params, position, timeSeconds = 0) {
|
|
335
|
+
const windX = (params.wind[0] ?? 0) * timeSeconds;
|
|
336
|
+
const windY = (params.wind[1] ?? 0) * timeSeconds;
|
|
337
|
+
const windZ = (params.wind[2] ?? 0) * timeSeconds;
|
|
338
|
+
const relativeHeight = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
|
|
339
|
+
const advectedHeight = relativeHeight + windY * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
|
|
340
|
+
return formationField(
|
|
341
|
+
((position[0] ?? 0) + windX) * params.scale,
|
|
342
|
+
fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
|
|
343
|
+
((position[2] ?? 0) + windZ) * params.scale,
|
|
344
|
+
params.seed
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
function evaluateCloudDensity(params, position, timeSeconds = 0) {
|
|
348
|
+
const relativeHeight = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
|
|
349
|
+
if (relativeHeight <= 0 || relativeHeight >= 1 || params.density <= 0 || params.coverage <= 0) {
|
|
350
|
+
return { density: 0, heightFraction: saturate(relativeHeight), noise: 0 };
|
|
351
|
+
}
|
|
352
|
+
const windX = (params.wind[0] ?? 0) * timeSeconds;
|
|
353
|
+
const windY = (params.wind[1] ?? 0) * timeSeconds;
|
|
354
|
+
const windZ = (params.wind[2] ?? 0) * timeSeconds;
|
|
355
|
+
const x = ((position[0] ?? 0) + windX) * params.scale;
|
|
356
|
+
const advectedHeight = relativeHeight + windY * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
|
|
357
|
+
const y = fract(advectedHeight) * CLOUD_VERTICAL_CELLS;
|
|
358
|
+
const z = ((position[2] ?? 0) + windZ) * params.scale;
|
|
359
|
+
const formation = formationField(x, y, z, params.seed, 3);
|
|
360
|
+
const noise = formation.noise;
|
|
361
|
+
return {
|
|
362
|
+
density: composeCloudDensity(relativeHeight, formation, params.coverage) * params.density,
|
|
363
|
+
heightFraction: relativeHeight,
|
|
364
|
+
noise,
|
|
365
|
+
weather: formation.weather,
|
|
366
|
+
body: formation.body,
|
|
367
|
+
erosion: formation.erosion
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function cacheIndex(resolution, x, y, z) {
|
|
371
|
+
return z * resolution * resolution + y * resolution + x;
|
|
372
|
+
}
|
|
373
|
+
function digestBytes(data) {
|
|
374
|
+
let hash = 2166136261;
|
|
375
|
+
for (const byte of data) {
|
|
376
|
+
hash ^= byte;
|
|
377
|
+
hash = Math.imul(hash, 16777619);
|
|
378
|
+
}
|
|
379
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
380
|
+
}
|
|
381
|
+
function buildCloudDensityCache(params, profile = CLOUD_QUALITY_PROFILES[params.quality]) {
|
|
382
|
+
const resolution = Math.max(4, Math.floor(profile.cacheResolution));
|
|
383
|
+
const data = new Uint8Array(resolution * resolution * resolution);
|
|
384
|
+
const formationData = new Uint8Array(data.byteLength * 3);
|
|
385
|
+
const position = [0, 0, 0];
|
|
386
|
+
for (let z = 0; z < resolution; z += 1) {
|
|
387
|
+
for (let y = 0; y < resolution; y += 1) {
|
|
388
|
+
position[1] = params.baseHeight + (y + 0.5) / resolution * params.thickness;
|
|
389
|
+
for (let x = 0; x < resolution; x += 1) {
|
|
390
|
+
position[0] = (x + 0.5) / resolution / params.scale;
|
|
391
|
+
position[2] = (z + 0.5) / resolution / params.scale;
|
|
392
|
+
const index = cacheIndex(resolution, x, y, z);
|
|
393
|
+
const formation = evaluateCloudFormation(params, position, 0);
|
|
394
|
+
formationData[index] = Math.round(saturate(formation.weather) * 255);
|
|
395
|
+
formationData[data.byteLength + index] = Math.round(saturate(formation.body) * 255);
|
|
396
|
+
formationData[data.byteLength * 2 + index] = Math.round(saturate(formation.erosion) * 255);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const cache = {
|
|
401
|
+
sourceKey: cloudLayerFormationKey(params),
|
|
402
|
+
resolution,
|
|
403
|
+
data,
|
|
404
|
+
formationData,
|
|
405
|
+
formationByteLength: formationData.byteLength,
|
|
406
|
+
byteLength: formationData.byteLength,
|
|
407
|
+
digest: digestBytes(formationData)
|
|
408
|
+
};
|
|
409
|
+
for (let z = 0; z < resolution; z++) {
|
|
410
|
+
for (let y = 0; y < resolution; y++) {
|
|
411
|
+
for (let x = 0; x < resolution; x++) {
|
|
412
|
+
position[0] = (x + 0.5) / resolution / params.scale;
|
|
413
|
+
position[1] = params.baseHeight + (y + 0.5) / resolution * params.thickness;
|
|
414
|
+
position[2] = (z + 0.5) / resolution / params.scale;
|
|
415
|
+
data[cacheIndex(resolution, x, y, z)] = Math.round(
|
|
416
|
+
saturate(sampleCloudDensity(cache, params, position)) * 255
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze(cache);
|
|
422
|
+
}
|
|
423
|
+
function snapshotCloudDensityCache(cache) {
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
sourceKey: cache.sourceKey,
|
|
426
|
+
resolution: cache.resolution,
|
|
427
|
+
data: new Uint8Array(cache.data),
|
|
428
|
+
formationData: new Uint8Array(cache.formationData),
|
|
429
|
+
digest: cache.digest
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function reconstructCloudDensityCache(params, snapshot) {
|
|
433
|
+
const expectedKey = cloudLayerFormationKey(params);
|
|
434
|
+
if (snapshot.sourceKey !== expectedKey) {
|
|
435
|
+
return err(new CloudLayerCacheInvalidError(snapshot.sourceKey, "source key mismatch"));
|
|
436
|
+
}
|
|
437
|
+
if (!Number.isInteger(snapshot.resolution) || snapshot.resolution < 4 || snapshot.resolution > 256 || snapshot.data.byteLength !== snapshot.resolution ** 3 || snapshot.formationData !== void 0 && snapshot.formationData.byteLength !== snapshot.resolution ** 3 * 3) {
|
|
438
|
+
return err(
|
|
439
|
+
new CloudLayerCacheInvalidError(snapshot.sourceKey, "resolution and payload length mismatch")
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
const formationData = snapshot.formationData === void 0 ? new Uint8Array(snapshot.data.length * 3).fill(0) : new Uint8Array(snapshot.formationData);
|
|
443
|
+
const digest = digestBytes(formationData);
|
|
444
|
+
if (digest !== snapshot.digest) {
|
|
445
|
+
return err(new CloudLayerCacheInvalidError(snapshot.sourceKey, "payload digest mismatch"));
|
|
446
|
+
}
|
|
447
|
+
const data = new Uint8Array(snapshot.data);
|
|
448
|
+
return ok(
|
|
449
|
+
Object.freeze({
|
|
450
|
+
sourceKey: snapshot.sourceKey,
|
|
451
|
+
resolution: snapshot.resolution,
|
|
452
|
+
data,
|
|
453
|
+
formationData,
|
|
454
|
+
formationByteLength: formationData.byteLength,
|
|
455
|
+
byteLength: formationData.byteLength,
|
|
456
|
+
digest
|
|
457
|
+
})
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
function wrapped(value, resolution) {
|
|
461
|
+
const result = value - Math.floor(value);
|
|
462
|
+
return Math.min(resolution - 1e-6, Math.max(0, result * resolution));
|
|
463
|
+
}
|
|
464
|
+
function sampleCloudDensity(cache, params, position, timeSeconds = 0) {
|
|
465
|
+
const height = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
|
|
466
|
+
if (height <= 0 || height >= 1 || params.coverage <= 0 || params.density <= 0) return 0;
|
|
467
|
+
const period = 1 / Math.max(1e-6, params.scale);
|
|
468
|
+
const x = wrapped(
|
|
469
|
+
((position[0] ?? 0) + (params.wind[0] ?? 0) * timeSeconds) / period - 0.5 / cache.resolution,
|
|
470
|
+
cache.resolution
|
|
471
|
+
);
|
|
472
|
+
const z = wrapped(
|
|
473
|
+
((position[2] ?? 0) + (params.wind[2] ?? 0) * timeSeconds) / period - 0.5 / cache.resolution,
|
|
474
|
+
cache.resolution
|
|
475
|
+
);
|
|
476
|
+
const advectedHeight = height + (params.wind[1] ?? 0) * timeSeconds * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
|
|
477
|
+
const y = wrapped(advectedHeight - 0.5 / cache.resolution, cache.resolution);
|
|
478
|
+
const x0 = Math.floor(x);
|
|
479
|
+
const y0 = Math.floor(y);
|
|
480
|
+
const z0 = Math.floor(z);
|
|
481
|
+
const x1 = (x0 + 1) % cache.resolution;
|
|
482
|
+
const y1 = (y0 + 1) % cache.resolution;
|
|
483
|
+
const z1 = (z0 + 1) % cache.resolution;
|
|
484
|
+
const fx = x - x0;
|
|
485
|
+
const fy = y - y0;
|
|
486
|
+
const fz = z - z0;
|
|
487
|
+
const stride = cache.resolution ** 3;
|
|
488
|
+
if (cache.formationData === void 0 || cache.formationData.byteLength < stride * 3) {
|
|
489
|
+
const sampleLegacy = (sx, sy, sz) => (cache.data[cacheIndex(cache.resolution, sx, sy, sz)] ?? 0) / 255;
|
|
490
|
+
const c00 = sampleLegacy(x0, y0, z0) + (sampleLegacy(x1, y0, z0) - sampleLegacy(x0, y0, z0)) * fx;
|
|
491
|
+
const c10 = sampleLegacy(x0, y1, z0) + (sampleLegacy(x1, y1, z0) - sampleLegacy(x0, y1, z0)) * fx;
|
|
492
|
+
const c01 = sampleLegacy(x0, y0, z1) + (sampleLegacy(x1, y0, z1) - sampleLegacy(x0, y0, z1)) * fx;
|
|
493
|
+
const c11 = sampleLegacy(x0, y1, z1) + (sampleLegacy(x1, y1, z1) - sampleLegacy(x0, y1, z1)) * fx;
|
|
494
|
+
return (c00 + (c10 - c00) * fy + (c01 + (c11 - c01) * fy - (c00 + (c10 - c00) * fy)) * fz) * heightEnvelope(height, 1, 0.5) * params.density;
|
|
495
|
+
}
|
|
496
|
+
const samplePlane = (plane, sx, sy, sz) => {
|
|
497
|
+
const payload = cache.formationData;
|
|
498
|
+
const offset = plane * stride + cacheIndex(cache.resolution, sx, sy, sz);
|
|
499
|
+
return (payload[offset] ?? 0) / 255;
|
|
500
|
+
};
|
|
501
|
+
const sample = (plane) => {
|
|
502
|
+
const repeat = plane === 2 ? 3 : 1;
|
|
503
|
+
const sx = wrapped(
|
|
504
|
+
(x + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
|
|
505
|
+
cache.resolution
|
|
506
|
+
);
|
|
507
|
+
const sy = wrapped(
|
|
508
|
+
(y + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
|
|
509
|
+
cache.resolution
|
|
510
|
+
);
|
|
511
|
+
const sz = wrapped(
|
|
512
|
+
(z + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
|
|
513
|
+
cache.resolution
|
|
514
|
+
);
|
|
515
|
+
const x02 = Math.floor(sx), y02 = Math.floor(sy), z02 = Math.floor(sz);
|
|
516
|
+
const x12 = (x02 + 1) % cache.resolution, y12 = (y02 + 1) % cache.resolution, z12 = (z02 + 1) % cache.resolution;
|
|
517
|
+
const fx2 = sx - x02, fy2 = sy - y02, fz2 = sz - z02;
|
|
518
|
+
const c00 = samplePlane(plane, x02, y02, z02) + (samplePlane(plane, x12, y02, z02) - samplePlane(plane, x02, y02, z02)) * fx2;
|
|
519
|
+
const c10 = samplePlane(plane, x02, y12, z02) + (samplePlane(plane, x12, y12, z02) - samplePlane(plane, x02, y12, z02)) * fx2;
|
|
520
|
+
const c01 = samplePlane(plane, x02, y02, z12) + (samplePlane(plane, x12, y02, z12) - samplePlane(plane, x02, y02, z12)) * fx2;
|
|
521
|
+
const c11 = samplePlane(plane, x02, y12, z12) + (samplePlane(plane, x12, y12, z12) - samplePlane(plane, x02, y12, z12)) * fx2;
|
|
522
|
+
return c00 + (c10 - c00) * fy2 + (c01 + (c11 - c01) * fy2 - (c00 + (c10 - c00) * fy2)) * fz2;
|
|
523
|
+
};
|
|
524
|
+
const weather = sample(0);
|
|
525
|
+
const body = sample(1);
|
|
526
|
+
const erosion = sample(2);
|
|
527
|
+
return composeCloudDensity(height, { weather, body, erosion}, params.coverage) * params.density;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/cloud/inspection.ts
|
|
531
|
+
function resourceStage(input) {
|
|
532
|
+
if (!input.authored || input.generation === void 0) return "none";
|
|
533
|
+
if (input.candidateGeneration !== void 0) return "candidate";
|
|
534
|
+
if (input.degraded === true || input.lkgGeneration !== void 0) return "lkg";
|
|
535
|
+
return "accepted";
|
|
536
|
+
}
|
|
537
|
+
function inspectCloudLayer(input) {
|
|
538
|
+
const capability = {
|
|
539
|
+
compute: input.capability?.compute ?? false,
|
|
540
|
+
storageBuffer: input.capability?.storageBuffer ?? false,
|
|
541
|
+
rgba16floatRenderable: input.capability?.rgba16floatRenderable ?? false,
|
|
542
|
+
physicalGpu: input.capability?.physicalGpu ?? "unknown",
|
|
543
|
+
timestampQuery: input.capability?.timestampQuery ?? false
|
|
544
|
+
};
|
|
545
|
+
const budget = {
|
|
546
|
+
measured: input.budget?.measured ?? false,
|
|
547
|
+
physicalGpu: input.budget?.physicalGpu ?? capability.physicalGpu,
|
|
548
|
+
gpuTimestamp: input.budget?.gpuTimestamp ?? capability.timestampQuery,
|
|
549
|
+
quality: input.budget?.quality,
|
|
550
|
+
warmupFrames: input.budget?.warmupFrames ?? 0,
|
|
551
|
+
sampleFrames: input.budget?.sampleFrames ?? 0,
|
|
552
|
+
p50GpuMs: input.budget?.p50GpuMs,
|
|
553
|
+
p95GpuMs: input.budget?.p95GpuMs,
|
|
554
|
+
coldGenerationMs: input.budget?.coldGenerationMs,
|
|
555
|
+
frameBudgetMs: input.budget?.frameBudgetMs,
|
|
556
|
+
...input.budget?.reason === void 0 ? {} : { reason: input.budget.reason }
|
|
557
|
+
};
|
|
558
|
+
const available = capability.compute && capability.storageBuffer && capability.rgba16floatRenderable;
|
|
559
|
+
return Object.freeze({
|
|
560
|
+
status: !input.authored ? "off" : !available ? "unavailable" : input.degraded === true ? "degraded" : "available",
|
|
561
|
+
resourceStage: resourceStage(input),
|
|
562
|
+
sourceKey: input.sourceKey,
|
|
563
|
+
generation: input.generation,
|
|
564
|
+
candidateGeneration: input.candidateGeneration,
|
|
565
|
+
lkgGeneration: input.lkgGeneration,
|
|
566
|
+
shadowRevision: input.shadowRevision,
|
|
567
|
+
temporalResets: input.temporalResets ?? 0,
|
|
568
|
+
lastTemporalReset: input.lastTemporalReset,
|
|
569
|
+
resourceFacts: input.resourceFacts,
|
|
570
|
+
capability,
|
|
571
|
+
budget
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function cloudCapabilitiesFromRhi(caps) {
|
|
575
|
+
return Object.freeze({
|
|
576
|
+
compute: caps.compute === true,
|
|
577
|
+
storageBuffer: caps.storageBuffer === true,
|
|
578
|
+
rgba16floatRenderable: caps.rgba16floatRenderable === true,
|
|
579
|
+
// RHI caps intentionally do not claim a physical adapter. A browser or
|
|
580
|
+
// Dawn receipt must promote this fact to available/unavailable.
|
|
581
|
+
physicalGpu: "unknown",
|
|
582
|
+
timestampQuery: caps.timestampQuery === true
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/cloud/resources.ts
|
|
587
|
+
var CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL = 8;
|
|
588
|
+
var CLOUD_HISTORY_SURFACE_COUNT = 6;
|
|
589
|
+
function inspectCloudLayerResources(input) {
|
|
590
|
+
const cacheBytes = input.cache?.byteLength ?? 0;
|
|
591
|
+
const shadowResolution = input.shadow?.resolution ?? 0;
|
|
592
|
+
const declaredShadowBytes = shadowResolution * shadowResolution * CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL;
|
|
593
|
+
const historyWidth = input.history?.width ?? 0;
|
|
594
|
+
const historyHeight = input.history?.height ?? 0;
|
|
595
|
+
const declaredHistoryBytes = historyWidth * historyHeight * CLOUD_HISTORY_SURFACE_COUNT * CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL;
|
|
596
|
+
const shadowBytes = input.shadowBytes ?? 0;
|
|
597
|
+
const historyBytes = input.historyBytes ?? 0;
|
|
598
|
+
const inFlightBytes = Math.max(0, input.inFlightBytes ?? 0);
|
|
599
|
+
const residentBytes = cacheBytes + shadowBytes + historyBytes;
|
|
600
|
+
const measuredGpuResourceCount = Number(input.shadowBytes !== void 0) + Number(input.historyBytes !== void 0);
|
|
601
|
+
const gpuEvidence = measuredGpuResourceCount > 0 || input.inFlightBytes !== void 0 ? "measured" : input.shadow !== void 0 || input.history !== void 0 ? "declared" : "unavailable";
|
|
602
|
+
const resourceCount = Number(input.cache !== void 0) + Number(input.shadow !== void 0) + Number(input.history !== void 0);
|
|
603
|
+
return Object.freeze({
|
|
604
|
+
generation: input.generation,
|
|
605
|
+
cacheBytes,
|
|
606
|
+
shadowBytes,
|
|
607
|
+
historyBytes,
|
|
608
|
+
inFlightBytes,
|
|
609
|
+
residentBytes,
|
|
610
|
+
resourceCount,
|
|
611
|
+
gpuEvidence,
|
|
612
|
+
declaredShadowBytes,
|
|
613
|
+
declaredHistoryBytes,
|
|
614
|
+
declaredResidentBytes: cacheBytes + declaredShadowBytes + declaredHistoryBytes,
|
|
615
|
+
declaredResourceCount: resourceCount,
|
|
616
|
+
measuredGpuResourceCount,
|
|
617
|
+
cacheResolution: input.cache?.resolution ?? 0,
|
|
618
|
+
shadowResolution
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function acceptCloudLayerGeneration(candidate, previous) {
|
|
622
|
+
return Object.freeze({
|
|
623
|
+
active: Object.freeze({ ...candidate, status: "active" }),
|
|
624
|
+
lkg: previous === void 0 ? void 0 : Object.freeze({ ...previous, status: "lkg" })
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function retainCloudLayerAfterFailure(active, candidate) {
|
|
628
|
+
return Object.freeze({
|
|
629
|
+
active,
|
|
630
|
+
retiring: Object.freeze({ ...candidate, status: "retiring" })
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/cloud/optics.ts
|
|
635
|
+
var EMPTY_SCATTERING = [0, 0, 0];
|
|
636
|
+
function normalize(vector) {
|
|
637
|
+
const x = vector[0] ?? 0;
|
|
638
|
+
const y = vector[1] ?? 0;
|
|
639
|
+
const z = vector[2] ?? 0;
|
|
640
|
+
const length = Math.hypot(x, y, z);
|
|
641
|
+
return length <= 1e-8 ? [0, 1, 0] : [x / length, y / length, z / length];
|
|
642
|
+
}
|
|
643
|
+
function rayBoxInterval(ray, params) {
|
|
644
|
+
const min = [Number.NEGATIVE_INFINITY, params.baseHeight, Number.NEGATIVE_INFINITY];
|
|
645
|
+
const max = [
|
|
646
|
+
Number.POSITIVE_INFINITY,
|
|
647
|
+
params.baseHeight + params.thickness,
|
|
648
|
+
Number.POSITIVE_INFINITY
|
|
649
|
+
];
|
|
650
|
+
let start = 0;
|
|
651
|
+
let end = Math.max(0, ray.maxDistance);
|
|
652
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
653
|
+
const origin = ray.origin[axis] ?? 0;
|
|
654
|
+
const direction = ray.direction[axis] ?? 0;
|
|
655
|
+
const lower = min[axis] ?? 0;
|
|
656
|
+
const upper = max[axis] ?? 0;
|
|
657
|
+
if (Math.abs(direction) < 1e-8) {
|
|
658
|
+
if (origin < lower || origin > upper) return void 0;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const a = (lower - origin) / direction;
|
|
662
|
+
const b = (upper - origin) / direction;
|
|
663
|
+
start = Math.max(start, Math.min(a, b));
|
|
664
|
+
end = Math.min(end, Math.max(a, b));
|
|
665
|
+
if (end <= start) return void 0;
|
|
666
|
+
}
|
|
667
|
+
return end > start ? { start, end } : void 0;
|
|
668
|
+
}
|
|
669
|
+
function intersectCloudLayer(ray, params) {
|
|
670
|
+
return rayBoxInterval(ray, params);
|
|
671
|
+
}
|
|
672
|
+
function phaseFunction(cosTheta, g) {
|
|
673
|
+
const boundedG = Math.max(-0.95, Math.min(0.95, g));
|
|
674
|
+
const denominator = Math.max(1e-4, 1 + boundedG * boundedG - 2 * boundedG * cosTheta);
|
|
675
|
+
return (1 - boundedG * boundedG) / denominator ** 1.5 * (1 / (4 * Math.PI));
|
|
676
|
+
}
|
|
677
|
+
function cloudPhase(cosTheta, forwardG) {
|
|
678
|
+
return phaseFunction(cosTheta, forwardG) * 0.8 + phaseFunction(cosTheta, -0.2) * 0.2;
|
|
679
|
+
}
|
|
680
|
+
function smoothstep2(edge0, edge1, value) {
|
|
681
|
+
const t = Math.max(0, Math.min(1, (value - edge0) / Math.max(1e-6, edge1 - edge0)));
|
|
682
|
+
return t * t * (3 - 2 * t);
|
|
683
|
+
}
|
|
684
|
+
function cloudIncidentLight(solarT, cosTheta, height, forwardG = 0.55) {
|
|
685
|
+
const direct = solarT * cloudPhase(cosTheta, forwardG);
|
|
686
|
+
const multiple = (0.5 * Math.sqrt(solarT) + 0.25 * solarT ** 0.25) / (4 * Math.PI);
|
|
687
|
+
const energy = (direct + multiple) * (4 * Math.PI);
|
|
688
|
+
const skyFill = 0.025 + 0.05 * smoothstep2(0.1, 0.85, height);
|
|
689
|
+
return [energy + 0.72 * skyFill, energy + 0.84 * skyFill, energy + skyFill];
|
|
690
|
+
}
|
|
691
|
+
function densityAt(input, position) {
|
|
692
|
+
return input.cache === void 0 ? evaluateCloudDensity(input.params, position, input.timeSeconds ?? 0).density : sampleCloudDensity(input.cache, input.params, position, input.timeSeconds ?? 0);
|
|
693
|
+
}
|
|
694
|
+
function integrateCloudPath(input) {
|
|
695
|
+
const direction = normalize(input.ray.direction);
|
|
696
|
+
const interval = rayBoxInterval({ ...input.ray, direction }, input.params);
|
|
697
|
+
if (interval === void 0 || input.params.density <= 0) {
|
|
698
|
+
return {
|
|
699
|
+
path: input.path,
|
|
700
|
+
transmittance: 1,
|
|
701
|
+
scattering: EMPTY_SCATTERING,
|
|
702
|
+
opticalDepth: 0,
|
|
703
|
+
representativeDepth: input.ray.maxDistance,
|
|
704
|
+
steps: 0,
|
|
705
|
+
entered: false,
|
|
706
|
+
exited: false
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const steps = Math.max(1, Math.floor(input.steps ?? 32));
|
|
710
|
+
const length = interval.end - interval.start;
|
|
711
|
+
const stepLength = length / steps;
|
|
712
|
+
let opticalDepth = 0;
|
|
713
|
+
const scattering = [0, 0, 0];
|
|
714
|
+
let weightedDepth = 0;
|
|
715
|
+
let scatteringWeight = 0;
|
|
716
|
+
const lighting = input.lighting;
|
|
717
|
+
const lightDirection = lighting === void 0 ? void 0 : normalize(lighting.sunDirection);
|
|
718
|
+
const viewDirection = normalize(input.ray.direction);
|
|
719
|
+
for (let index = 0; index < steps; index += 1) {
|
|
720
|
+
const distance = interval.start + (index + 0.5) * stepLength;
|
|
721
|
+
const position = [
|
|
722
|
+
(input.ray.origin[0] ?? 0) + direction[0] * distance,
|
|
723
|
+
(input.ray.origin[1] ?? 0) + direction[1] * distance,
|
|
724
|
+
(input.ray.origin[2] ?? 0) + direction[2] * distance
|
|
725
|
+
];
|
|
726
|
+
const localDensity = Math.max(0, densityAt(input, position));
|
|
727
|
+
const extinction = localDensity * CLOUD_EXTINCTION_COEFFICIENT * stepLength;
|
|
728
|
+
const transmittanceBefore = Math.exp(-opticalDepth);
|
|
729
|
+
opticalDepth += extinction;
|
|
730
|
+
if (lighting !== void 0 && lightDirection !== void 0 && localDensity > 0) {
|
|
731
|
+
const localShadow = integrateCloudPath({
|
|
732
|
+
params: input.params,
|
|
733
|
+
path: "solar-column",
|
|
734
|
+
ray: {
|
|
735
|
+
origin: position,
|
|
736
|
+
direction: lightDirection,
|
|
737
|
+
maxDistance: input.params.shadowRange
|
|
738
|
+
},
|
|
739
|
+
steps: Math.max(4, Math.ceil((input.steps ?? 32) * 0.5)),
|
|
740
|
+
...input.timeSeconds === void 0 ? {} : { timeSeconds: input.timeSeconds },
|
|
741
|
+
...input.cache === void 0 ? {} : { cache: input.cache }
|
|
742
|
+
});
|
|
743
|
+
const cosTheta = viewDirection[0] * lightDirection[0] + viewDirection[1] * lightDirection[1] + viewDirection[2] * lightDirection[2];
|
|
744
|
+
const segmentWeight = transmittanceBefore * (1 - Math.exp(-extinction));
|
|
745
|
+
const heightFraction = Math.max(
|
|
746
|
+
0,
|
|
747
|
+
Math.min(1, ((position[1] ?? 0) - input.params.baseHeight) / input.params.thickness)
|
|
748
|
+
);
|
|
749
|
+
const incident = cloudIncidentLight(
|
|
750
|
+
localShadow.transmittance,
|
|
751
|
+
cosTheta,
|
|
752
|
+
heightFraction,
|
|
753
|
+
lighting.phaseG
|
|
754
|
+
);
|
|
755
|
+
for (let channel = 0; channel < 3; channel++) {
|
|
756
|
+
scattering[channel] = (scattering[channel] ?? 0) + (lighting.sunRadiance[channel] ?? 0) * (incident[channel] ?? 0) * segmentWeight;
|
|
757
|
+
}
|
|
758
|
+
const weight = segmentWeight;
|
|
759
|
+
scatteringWeight += weight;
|
|
760
|
+
weightedDepth += distance * weight;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const transmittance = Math.exp(-opticalDepth);
|
|
764
|
+
return {
|
|
765
|
+
path: input.path,
|
|
766
|
+
transmittance,
|
|
767
|
+
scattering,
|
|
768
|
+
opticalDepth,
|
|
769
|
+
representativeDepth: scatteringWeight > 1e-6 ? weightedDepth / scatteringWeight : interval.start,
|
|
770
|
+
steps,
|
|
771
|
+
entered: true,
|
|
772
|
+
exited: interval.end < input.ray.maxDistance
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function integrateCloudCameraPath(params, ray, options = {}) {
|
|
776
|
+
return integrateCloudPath({ ...options, params, ray, path: "camera" });
|
|
777
|
+
}
|
|
778
|
+
function integrateCloudSolarColumn(params, origin, sunDirection, options = {}) {
|
|
779
|
+
return integrateCloudPath({
|
|
780
|
+
...options,
|
|
781
|
+
params,
|
|
782
|
+
path: "solar-column",
|
|
783
|
+
ray: {
|
|
784
|
+
origin,
|
|
785
|
+
direction: sunDirection,
|
|
786
|
+
maxDistance: options.maxDistance ?? params.shadowRange
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
function integrateCloudInterior(params, ray, lighting, options = {}) {
|
|
791
|
+
return integrateCloudPath({ ...options, params, ray, path: "cloud-interior", lighting });
|
|
792
|
+
}
|
|
793
|
+
function applyCloudSolarTransmittance(directSunRadiance, cloudTransmittance, meshVisibility = 1) {
|
|
794
|
+
const factor = Math.min(1, Math.max(0, cloudTransmittance)) * Math.min(1, Math.max(0, meshVisibility));
|
|
795
|
+
return [
|
|
796
|
+
(directSunRadiance[0] ?? 0) * factor,
|
|
797
|
+
(directSunRadiance[1] ?? 0) * factor,
|
|
798
|
+
(directSunRadiance[2] ?? 0) * factor
|
|
799
|
+
];
|
|
800
|
+
}
|
|
801
|
+
function compositeCloudRadiance(sceneLinearHdr, cloud, sceneDepth, cloudDepth = cloud.representativeDepth) {
|
|
802
|
+
if (!Number.isFinite(sceneDepth) || cloudDepth > sceneDepth) return sceneLinearHdr;
|
|
803
|
+
const transmittance = Math.min(1, Math.max(0, cloud.transmittance));
|
|
804
|
+
return [
|
|
805
|
+
(sceneLinearHdr[0] ?? 0) * transmittance + (cloud.scattering[0] ?? 0),
|
|
806
|
+
(sceneLinearHdr[1] ?? 0) * transmittance + (cloud.scattering[1] ?? 0),
|
|
807
|
+
(sceneLinearHdr[2] ?? 0) * transmittance + (cloud.scattering[2] ?? 0),
|
|
808
|
+
1
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/cloud/shadow.ts
|
|
813
|
+
function normalize2(vector) {
|
|
814
|
+
const x = vector[0] ?? 0;
|
|
815
|
+
const y = vector[1] ?? 0;
|
|
816
|
+
const z = vector[2] ?? 0;
|
|
817
|
+
const length = Math.hypot(x, y, z);
|
|
818
|
+
return length <= 1e-8 ? [0, 1, 0] : [x / length, y / length, z / length];
|
|
819
|
+
}
|
|
820
|
+
function cross(a, b) {
|
|
821
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
822
|
+
}
|
|
823
|
+
function dot(a, b) {
|
|
824
|
+
return (a[0] ?? 0) * (b[0] ?? 0) + (a[1] ?? 0) * (b[1] ?? 0) + (a[2] ?? 0) * (b[2] ?? 0);
|
|
825
|
+
}
|
|
826
|
+
function stableRevision(input) {
|
|
827
|
+
let hash = 2166136261;
|
|
828
|
+
const values = [...input.center, ...input.sunDirection, input.range, input.resolution];
|
|
829
|
+
for (const value of values) {
|
|
830
|
+
const bits = new Float32Array([value]);
|
|
831
|
+
const bytes = new Uint8Array(bits.buffer);
|
|
832
|
+
for (const byte of bytes) hash = Math.imul(hash ^ byte, 16777619);
|
|
833
|
+
}
|
|
834
|
+
return hash >>> 0;
|
|
835
|
+
}
|
|
836
|
+
function createCloudShadowProjection(input) {
|
|
837
|
+
const sunDirection = normalize2(input.sunDirection);
|
|
838
|
+
const resolution = Math.max(16, Math.floor(input.resolution ?? 1024));
|
|
839
|
+
const range = Math.max(1, input.range);
|
|
840
|
+
const texelSize = range / resolution;
|
|
841
|
+
const reference = Math.abs(sunDirection[1]) < 0.95 ? [0, 1, 0] : [1, 0, 0];
|
|
842
|
+
const right = normalize2(cross(reference, sunDirection));
|
|
843
|
+
const up = normalize2(cross(sunDirection, right));
|
|
844
|
+
const center = input.center;
|
|
845
|
+
const lightX = dot(center, right);
|
|
846
|
+
const lightY = dot(center, up);
|
|
847
|
+
const lightZ = dot(center, sunDirection);
|
|
848
|
+
const snappedX = Math.floor(lightX / texelSize + 0.5) * texelSize;
|
|
849
|
+
const snappedY = Math.floor(lightY / texelSize + 0.5) * texelSize;
|
|
850
|
+
const origin = [
|
|
851
|
+
right[0] * snappedX + up[0] * snappedY + sunDirection[0] * lightZ,
|
|
852
|
+
right[1] * snappedX + up[1] * snappedY + sunDirection[1] * lightZ,
|
|
853
|
+
right[2] * snappedX + up[2] * snappedY + sunDirection[2] * lightZ
|
|
854
|
+
];
|
|
855
|
+
return Object.freeze({
|
|
856
|
+
revision: stableRevision({ center, sunDirection, range, resolution }),
|
|
857
|
+
resolution,
|
|
858
|
+
range,
|
|
859
|
+
texelSize,
|
|
860
|
+
center: Object.freeze([...center]),
|
|
861
|
+
origin: Object.freeze(origin),
|
|
862
|
+
right,
|
|
863
|
+
up,
|
|
864
|
+
sunDirection,
|
|
865
|
+
lowSun: sunDirection[1] <= 0.08
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function projectCloudShadowUv(projection, position) {
|
|
869
|
+
const relative = [
|
|
870
|
+
position[0] - projection.origin[0],
|
|
871
|
+
position[1] - projection.origin[1],
|
|
872
|
+
position[2] - projection.origin[2]
|
|
873
|
+
];
|
|
874
|
+
const x = dot(relative, projection.right);
|
|
875
|
+
const y = dot(relative, projection.up);
|
|
876
|
+
const uv = [0.5 + x / projection.range, 0.5 + y / projection.range];
|
|
877
|
+
return { uv, inRange: uv[0] >= 0 && uv[0] <= 1 && uv[1] >= 0 && uv[1] <= 1 };
|
|
878
|
+
}
|
|
879
|
+
function sampleCloudShadow(params, projection, worldPosition, options = {}) {
|
|
880
|
+
const projected = projectCloudShadowUv(projection, worldPosition);
|
|
881
|
+
if (projection.lowSun) {
|
|
882
|
+
return {
|
|
883
|
+
transmittance: 1,
|
|
884
|
+
uv: projected.uv,
|
|
885
|
+
valid: false,
|
|
886
|
+
fallback: "low-sun",
|
|
887
|
+
revision: projection.revision
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (!projected.inRange) {
|
|
891
|
+
return {
|
|
892
|
+
transmittance: 1,
|
|
893
|
+
uv: projected.uv,
|
|
894
|
+
valid: false,
|
|
895
|
+
fallback: "outside-range",
|
|
896
|
+
revision: projection.revision
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
if (params.coverage >= 1 || params.density <= 0) {
|
|
900
|
+
return {
|
|
901
|
+
transmittance: 1,
|
|
902
|
+
uv: projected.uv,
|
|
903
|
+
valid: false,
|
|
904
|
+
fallback: "empty-coverage",
|
|
905
|
+
revision: projection.revision
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const integrated = integrateCloudSolarColumn(params, worldPosition, projection.sunDirection, {
|
|
909
|
+
steps: 20,
|
|
910
|
+
maxDistance: projection.range,
|
|
911
|
+
...options.cache === void 0 ? {} : { cache: options.cache },
|
|
912
|
+
...options.timeSeconds === void 0 ? {} : { timeSeconds: options.timeSeconds }
|
|
913
|
+
});
|
|
914
|
+
return {
|
|
915
|
+
transmittance: integrated.transmittance,
|
|
916
|
+
uv: projected.uv,
|
|
917
|
+
valid: integrated.entered,
|
|
918
|
+
fallback: "none",
|
|
919
|
+
revision: projection.revision
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/cloud/temporal.ts
|
|
924
|
+
function cloudTemporalSignature(input) {
|
|
925
|
+
return [
|
|
926
|
+
"cloud-temporal-v1",
|
|
927
|
+
input.viewId,
|
|
928
|
+
input.sourceKey,
|
|
929
|
+
input.authoringGeneration,
|
|
930
|
+
input.cloudShadowRevision,
|
|
931
|
+
input.cameraRevision,
|
|
932
|
+
input.deviceGeneration,
|
|
933
|
+
input.width,
|
|
934
|
+
input.height,
|
|
935
|
+
input.timeSeconds,
|
|
936
|
+
input.quality,
|
|
937
|
+
input.sceneDepthVersion ?? 0
|
|
938
|
+
].join(":");
|
|
939
|
+
}
|
|
940
|
+
function sameLayout(left, right) {
|
|
941
|
+
return left.viewId === right.viewId && left.width === right.width && left.height === right.height;
|
|
942
|
+
}
|
|
943
|
+
function cloudTemporalResetReasons(previous, next, options = {}) {
|
|
944
|
+
if (previous === void 0) return ["history-missing"];
|
|
945
|
+
const reasons = [];
|
|
946
|
+
if (previous.sourceKey !== next.sourceKey || previous.authoringGeneration !== next.authoringGeneration) {
|
|
947
|
+
reasons.push("authoring-generation");
|
|
948
|
+
}
|
|
949
|
+
if (previous.cloudShadowRevision !== next.cloudShadowRevision)
|
|
950
|
+
reasons.push("cloud-shadow-revision");
|
|
951
|
+
if (previous.deviceGeneration !== next.deviceGeneration) reasons.push("device-generation");
|
|
952
|
+
if (previous.sceneDepthVersion !== void 0 && next.sceneDepthVersion !== void 0 && previous.sceneDepthVersion !== next.sceneDepthVersion) {
|
|
953
|
+
reasons.push("scene-depth-disocclusion");
|
|
954
|
+
}
|
|
955
|
+
if (!sameLayout(previous, next)) reasons.push("resize");
|
|
956
|
+
if (options.cameraCut === true || previous.cameraRevision !== next.cameraRevision)
|
|
957
|
+
reasons.push("camera-cut");
|
|
958
|
+
const dt = next.timeSeconds - previous.timeSeconds;
|
|
959
|
+
if (!Number.isFinite(dt) || dt < 0 || dt > 0.5) reasons.push("time-discontinuity");
|
|
960
|
+
if (options.recovery === true) reasons.push("recovery");
|
|
961
|
+
return Object.freeze(reasons);
|
|
962
|
+
}
|
|
963
|
+
function createCloudHistory(signature, generation, valid = false) {
|
|
964
|
+
return Object.freeze({
|
|
965
|
+
width: Math.max(1, Math.floor(signature.width)),
|
|
966
|
+
height: Math.max(1, Math.floor(signature.height)),
|
|
967
|
+
signature: cloudTemporalSignature(signature),
|
|
968
|
+
temporal: Object.freeze({ ...signature }),
|
|
969
|
+
generation,
|
|
970
|
+
valid
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
var CloudHistoryStore = class {
|
|
974
|
+
maxViews;
|
|
975
|
+
entries = /* @__PURE__ */ new Map();
|
|
976
|
+
constructor(maxViews = 4) {
|
|
977
|
+
this.maxViews = Math.max(1, Math.floor(maxViews));
|
|
978
|
+
}
|
|
979
|
+
begin(signature, options = {}) {
|
|
980
|
+
const key = signature.viewId;
|
|
981
|
+
const previousEntry = this.entries.get(key);
|
|
982
|
+
const previous = previousEntry?.history;
|
|
983
|
+
const reasons = cloudTemporalResetReasons(previousEntry?.signature, signature, options);
|
|
984
|
+
const reset = reasons.length > 0;
|
|
985
|
+
return Object.freeze({
|
|
986
|
+
reset,
|
|
987
|
+
reasons,
|
|
988
|
+
signature: cloudTemporalSignature(signature),
|
|
989
|
+
history: reset ? void 0 : previous
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
commit(signature, history) {
|
|
993
|
+
if (history.width !== Math.max(1, Math.floor(signature.width)) || history.height !== Math.max(1, Math.floor(signature.height))) {
|
|
994
|
+
this.entries.delete(signature.viewId);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
this.entries.set(signature.viewId, { history, signature: Object.freeze({ ...signature }) });
|
|
998
|
+
while (this.entries.size > this.maxViews) {
|
|
999
|
+
const oldest = this.entries.keys().next().value;
|
|
1000
|
+
if (oldest === void 0) break;
|
|
1001
|
+
this.entries.delete(oldest);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
reset(viewId) {
|
|
1005
|
+
if (viewId === void 0) this.entries.clear();
|
|
1006
|
+
else this.entries.delete(viewId);
|
|
1007
|
+
}
|
|
1008
|
+
get(viewId) {
|
|
1009
|
+
return this.entries.get(viewId)?.history;
|
|
1010
|
+
}
|
|
1011
|
+
dispose() {
|
|
1012
|
+
this.entries.clear();
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
function reprojectCloudHistory(input, historyWeight = 0.9) {
|
|
1016
|
+
const width = Math.max(1, input.historySize[0] ?? 1);
|
|
1017
|
+
const height = Math.max(1, input.historySize[1] ?? 1);
|
|
1018
|
+
const uv = [
|
|
1019
|
+
((input.currentPixel[0] ?? 0) - (input.motion[0] ?? 0)) / width,
|
|
1020
|
+
((input.currentPixel[1] ?? 0) - (input.motion[1] ?? 0)) / height
|
|
1021
|
+
];
|
|
1022
|
+
if (uv[0] < 0 || uv[0] > 1 || uv[1] < 0 || uv[1] > 1) {
|
|
1023
|
+
return { accepted: false, historyUv: uv, weight: 0, reason: "outside" };
|
|
1024
|
+
}
|
|
1025
|
+
const depthTolerance = input.depthTolerance ?? Math.max(0.01, Math.abs(input.currentDepth) * 0.02);
|
|
1026
|
+
if (!Number.isFinite(input.currentDepth) || !Number.isFinite(input.historyDepth) || Math.abs(input.currentDepth - input.historyDepth) > depthTolerance) {
|
|
1027
|
+
return { accepted: false, historyUv: uv, weight: 0, reason: "depth-disocclusion" };
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
accepted: true,
|
|
1031
|
+
historyUv: uv,
|
|
1032
|
+
weight: Math.min(0.99, Math.max(0, historyWeight)),
|
|
1033
|
+
reason: "accepted"
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
var CLOUD_LAYER_FEATURE_IDENTITY = "forgeax.cloud-layer";
|
|
1037
|
+
var CLOUD_LAYER_DENSITY_PROGRAM = "cloud-layer-density";
|
|
1038
|
+
var CLOUD_LAYER_DENSITY_BINDINGS = "cloud-layer-density-bindings";
|
|
1039
|
+
var CLOUD_LAYER_DENSITY_CACHE = "cloud-layer-density-cache";
|
|
1040
|
+
var CLOUD_LAYER_DENSITY_OUTPUT = "cloud-layer-density-output";
|
|
1041
|
+
var COMPUTE_STAGE = 4;
|
|
1042
|
+
var CLOUD_WORKGROUP_SIZE = 64;
|
|
1043
|
+
var CLOUD_VIEW_PARAMS_BYTES = 176;
|
|
1044
|
+
function packCloudDensityCache(cache) {
|
|
1045
|
+
const packed = new Uint32Array(Math.max(1, Math.ceil(cache.formationData.byteLength / 4)));
|
|
1046
|
+
for (let index = 0; index < cache.formationData.byteLength; index += 1) {
|
|
1047
|
+
const wordIndex = index >>> 2;
|
|
1048
|
+
packed[wordIndex] = (packed[wordIndex] ?? 0) | (cache.formationData[index] ?? 0) << (index & 3) * 8;
|
|
1049
|
+
}
|
|
1050
|
+
return packed;
|
|
1051
|
+
}
|
|
1052
|
+
var CLOUD_DENSITY_COMPUTE_WGSL = (
|
|
1053
|
+
/* wgsl */
|
|
1054
|
+
`
|
|
1055
|
+
struct CloudCacheParams { count: u32, }
|
|
1056
|
+
@group(0) @binding(0) var<storage, read> sourceDensity: array<u32>;
|
|
1057
|
+
@group(0) @binding(1) var<storage, read_write> cachedDensity: array<u32>;
|
|
1058
|
+
@group(0) @binding(2) var<uniform> params: CloudCacheParams;
|
|
1059
|
+
@compute @workgroup_size(64)
|
|
1060
|
+
fn cloud_density_cache(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
1061
|
+
if (id.x >= params.count) { return; }
|
|
1062
|
+
cachedDensity[id.x] = sourceDensity[id.x];
|
|
1063
|
+
}
|
|
1064
|
+
`
|
|
1065
|
+
);
|
|
1066
|
+
var CLOUD_VIEW_FULLSCREEN_WGSL = (
|
|
1067
|
+
/* wgsl */
|
|
1068
|
+
`
|
|
1069
|
+
const CLOUD_VERTICAL_CELLS: f32 = ${CLOUD_VERTICAL_CELLS};
|
|
1070
|
+
const CLOUD_VERTICAL_NOISE_CELLS: f32 = 2.0;
|
|
1071
|
+
|
|
1072
|
+
struct CloudCameraView {
|
|
1073
|
+
worldViewProj: mat4x4<f32>,
|
|
1074
|
+
lightDir: vec3<f32>,
|
|
1075
|
+
lightColor: vec3<f32>,
|
|
1076
|
+
cameraPos: vec3<f32>,
|
|
1077
|
+
lightViewProj_A: mat4x4<f32>,
|
|
1078
|
+
inverseViewProj: mat4x4<f32>,
|
|
1079
|
+
lightViewProj_B: mat4x4<f32>,
|
|
1080
|
+
lightViewProj_C: mat4x4<f32>,
|
|
1081
|
+
lightViewProj_D: mat4x4<f32>,
|
|
1082
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
1083
|
+
cascadeCount: f32,
|
|
1084
|
+
cascadeBlend: f32,
|
|
1085
|
+
depthBias: f32,
|
|
1086
|
+
normalBias: f32,
|
|
1087
|
+
directionalShadowFilter: vec4<f32>,
|
|
1088
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
1089
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
1090
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
1091
|
+
temporalProjection: vec4<f32>,
|
|
1092
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
1093
|
+
ssrParams: vec4<f32>,
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
struct CloudViewParams {
|
|
1097
|
+
layer: vec4<f32>, // baseHeight, thickness, scale, density
|
|
1098
|
+
field: vec4<f32>, // coverage, timeSeconds, maxDistance, seed
|
|
1099
|
+
wind: vec4<f32>, // xyz m/s
|
|
1100
|
+
sunDirection: vec4<f32>,
|
|
1101
|
+
sunRadiance: vec4<f32>,
|
|
1102
|
+
integration: vec4<u32>, // cacheResolution, viewSteps, solarSteps, reserved
|
|
1103
|
+
shadowOrigin: vec4<f32>,
|
|
1104
|
+
shadowRight: vec4<f32>,
|
|
1105
|
+
shadowUp: vec4<f32>,
|
|
1106
|
+
shadowProjection: vec4<f32>, // range, valid, lowSun, texelSize
|
|
1107
|
+
temporal: vec4<f32>, // valid, reset, historyWeight, reserved
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
1111
|
+
@group(1) @binding(0) var sceneColor: texture_2d<f32>;
|
|
1112
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
1113
|
+
@group(1) @binding(2) var<uniform> cloud: CloudViewParams;
|
|
1114
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
1115
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
1116
|
+
@group(1) @binding(5) var previousRadiance: texture_2d<f32>;
|
|
1117
|
+
@group(1) @binding(6) var previousTransmittance: texture_2d<f32>;
|
|
1118
|
+
@group(1) @binding(7) var previousDepth: texture_2d<f32>;
|
|
1119
|
+
@group(1) @binding(8) var<storage, read> densityCache: array<u32>;
|
|
1120
|
+
|
|
1121
|
+
struct CloudViewOut {
|
|
1122
|
+
@builtin(position) position: vec4<f32>,
|
|
1123
|
+
@location(0) uv: vec2<f32>,
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
fn cloud_hash3(cell: vec3<i32>, seed: u32) -> f32 {
|
|
1127
|
+
var h = bitcast<u32>(cell.x) * 0x45d9f3bu;
|
|
1128
|
+
h = (h ^ (bitcast<u32>(cell.y) * 0x119de1f3u)) * 0x45d9f3bu;
|
|
1129
|
+
h = (h ^ (bitcast<u32>(cell.z) * 0x3449fu)) * 0x45d9f3bu;
|
|
1130
|
+
h = h ^ seed;
|
|
1131
|
+
h = (h ^ (h >> 16u)) * 0x45d9f3bu;
|
|
1132
|
+
h = (h ^ (h >> 13u)) * 0x27d4eb2du;
|
|
1133
|
+
return f32(h ^ (h >> 16u)) / 4294967296.0;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
fn cloud_wrap_cell(value: i32, period: i32) -> i32 {
|
|
1137
|
+
let wrapped = value % period;
|
|
1138
|
+
return select(wrapped, wrapped + period, wrapped < 0);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
fn cloud_hash3_periodic(
|
|
1142
|
+
cell: vec3<i32>,
|
|
1143
|
+
seed: u32,
|
|
1144
|
+
periodX: i32,
|
|
1145
|
+
periodY: i32,
|
|
1146
|
+
periodZ: i32,
|
|
1147
|
+
) -> f32 {
|
|
1148
|
+
return cloud_hash3(
|
|
1149
|
+
vec3<i32>(
|
|
1150
|
+
cloud_wrap_cell(cell.x, periodX),
|
|
1151
|
+
cloud_wrap_cell(cell.y, periodY),
|
|
1152
|
+
cloud_wrap_cell(cell.z, periodZ),
|
|
1153
|
+
),
|
|
1154
|
+
seed,
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
fn cloud_value_noise(
|
|
1159
|
+
position: vec3<f32>,
|
|
1160
|
+
seed: u32,
|
|
1161
|
+
periodX: i32,
|
|
1162
|
+
periodY: i32,
|
|
1163
|
+
periodZ: i32,
|
|
1164
|
+
) -> f32 {
|
|
1165
|
+
let cell = vec3<i32>(floor(position));
|
|
1166
|
+
let fraction = fract(position);
|
|
1167
|
+
let smoothFraction = fraction * fraction * (3.0 - 2.0 * fraction);
|
|
1168
|
+
let c000 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 0), seed, periodX, periodY, periodZ);
|
|
1169
|
+
let c100 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 0), seed, periodX, periodY, periodZ);
|
|
1170
|
+
let c010 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 0), seed, periodX, periodY, periodZ);
|
|
1171
|
+
let c110 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 0), seed, periodX, periodY, periodZ);
|
|
1172
|
+
let c001 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 1), seed, periodX, periodY, periodZ);
|
|
1173
|
+
let c101 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 1), seed, periodX, periodY, periodZ);
|
|
1174
|
+
let c011 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 1), seed, periodX, periodY, periodZ);
|
|
1175
|
+
let c111 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 1), seed, periodX, periodY, periodZ);
|
|
1176
|
+
let x00 = mix(c000, c100, smoothFraction.x);
|
|
1177
|
+
let x10 = mix(c010, c110, smoothFraction.x);
|
|
1178
|
+
let x01 = mix(c001, c101, smoothFraction.x);
|
|
1179
|
+
let x11 = mix(c011, c111, smoothFraction.x);
|
|
1180
|
+
return mix(mix(x00, x10, smoothFraction.y), mix(x01, x11, smoothFraction.y), smoothFraction.z);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
fn cloud_cellular_noise(position: vec3<f32>, seed: u32, periodXZ: i32, periodY: i32) -> f32 {
|
|
1184
|
+
let cell = vec3<i32>(floor(position));
|
|
1185
|
+
let fraction = fract(position);
|
|
1186
|
+
var nearest = 1e9;
|
|
1187
|
+
var dz: i32 = -1;
|
|
1188
|
+
loop {
|
|
1189
|
+
var dy: i32 = -1;
|
|
1190
|
+
loop {
|
|
1191
|
+
var dx: i32 = -1;
|
|
1192
|
+
loop {
|
|
1193
|
+
let random = cloud_hash3_periodic(
|
|
1194
|
+
cell + vec3<i32>(dx, dy, dz),
|
|
1195
|
+
seed ^ 0x9e3779b9u,
|
|
1196
|
+
periodXZ,
|
|
1197
|
+
periodY,
|
|
1198
|
+
periodXZ,
|
|
1199
|
+
);
|
|
1200
|
+
let point = vec3<f32>(
|
|
1201
|
+
f32(dx) + fract(random * 17.0) - fraction.x,
|
|
1202
|
+
f32(dy) + fract(random * 31.0) - fraction.y,
|
|
1203
|
+
f32(dz) + fract(random * 47.0) - fraction.z,
|
|
1204
|
+
);
|
|
1205
|
+
nearest = min(nearest, dot(point, point));
|
|
1206
|
+
if (dx >= 1) { break; }
|
|
1207
|
+
dx = dx + 1;
|
|
1208
|
+
}
|
|
1209
|
+
if (dy >= 1) { break; }
|
|
1210
|
+
dy = dy + 1;
|
|
1211
|
+
}
|
|
1212
|
+
if (dz >= 1) { break; }
|
|
1213
|
+
dz = dz + 1;
|
|
1214
|
+
}
|
|
1215
|
+
return 1.0 - clamp(sqrt(nearest) * 1.25, 0.0, 1.0);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
fn cloud_weather_field(position: vec3<f32>, seed: u32) -> f32 {
|
|
1219
|
+
return smoothstep(
|
|
1220
|
+
0.34,
|
|
1221
|
+
0.66,
|
|
1222
|
+
cloud_value_noise(
|
|
1223
|
+
vec3<f32>(position.x * 2.0, 0.37, position.z * 2.0),
|
|
1224
|
+
seed + 17041u,
|
|
1225
|
+
2,
|
|
1226
|
+
1,
|
|
1227
|
+
2,
|
|
1228
|
+
),
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
fn cloud_formation_field(position: vec3<f32>, seed: u32) -> vec3<f32> {
|
|
1233
|
+
let macroWarpX = cloud_value_noise(
|
|
1234
|
+
vec3<f32>(position.x * 2.0, position.y * CLOUD_VERTICAL_NOISE_CELLS + 3.0, position.z * 2.0),
|
|
1235
|
+
seed + 41023u,
|
|
1236
|
+
2,
|
|
1237
|
+
2,
|
|
1238
|
+
2,
|
|
1239
|
+
) - 0.5;
|
|
1240
|
+
let macroWarpZ = cloud_value_noise(
|
|
1241
|
+
vec3<f32>(position.x * 2.0 + 5.0, position.y * CLOUD_VERTICAL_NOISE_CELLS - 7.0, position.z * 2.0),
|
|
1242
|
+
seed + 41023u,
|
|
1243
|
+
2,
|
|
1244
|
+
2,
|
|
1245
|
+
2,
|
|
1246
|
+
) - 0.5;
|
|
1247
|
+
let bodyX = position.x + macroWarpX * 0.3;
|
|
1248
|
+
let bodyZ = position.z + macroWarpZ * 0.3;
|
|
1249
|
+
var noise = 0.0;
|
|
1250
|
+
var weight = 0.0;
|
|
1251
|
+
var amplitude = 0.5;
|
|
1252
|
+
var frequency = 1.0;
|
|
1253
|
+
for (var octave = 0u; octave < 4u; octave = octave + 1u) {
|
|
1254
|
+
let tilePeriod = 4.0 * frequency;
|
|
1255
|
+
noise = noise + cloud_value_noise(
|
|
1256
|
+
vec3<f32>(bodyX * tilePeriod, position.y * frequency * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * tilePeriod),
|
|
1257
|
+
seed + octave * 1013u,
|
|
1258
|
+
i32(tilePeriod),
|
|
1259
|
+
i32(frequency * CLOUD_VERTICAL_NOISE_CELLS),
|
|
1260
|
+
i32(tilePeriod),
|
|
1261
|
+
) * amplitude;
|
|
1262
|
+
weight = weight + amplitude;
|
|
1263
|
+
amplitude = amplitude * 0.4;
|
|
1264
|
+
frequency = frequency * 2.0;
|
|
1265
|
+
}
|
|
1266
|
+
let broad = noise / max(0.0001, weight);
|
|
1267
|
+
let cells = cloud_cellular_noise(
|
|
1268
|
+
vec3<f32>(bodyX * 4.0, position.y * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * 4.0),
|
|
1269
|
+
seed + 5011u,
|
|
1270
|
+
4,
|
|
1271
|
+
i32(CLOUD_VERTICAL_NOISE_CELLS),
|
|
1272
|
+
);
|
|
1273
|
+
// Keep the broad field responsible for the cloud silhouette and use the
|
|
1274
|
+
// cellular term as bounded breakup. A broad-dominant mix reads as fewer,
|
|
1275
|
+
// wider formations instead of evenly spaced foam balls.
|
|
1276
|
+
let base = clamp(broad * 0.85 + cells * 0.15 + 0.15, 0.0, 1.0);
|
|
1277
|
+
let weather = cloud_weather_field(position, seed);
|
|
1278
|
+
let detailPosition = position * 3.0;
|
|
1279
|
+
let warpX = cloud_value_noise(
|
|
1280
|
+
vec3<f32>(detailPosition.x * 2.0, detailPosition.y * 2.0 + 11.0, detailPosition.z * 2.0),
|
|
1281
|
+
seed + 29011u,
|
|
1282
|
+
2,
|
|
1283
|
+
2,
|
|
1284
|
+
2,
|
|
1285
|
+
) - 0.5;
|
|
1286
|
+
let warpZ = cloud_value_noise(
|
|
1287
|
+
vec3<f32>(detailPosition.x * 2.0 + 7.0, detailPosition.y * 2.0 - 5.0, detailPosition.z * 2.0),
|
|
1288
|
+
seed + 29011u,
|
|
1289
|
+
2,
|
|
1290
|
+
2,
|
|
1291
|
+
2,
|
|
1292
|
+
) - 0.5;
|
|
1293
|
+
let erosion = cloud_cellular_noise(
|
|
1294
|
+
vec3<f32>(
|
|
1295
|
+
(detailPosition.x + warpX * 0.22) * 12.0,
|
|
1296
|
+
detailPosition.y * 3.0,
|
|
1297
|
+
(detailPosition.z + warpZ * 0.22) * 12.0,
|
|
1298
|
+
),
|
|
1299
|
+
seed + 7919u,
|
|
1300
|
+
12,
|
|
1301
|
+
3,
|
|
1302
|
+
);
|
|
1303
|
+
return vec3<f32>(weather, base, erosion);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
fn cloud_height_profile(height: f32, body: f32, weather: f32) -> f32 {
|
|
1307
|
+
// A body-dependent crown breaks the fixed horizontal slab silhouette while
|
|
1308
|
+
// keeping the profile smooth enough for the bounded ray step budget.
|
|
1309
|
+
let lowerEdge = 0.05 + (1.0 - body) * 0.1;
|
|
1310
|
+
let crownEdge = min(0.96, 0.6 + body * 0.3 + weather * 0.08);
|
|
1311
|
+
let lower = smoothstep(lowerEdge, min(1.0, lowerEdge + 0.12), height);
|
|
1312
|
+
let upper = 1.0 - smoothstep(crownEdge, min(1.0, crownEdge + 0.14), height);
|
|
1313
|
+
return lower * upper;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
fn cloud_compose_density(height: f32, formation: vec3<f32>, coverage: f32) -> f32 {
|
|
1317
|
+
let threshold = 1.0 - coverage * (0.45 + formation.x * 0.55);
|
|
1318
|
+
let covered = smoothstep(0.0, 0.4, (formation.y - threshold) / max(0.001, 1.0 - threshold));
|
|
1319
|
+
let shaped = covered * cloud_height_profile(height, formation.y, formation.x);
|
|
1320
|
+
let erosion = (1.0 - formation.z) * 0.18;
|
|
1321
|
+
return clamp((shaped - erosion) / (1.0 - erosion), 0.0, 1.0);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
fn cloud_analytic_density(position: vec3<f32>) -> f32 {
|
|
1325
|
+
let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
|
|
1326
|
+
if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
|
|
1327
|
+
let advected = position + cloud.wind.xyz * cloud.field.y;
|
|
1328
|
+
let advectedHeight = height +
|
|
1329
|
+
cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
|
|
1330
|
+
let p = vec3<f32>(
|
|
1331
|
+
advected.x * cloud.layer.z,
|
|
1332
|
+
fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
|
|
1333
|
+
advected.z * cloud.layer.z,
|
|
1334
|
+
);
|
|
1335
|
+
let formation = cloud_formation_field(p, u32(round(cloud.field.w)));
|
|
1336
|
+
return cloud_compose_density(height, formation, cloud.field.x) * cloud.layer.w;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
fn cloud_cache_byte(index: u32) -> f32 {
|
|
1340
|
+
let word = densityCache[index >> 2u];
|
|
1341
|
+
let shift = (index & 3u) * 8u;
|
|
1342
|
+
return f32((word >> shift) & 255u) / 255.0;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
fn cloud_cache_component(plane: u32, index: u32, stride: u32) -> f32 {
|
|
1346
|
+
return cloud_cache_byte(plane * stride + index);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
fn cloud_cache_trilinear(
|
|
1350
|
+
plane: u32,
|
|
1351
|
+
resolution: u32,
|
|
1352
|
+
x0: u32,
|
|
1353
|
+
x1: u32,
|
|
1354
|
+
y0: u32,
|
|
1355
|
+
y1: u32,
|
|
1356
|
+
z0: u32,
|
|
1357
|
+
z1: u32,
|
|
1358
|
+
fx: f32,
|
|
1359
|
+
fy: f32,
|
|
1360
|
+
fz: f32,
|
|
1361
|
+
) -> f32 {
|
|
1362
|
+
let stride = resolution * resolution * resolution;
|
|
1363
|
+
let layer = resolution * resolution;
|
|
1364
|
+
let c000 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x0, stride);
|
|
1365
|
+
let c100 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x1, stride);
|
|
1366
|
+
let c010 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x0, stride);
|
|
1367
|
+
let c110 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x1, stride);
|
|
1368
|
+
let c001 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x0, stride);
|
|
1369
|
+
let c101 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x1, stride);
|
|
1370
|
+
let c011 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x0, stride);
|
|
1371
|
+
let c111 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x1, stride);
|
|
1372
|
+
let x00 = mix(c000, c100, fx);
|
|
1373
|
+
let x10 = mix(c010, c110, fx);
|
|
1374
|
+
let x01 = mix(c001, c101, fx);
|
|
1375
|
+
let x11 = mix(c011, c111, fx);
|
|
1376
|
+
return mix(mix(x00, x10, fy), mix(x01, x11, fy), fz);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
fn cloud_density(position: vec3<f32>) -> f32 {
|
|
1380
|
+
let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
|
|
1381
|
+
if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
|
|
1382
|
+
let resolution = cloud.integration.x;
|
|
1383
|
+
if (resolution < 4u) { return max(cloud_analytic_density(position), 0.0); }
|
|
1384
|
+
let periodCoordinate = fract((position.xz + cloud.wind.xz * cloud.field.y) * cloud.layer.z);
|
|
1385
|
+
let x = fract(periodCoordinate.x - 0.5 / f32(resolution)) * f32(resolution);
|
|
1386
|
+
let z = fract(periodCoordinate.y - 0.5 / f32(resolution)) * f32(resolution);
|
|
1387
|
+
let advectedHeight = height +
|
|
1388
|
+
cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
|
|
1389
|
+
let y = fract(advectedHeight - 0.5 / f32(resolution)) * f32(resolution);
|
|
1390
|
+
let x0 = u32(floor(x));
|
|
1391
|
+
let y0 = u32(floor(y));
|
|
1392
|
+
let z0 = u32(floor(z));
|
|
1393
|
+
let x1 = (x0 + 1u) % resolution;
|
|
1394
|
+
let y1 = (y0 + 1u) % resolution;
|
|
1395
|
+
let z1 = (z0 + 1u) % resolution;
|
|
1396
|
+
let fx = x - f32(x0);
|
|
1397
|
+
let fy = y - f32(y0);
|
|
1398
|
+
let fz = z - f32(z0);
|
|
1399
|
+
let weather = cloud_cache_trilinear(0u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
|
|
1400
|
+
let body = cloud_cache_trilinear(1u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
|
|
1401
|
+
// Repeat only the independent erosion plane: fine boundary structure with
|
|
1402
|
+
// the same eight packed reads and the same cache allocation.
|
|
1403
|
+
let detail = fract(vec3<f32>(periodCoordinate.x, advectedHeight, periodCoordinate.y) * 3.0 - vec3<f32>(0.5 / f32(resolution))) * f32(resolution);
|
|
1404
|
+
let d0 = vec3<u32>(floor(detail));
|
|
1405
|
+
let d1 = (d0 + vec3<u32>(1u)) % vec3<u32>(resolution);
|
|
1406
|
+
let df = fract(detail);
|
|
1407
|
+
let erosion = cloud_cache_trilinear(2u, resolution, d0.x, d1.x, d0.y, d1.y, d0.z, d1.z, df.x, df.y, df.z);
|
|
1408
|
+
return cloud_compose_density(height, vec3<f32>(weather, body, erosion), cloud.field.x) * cloud.layer.w;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
fn cloud_layer_interval(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> vec2<f32> {
|
|
1412
|
+
if (abs(direction.y) < 0.000001) {
|
|
1413
|
+
if (origin.y < cloud.layer.x || origin.y > cloud.layer.x + cloud.layer.y) {
|
|
1414
|
+
return vec2<f32>(1.0, 0.0);
|
|
1415
|
+
}
|
|
1416
|
+
return vec2<f32>(0.0, maxDistance);
|
|
1417
|
+
}
|
|
1418
|
+
let lower = (cloud.layer.x - origin.y) / direction.y;
|
|
1419
|
+
let upper = (cloud.layer.x + cloud.layer.y - origin.y) / direction.y;
|
|
1420
|
+
return vec2<f32>(
|
|
1421
|
+
max(0.0, min(lower, upper)),
|
|
1422
|
+
min(maxDistance, max(lower, upper)),
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
fn cloud_solar_transmittance(position: vec3<f32>) -> f32 {
|
|
1427
|
+
let sunDirection = normalize(-view.lightDir);
|
|
1428
|
+
let solarDistance = select(cloud.field.z, cloud.shadowProjection.x, cloud.shadowProjection.x > 0.0);
|
|
1429
|
+
let interval = cloud_layer_interval(position, sunDirection, solarDistance);
|
|
1430
|
+
if (interval.y <= interval.x) { return 1.0; }
|
|
1431
|
+
let steps = min(cloud.integration.z, 64u);
|
|
1432
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
1433
|
+
var opticalDepth = 0.0;
|
|
1434
|
+
let solarJitter = cloud_solar_jitter(position);
|
|
1435
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
1436
|
+
if (index >= steps) { break; }
|
|
1437
|
+
let samplePosition = position + sunDirection * (interval.x + (f32(index) + solarJitter) * stepLength);
|
|
1438
|
+
opticalDepth = opticalDepth + cloud_density(samplePosition) * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
1439
|
+
}
|
|
1440
|
+
return exp(-max(opticalDepth, 0.0));
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
fn cloud_henyey_greenstein(cosTheta: f32, g: f32) -> f32 {
|
|
1444
|
+
let denominator = max(0.0001, 1.0 + g * g - 2.0 * g * cosTheta);
|
|
1445
|
+
return (1.0 - g * g) / (denominator * sqrt(denominator)) * 0.0795774715;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
fn cloud_phase(cosTheta: f32) -> f32 {
|
|
1449
|
+
// A bounded dual lobe gives a readable forward highlight and a small
|
|
1450
|
+
// back-scatter rim without another solar march.
|
|
1451
|
+
return cloud_henyey_greenstein(cosTheta, 0.55) * 0.8 +
|
|
1452
|
+
cloud_henyey_greenstein(cosTheta, -0.2) * 0.2;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Reuse the same remaining-path optical depth for two bounded scattering
|
|
1456
|
+
// orders. This adds arithmetic only; no extra density or light-ray samples.
|
|
1457
|
+
fn cloud_incident_light(solarT: f32, cosTheta: f32, height: f32) -> vec3<f32> {
|
|
1458
|
+
let direct = solarT * cloud_phase(cosTheta);
|
|
1459
|
+
let multiple = (0.5 * pow(solarT, 0.5) + 0.25 * pow(solarT, 0.25)) * 0.0795774715;
|
|
1460
|
+
let skyFill = mix(0.025, 0.075, smoothstep(0.1, 0.85, height));
|
|
1461
|
+
// Match volume-integrate: the shared DirectionalLight value uses the
|
|
1462
|
+
// unnormalized phase convention. Convert the normalized HG basis once.
|
|
1463
|
+
return cloud.sunRadiance.xyz *
|
|
1464
|
+
(vec3<f32>((direct + multiple) * 12.5663706144) + vec3<f32>(0.72, 0.84, 1.0) * skyFill);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
fn cloud_ray_jitter(pixel: vec2<f32>) -> f32 {
|
|
1468
|
+
// A stable per-pixel offset removes view-step contour bands without adding
|
|
1469
|
+
// a frame-varying noise source that would fight temporal reprojection.
|
|
1470
|
+
return fract(sin(dot(pixel, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
fn cloud_solar_jitter(position: vec3<f32>) -> f32 {
|
|
1474
|
+
// Dither the light-column samples in world space so low shadow-step
|
|
1475
|
+
// profiles do not paint parallel contour bands across cloud bases.
|
|
1476
|
+
return fract(sin(dot(position.xz, vec2<f32>(19.193, 47.117))) * 15731.743);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
1480
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
1481
|
+
let world = view.inverseViewProj * ndc;
|
|
1482
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
@vertex
|
|
1486
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudViewOut {
|
|
1487
|
+
var positions = array<vec2<f32>, 3>(
|
|
1488
|
+
vec2<f32>(-1.0, -1.0),
|
|
1489
|
+
vec2<f32>(3.0, -1.0),
|
|
1490
|
+
vec2<f32>(-1.0, 3.0),
|
|
1491
|
+
);
|
|
1492
|
+
var output: CloudViewOut;
|
|
1493
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
1494
|
+
// WebGPU clip-space Y grows upward while sampled framebuffer rows and the
|
|
1495
|
+
// depth texture use a top-left origin. Keep this one top-left UV for color,
|
|
1496
|
+
// depth and inverse-projection reconstruction; flipping only the final
|
|
1497
|
+
// color would leave the camera ray and depth cut mirrored.
|
|
1498
|
+
output.uv = vec2<f32>(
|
|
1499
|
+
positions[index].x * 0.5 + 0.5,
|
|
1500
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
1501
|
+
);
|
|
1502
|
+
return output;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
@fragment
|
|
1506
|
+
fn fs_main(input: CloudViewOut) -> @location(0) vec4<f32> {
|
|
1507
|
+
// The shadow producer uses this same production density/cache contract but
|
|
1508
|
+
// rasterizes a camera-independent, texel-snapped light-space map. It does
|
|
1509
|
+
// not consume scene colour; the generic fullscreen host still supplies the
|
|
1510
|
+
// input binding to keep the feature binding ABI single and validated.
|
|
1511
|
+
if (cloud.integration.w == 1u) {
|
|
1512
|
+
let projection = cloud.shadowProjection;
|
|
1513
|
+
if (projection.y < 0.5 || projection.z > 0.5 || projection.x <= 0.0) {
|
|
1514
|
+
return vec4<f32>(1.0);
|
|
1515
|
+
}
|
|
1516
|
+
let offset = (input.uv - vec2<f32>(0.5)) * projection.x;
|
|
1517
|
+
let receiver = cloud.shadowOrigin.xyz +
|
|
1518
|
+
cloud.shadowRight.xyz * offset.x + cloud.shadowUp.xyz * offset.y;
|
|
1519
|
+
let sunDirection = normalize(-view.lightDir);
|
|
1520
|
+
let interval = cloud_layer_interval(receiver, sunDirection, projection.x);
|
|
1521
|
+
if (interval.y <= interval.x) { return vec4<f32>(1.0); }
|
|
1522
|
+
let steps = min(cloud.integration.z, 64u);
|
|
1523
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
1524
|
+
var opticalDepth = 0.0;
|
|
1525
|
+
var firstHeight = 1.0;
|
|
1526
|
+
var lastHeight = 0.0;
|
|
1527
|
+
var occupied = false;
|
|
1528
|
+
let solarJitter = cloud_solar_jitter(receiver);
|
|
1529
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
1530
|
+
if (index >= steps) { break; }
|
|
1531
|
+
let samplePosition = receiver + sunDirection *
|
|
1532
|
+
(interval.x + (f32(index) + solarJitter) * stepLength);
|
|
1533
|
+
let localDensity = cloud_density(samplePosition);
|
|
1534
|
+
opticalDepth = opticalDepth + localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
1535
|
+
if (localDensity > 0.0001) {
|
|
1536
|
+
let sampleHeight = clamp(
|
|
1537
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
|
|
1538
|
+
0.0,
|
|
1539
|
+
1.0,
|
|
1540
|
+
);
|
|
1541
|
+
if (!occupied) { firstHeight = sampleHeight; }
|
|
1542
|
+
lastHeight = sampleHeight;
|
|
1543
|
+
occupied = true;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
let transmittance = exp(-max(opticalDepth, 0.0));
|
|
1547
|
+
// R stores full-column transmittance. G/B preserve the first/last
|
|
1548
|
+
// occupied normalized layer heights so interior samples can estimate the
|
|
1549
|
+
// remaining segment without pretending optical depth is uniform through
|
|
1550
|
+
// the whole layer. A stores the integrated optical depth and distinguishes
|
|
1551
|
+
// an empty projected column from a valid, nearly-clear one.
|
|
1552
|
+
return vec4<f32>(
|
|
1553
|
+
transmittance,
|
|
1554
|
+
select(1.0, firstHeight, occupied),
|
|
1555
|
+
select(0.0, lastHeight, occupied),
|
|
1556
|
+
select(0.0, opticalDepth, occupied),
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
// The shadow producer branch above is per-fragment (its light-space
|
|
1560
|
+
// interval depends on the rasterized receiver), so derivative-based
|
|
1561
|
+
// sampling here would violate WGSL uniform-control-flow rules. The cloud
|
|
1562
|
+
// scene path already owns an explicit level-0 sampler contract.
|
|
1563
|
+
// Keep point semantics for the current full-resolution scene path. The
|
|
1564
|
+
// half-resolution transport contract is introduced with its own resolve
|
|
1565
|
+
// shader so this path retains the established depth convention.
|
|
1566
|
+
let depthPixel = vec2<i32>(input.position.xy);
|
|
1567
|
+
let depth = textureLoad(sceneDepth, depthPixel, 0);
|
|
1568
|
+
let farWorld = reconstruct_world(input.uv, 1.0);
|
|
1569
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
|
|
1570
|
+
let ray = normalize(farWorld - view.cameraPos);
|
|
1571
|
+
let rayJitter = cloud_ray_jitter(input.position.xy);
|
|
1572
|
+
let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
|
|
1573
|
+
let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
|
|
1574
|
+
|
|
1575
|
+
// The depth producer shares the same camera ray, interval and density
|
|
1576
|
+
// contract as the display path. It writes a cloud representative world
|
|
1577
|
+
// position and a validity bit so history remains comparable after camera
|
|
1578
|
+
// translation or rotation and never uses an opaque scene distance as a
|
|
1579
|
+
// proxy for a participating medium.
|
|
1580
|
+
if (cloud.integration.w == 2u) {
|
|
1581
|
+
if (interval.y <= interval.x) { return vec4<f32>(sceneWorld, 0.0); }
|
|
1582
|
+
let depthSteps = min(cloud.integration.y, 64u);
|
|
1583
|
+
let depthStepLength = (interval.y - interval.x) / max(1.0, f32(depthSteps));
|
|
1584
|
+
var depthTransmittance = 1.0;
|
|
1585
|
+
var weightedDistance = 0.0;
|
|
1586
|
+
var opticalWeight = 0.0;
|
|
1587
|
+
for (var depthIndex = 0u; depthIndex < 64u; depthIndex = depthIndex + 1u) {
|
|
1588
|
+
if (depthIndex >= depthSteps) { break; }
|
|
1589
|
+
let sampleDistance = interval.x + (f32(depthIndex) + rayJitter) * depthStepLength;
|
|
1590
|
+
let samplePosition = view.cameraPos + ray * sampleDistance;
|
|
1591
|
+
let localDensity = cloud_density(samplePosition);
|
|
1592
|
+
let localTransmittance = exp(-max(localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * depthStepLength, 0.0));
|
|
1593
|
+
let weight = depthTransmittance * (1.0 - localTransmittance);
|
|
1594
|
+
weightedDistance = weightedDistance + sampleDistance * weight;
|
|
1595
|
+
opticalWeight = opticalWeight + weight;
|
|
1596
|
+
depthTransmittance = depthTransmittance * localTransmittance;
|
|
1597
|
+
}
|
|
1598
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
1599
|
+
let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
|
|
1600
|
+
let representativeWorld = view.cameraPos + ray * representativeDistance;
|
|
1601
|
+
return vec4<f32>(representativeWorld, select(0.0, 1.0, hasCloud));
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
|
|
1605
|
+
if (interval.y <= interval.x) { return scene; }
|
|
1606
|
+
let steps = min(cloud.integration.y, 64u);
|
|
1607
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
1608
|
+
var transmittance = 1.0;
|
|
1609
|
+
var scattering = vec3<f32>(0.0);
|
|
1610
|
+
var weightedDistance = 0.0;
|
|
1611
|
+
var opticalWeight = 0.0;
|
|
1612
|
+
let sunDirection = normalize(-view.lightDir);
|
|
1613
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
1614
|
+
if (index >= steps) { break; }
|
|
1615
|
+
let distance = interval.x + (f32(index) + rayJitter) * stepLength;
|
|
1616
|
+
let samplePosition = view.cameraPos + ray * distance;
|
|
1617
|
+
let localDensity = cloud_density(samplePosition);
|
|
1618
|
+
let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
1619
|
+
let localTransmittance = exp(-max(extinction, 0.0));
|
|
1620
|
+
let depthWeight = transmittance * (1.0 - localTransmittance);
|
|
1621
|
+
weightedDistance = weightedDistance + distance * depthWeight;
|
|
1622
|
+
opticalWeight = opticalWeight + depthWeight;
|
|
1623
|
+
// Empty samples are common in the separated-cloud field. Avoid a nested
|
|
1624
|
+
// solar march for them and stop once the camera path is effectively opaque.
|
|
1625
|
+
if (localDensity > 0.0001 && transmittance > 0.01) {
|
|
1626
|
+
let solarTransmittance = cloud_solar_transmittance(samplePosition);
|
|
1627
|
+
let cloudHeight = clamp(
|
|
1628
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
|
|
1629
|
+
);
|
|
1630
|
+
scattering = scattering + cloud_incident_light(
|
|
1631
|
+
solarTransmittance, dot(ray, sunDirection), cloudHeight,
|
|
1632
|
+
) * depthWeight;
|
|
1633
|
+
}
|
|
1634
|
+
transmittance = transmittance * localTransmittance;
|
|
1635
|
+
if (transmittance < 0.01) { break; }
|
|
1636
|
+
}
|
|
1637
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
1638
|
+
let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
|
|
1639
|
+
let representativeWorld = view.cameraPos + ray * representativeDistance;
|
|
1640
|
+
// Wind is an advective coordinate change. Reproject the cloud point into
|
|
1641
|
+
// the previous frame's world before applying the previous camera matrix;
|
|
1642
|
+
// authored field values remain immutable.
|
|
1643
|
+
let windDelta = cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w);
|
|
1644
|
+
// cloud_density evaluates the authored field at position + wind*time.
|
|
1645
|
+
// Keeping the same field sample in the previous frame therefore advances
|
|
1646
|
+
// the previous world point by the elapsed wind delta as well.
|
|
1647
|
+
let previousCloudWorld = representativeWorld + windDelta;
|
|
1648
|
+
let previousCloudClip = view.temporalPreviousViewProj * vec4<f32>(previousCloudWorld, 1.0);
|
|
1649
|
+
let previousCloudNdc = previousCloudClip.xyz / max(abs(previousCloudClip.w), 0.00001);
|
|
1650
|
+
let previousCloudUv = vec2<f32>(
|
|
1651
|
+
previousCloudNdc.x * 0.5 + 0.5,
|
|
1652
|
+
1.0 - (previousCloudNdc.y * 0.5 + 0.5),
|
|
1653
|
+
);
|
|
1654
|
+
let cloudInBounds = all(previousCloudUv >= vec2<f32>(0.0)) && all(previousCloudUv <= vec2<f32>(1.0));
|
|
1655
|
+
let cloudHistoryUv = clamp(previousCloudUv, vec2<f32>(0.0), vec2<f32>(1.0));
|
|
1656
|
+
let cloudHistoryDepthSample = textureSampleLevel(previousDepth, depthSampler, cloudHistoryUv, 0.0);
|
|
1657
|
+
let cloudHistoryWorld = cloudHistoryDepthSample.xyz;
|
|
1658
|
+
let previous = textureSampleLevel(previousRadiance, sceneSampler, cloudHistoryUv, 0.0);
|
|
1659
|
+
let previousTransport = textureSampleLevel(
|
|
1660
|
+
previousTransmittance,
|
|
1661
|
+
sceneSampler,
|
|
1662
|
+
cloudHistoryUv,
|
|
1663
|
+
0.0,
|
|
1664
|
+
);
|
|
1665
|
+
let worldTolerance = max(0.05, length(previousCloudWorld - view.cameraPos) * 0.02);
|
|
1666
|
+
let depthAccepted = cloudHistoryDepthSample.a > 0.5 &&
|
|
1667
|
+
distance(cloudHistoryWorld, previousCloudWorld) <= worldTolerance;
|
|
1668
|
+
let accepted =
|
|
1669
|
+
hasCloud && cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && cloudInBounds &&
|
|
1670
|
+
depthAccepted && previousTransport.a > 0.0;
|
|
1671
|
+
let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.95), accepted);
|
|
1672
|
+
let cloudRadiance = mix(scattering, previous.rgb, historyWeight);
|
|
1673
|
+
// Preserve scene-linear HDR output for the downstream material lane. The
|
|
1674
|
+
// history owner receives cloudRadiance separately through the history pass.
|
|
1675
|
+
return vec4<f32>(scene.rgb * transmittance + cloudRadiance, transmittance);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// The light-space shadow producer already paid for a bounded column integral.
|
|
1679
|
+
// Reuse that texel for camera samples instead of starting a full solar march
|
|
1680
|
+
// for every occupied camera step. Outside the occupied interval the cached
|
|
1681
|
+
// full-column transmittance is exact for the projected column. Inside the
|
|
1682
|
+
// interval, the cache supplies the stable projection/bounds and a short
|
|
1683
|
+
// residual march resolves the non-uniform density that a linear optical-depth
|
|
1684
|
+
// interpolation would lose. Invalid/out-of-range projections retain the
|
|
1685
|
+
// analytic path as an explicit fallback.
|
|
1686
|
+
fn cloud_solar_cached_transmittance(position: vec3<f32>) -> f32 {
|
|
1687
|
+
let projection = cloud.shadowProjection;
|
|
1688
|
+
if (projection.y < 0.5 || projection.x <= 0.0) {
|
|
1689
|
+
return cloud_solar_transmittance(position);
|
|
1690
|
+
}
|
|
1691
|
+
let relative = position - cloud.shadowOrigin.xyz;
|
|
1692
|
+
let uv = vec2<f32>(
|
|
1693
|
+
dot(relative, cloud.shadowRight.xyz) / projection.x + 0.5,
|
|
1694
|
+
dot(relative, cloud.shadowUp.xyz) / projection.x + 0.5,
|
|
1695
|
+
);
|
|
1696
|
+
if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
|
|
1697
|
+
return cloud_solar_transmittance(position);
|
|
1698
|
+
}
|
|
1699
|
+
let cachedShadow = textureSampleLevel(previousRadiance, sceneSampler, uv, 0.0);
|
|
1700
|
+
let cachedOpticalDepth = max(cachedShadow.a, 0.0);
|
|
1701
|
+
let layerHeight = clamp(
|
|
1702
|
+
(position.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
|
|
1703
|
+
0.0,
|
|
1704
|
+
1.0,
|
|
1705
|
+
);
|
|
1706
|
+
if (cachedOpticalDepth <= 0.0001) { return 1.0; }
|
|
1707
|
+
let firstHeight = clamp(cachedShadow.g, 0.0, 1.0);
|
|
1708
|
+
let lastHeight = clamp(cachedShadow.b, 0.0, 1.0);
|
|
1709
|
+
let occupiedMin = min(firstHeight, lastHeight);
|
|
1710
|
+
let occupiedMax = max(firstHeight, lastHeight);
|
|
1711
|
+
let sunDirection = normalize(-view.lightDir);
|
|
1712
|
+
// G/B are ordered along the light ray, not necessarily bottom-to-top. Only
|
|
1713
|
+
// use the cached full/empty fast paths when the light has enough vertical
|
|
1714
|
+
// component to make that ordering meaningful; grazing light stays on the
|
|
1715
|
+
// residual path so a low-sun column cannot invert the cache bounds.
|
|
1716
|
+
if (abs(sunDirection.y) > 0.2) {
|
|
1717
|
+
if (sunDirection.y > 0.0) {
|
|
1718
|
+
if (layerHeight <= occupiedMin) { return exp(-cachedOpticalDepth); }
|
|
1719
|
+
if (layerHeight >= occupiedMax) { return 1.0; }
|
|
1720
|
+
} else {
|
|
1721
|
+
if (layerHeight >= occupiedMax) { return exp(-cachedOpticalDepth); }
|
|
1722
|
+
if (layerHeight <= occupiedMin) { return 1.0; }
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// A short residual march is only paid for samples inside the projected
|
|
1727
|
+
// cloud span. Vertical light uses twelve taps, oblique light sixteen, and
|
|
1728
|
+
// grazing light up to thirty-two because the same layer projects across a
|
|
1729
|
+
// much longer solar segment. This removes the large error caused by
|
|
1730
|
+
// assuming optical depth is uniform between the first and last occupied
|
|
1731
|
+
// heights while keeping the normal camera path far below a second full
|
|
1732
|
+
// solar integration.
|
|
1733
|
+
let solarDistance = select(cloud.field.z, projection.x, projection.x > 0.0);
|
|
1734
|
+
let interval = cloud_layer_interval(position, sunDirection, solarDistance);
|
|
1735
|
+
if (interval.y <= interval.x) { return 1.0; }
|
|
1736
|
+
let absLightY = abs(sunDirection.y);
|
|
1737
|
+
let directionBudget = select(
|
|
1738
|
+
32u,
|
|
1739
|
+
select(16u, 12u, absLightY > 0.5),
|
|
1740
|
+
absLightY > 0.2,
|
|
1741
|
+
);
|
|
1742
|
+
let residualSteps = min(directionBudget, max(8u, cloud.integration.z * 3u));
|
|
1743
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(residualSteps));
|
|
1744
|
+
var opticalDepth = 0.0;
|
|
1745
|
+
let solarJitter = cloud_solar_jitter(position);
|
|
1746
|
+
for (var index = 0u; index < 32u; index = index + 1u) {
|
|
1747
|
+
if (index >= residualSteps) { break; }
|
|
1748
|
+
let samplePosition = position + sunDirection *
|
|
1749
|
+
(interval.x + (f32(index) + solarJitter) * stepLength);
|
|
1750
|
+
opticalDepth = opticalDepth + cloud_density(samplePosition) *
|
|
1751
|
+
${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
1752
|
+
}
|
|
1753
|
+
return exp(-max(opticalDepth, 0.0));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
struct CloudTransportOutput {
|
|
1757
|
+
@location(0) radiance: vec4<f32>,
|
|
1758
|
+
@location(1) transmittance: vec4<f32>,
|
|
1759
|
+
@location(2) depth: vec4<f32>,
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
// One half-resolution transport pass writes all three reusable cloud fields.
|
|
1763
|
+
// The full-resolution resolve below reconstructs colour and applies temporal
|
|
1764
|
+
// history; no second full raymarch exists solely to obtain cloud depth.
|
|
1765
|
+
@fragment
|
|
1766
|
+
fn fs_transport(input: CloudViewOut) -> CloudTransportOutput {
|
|
1767
|
+
let depth = textureSampleLevel(sceneDepth, depthSampler, input.uv, 0u);
|
|
1768
|
+
let farWorld = reconstruct_world(input.uv, 1.0);
|
|
1769
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
|
|
1770
|
+
let ray = normalize(farWorld - view.cameraPos);
|
|
1771
|
+
let rayJitter = cloud_ray_jitter(input.position.xy);
|
|
1772
|
+
let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
|
|
1773
|
+
let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
|
|
1774
|
+
var output: CloudTransportOutput;
|
|
1775
|
+
output.radiance = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
1776
|
+
output.transmittance = vec4<f32>(1.0, 1.0, 1.0, 1.0);
|
|
1777
|
+
output.depth = vec4<f32>(sceneWorld, 0.0);
|
|
1778
|
+
if (interval.y <= interval.x) { return output; }
|
|
1779
|
+
|
|
1780
|
+
let steps = min(cloud.integration.y, 64u);
|
|
1781
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
1782
|
+
var transmittance = 1.0;
|
|
1783
|
+
var scattering = vec3<f32>(0.0);
|
|
1784
|
+
var weightedDistance = 0.0;
|
|
1785
|
+
var opticalWeight = 0.0;
|
|
1786
|
+
let sunDirection = normalize(-view.lightDir);
|
|
1787
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
1788
|
+
if (index >= steps) { break; }
|
|
1789
|
+
let distance = interval.x + (f32(index) + rayJitter) * stepLength;
|
|
1790
|
+
let samplePosition = view.cameraPos + ray * distance;
|
|
1791
|
+
let localDensity = cloud_density(samplePosition);
|
|
1792
|
+
let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
1793
|
+
let localTransmittance = exp(-max(extinction, 0.0));
|
|
1794
|
+
let depthWeight = transmittance * (1.0 - localTransmittance);
|
|
1795
|
+
weightedDistance = weightedDistance + distance * depthWeight;
|
|
1796
|
+
opticalWeight = opticalWeight + depthWeight;
|
|
1797
|
+
if (localDensity > 0.0001 && transmittance > 0.01) {
|
|
1798
|
+
let solarTransmittance = cloud_solar_cached_transmittance(samplePosition);
|
|
1799
|
+
let cloudHeight = clamp(
|
|
1800
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
|
|
1801
|
+
);
|
|
1802
|
+
scattering = scattering + cloud_incident_light(
|
|
1803
|
+
solarTransmittance, dot(ray, sunDirection), cloudHeight,
|
|
1804
|
+
) * depthWeight;
|
|
1805
|
+
}
|
|
1806
|
+
transmittance = transmittance * localTransmittance;
|
|
1807
|
+
if (transmittance < 0.01) { break; }
|
|
1808
|
+
}
|
|
1809
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
1810
|
+
let representativeDistance = select(sceneDistance, weightedDistance / max(opticalWeight, 0.00001), hasCloud);
|
|
1811
|
+
output.radiance = vec4<f32>(scattering, select(0.0, 1.0, hasCloud));
|
|
1812
|
+
output.transmittance = vec4<f32>(transmittance, transmittance, transmittance, 1.0);
|
|
1813
|
+
output.depth = vec4<f32>(view.cameraPos + ray * representativeDistance, select(0.0, 1.0, hasCloud));
|
|
1814
|
+
return output;
|
|
1815
|
+
}
|
|
1816
|
+
`
|
|
1817
|
+
);
|
|
1818
|
+
var CLOUD_TRANSPORT_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL;
|
|
1819
|
+
var CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL.replace(
|
|
1820
|
+
"cloud_solar_cached_transmittance(samplePosition)",
|
|
1821
|
+
"cloud_solar_transmittance(samplePosition)"
|
|
1822
|
+
);
|
|
1823
|
+
var CLOUD_HISTORY_FULLSCREEN_WGSL = (
|
|
1824
|
+
/* wgsl */
|
|
1825
|
+
`
|
|
1826
|
+
struct CloudCameraView {
|
|
1827
|
+
worldViewProj: mat4x4<f32>,
|
|
1828
|
+
lightDir: vec3<f32>,
|
|
1829
|
+
lightColor: vec3<f32>,
|
|
1830
|
+
cameraPos: vec3<f32>,
|
|
1831
|
+
lightViewProj_A: mat4x4<f32>,
|
|
1832
|
+
inverseViewProj: mat4x4<f32>,
|
|
1833
|
+
lightViewProj_B: mat4x4<f32>,
|
|
1834
|
+
lightViewProj_C: mat4x4<f32>,
|
|
1835
|
+
lightViewProj_D: mat4x4<f32>,
|
|
1836
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
1837
|
+
cascadeCount: f32,
|
|
1838
|
+
cascadeBlend: f32,
|
|
1839
|
+
depthBias: f32,
|
|
1840
|
+
normalBias: f32,
|
|
1841
|
+
directionalShadowFilter: vec4<f32>,
|
|
1842
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
1843
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
1844
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
1845
|
+
temporalProjection: vec4<f32>,
|
|
1846
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
1847
|
+
ssrParams: vec4<f32>,
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
struct CloudHistoryParams {
|
|
1851
|
+
temporal: vec4<f32>, // valid, reset, historyWeight, reserved
|
|
1852
|
+
windTime: vec4<f32>, // wind.xyz, elapsed seconds since previous submit
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
1856
|
+
@group(1) @binding(0) var currentRadiance: texture_2d<f32>;
|
|
1857
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
1858
|
+
@group(1) @binding(2) var<uniform> params: CloudHistoryParams;
|
|
1859
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
1860
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
1861
|
+
@group(1) @binding(5) var currentCloudDepth: texture_2d<f32>;
|
|
1862
|
+
@group(1) @binding(6) var sceneBackground: texture_2d<f32>;
|
|
1863
|
+
|
|
1864
|
+
struct CloudHistoryInput {
|
|
1865
|
+
@builtin(position) position: vec4<f32>,
|
|
1866
|
+
@location(0) uv: vec2<f32>,
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
struct CloudHistoryOutput {
|
|
1870
|
+
@location(0) radiance: vec4<f32>,
|
|
1871
|
+
@location(1) transmittance: vec4<f32>,
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
1875
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
1876
|
+
let world = view.inverseViewProj * ndc;
|
|
1877
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
@vertex
|
|
1881
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudHistoryInput {
|
|
1882
|
+
var positions = array<vec2<f32>, 3>(
|
|
1883
|
+
vec2<f32>(-1.0, -1.0),
|
|
1884
|
+
vec2<f32>(3.0, -1.0),
|
|
1885
|
+
vec2<f32>(-1.0, 3.0),
|
|
1886
|
+
);
|
|
1887
|
+
var output: CloudHistoryInput;
|
|
1888
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
1889
|
+
output.uv = vec2<f32>(
|
|
1890
|
+
positions[index].x * 0.5 + 0.5,
|
|
1891
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
1892
|
+
);
|
|
1893
|
+
return output;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
@fragment
|
|
1897
|
+
fn fs_main(input: CloudHistoryInput) -> CloudHistoryOutput {
|
|
1898
|
+
let current = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
|
|
1899
|
+
let background = textureSampleLevel(sceneBackground, sceneSampler, input.uv, 0.0);
|
|
1900
|
+
let cloudDepth = textureSampleLevel(currentCloudDepth, depthSampler, input.uv, 0.0);
|
|
1901
|
+
let currentValid = cloudDepth.a > 0.5;
|
|
1902
|
+
// currentRadiance is the scene-linear cloud composite. Remove the
|
|
1903
|
+
// unchanged scene background before writing history so the persistent owner
|
|
1904
|
+
// stores raw cloud radiance only. The view pass is the sole temporal blend
|
|
1905
|
+
// owner; blending here as well would accumulate history twice per frame.
|
|
1906
|
+
let currentCloudRadiance = max(current.rgb - background.rgb * current.a, vec3<f32>(0.0));
|
|
1907
|
+
let currentTransmittance = vec4<f32>(current.a, current.a, current.a, 1.0);
|
|
1908
|
+
var output: CloudHistoryOutput;
|
|
1909
|
+
output.radiance = vec4<f32>(currentCloudRadiance, select(0.0, 1.0, currentValid));
|
|
1910
|
+
output.transmittance = currentTransmittance;
|
|
1911
|
+
return output;
|
|
1912
|
+
}
|
|
1913
|
+
`
|
|
1914
|
+
);
|
|
1915
|
+
var CLOUD_RESOLVE_FULLSCREEN_WGSL = (
|
|
1916
|
+
/* wgsl */
|
|
1917
|
+
`
|
|
1918
|
+
struct CloudCameraView {
|
|
1919
|
+
worldViewProj: mat4x4<f32>,
|
|
1920
|
+
lightDir: vec3<f32>,
|
|
1921
|
+
lightColor: vec3<f32>,
|
|
1922
|
+
cameraPos: vec3<f32>,
|
|
1923
|
+
lightViewProj_A: mat4x4<f32>,
|
|
1924
|
+
inverseViewProj: mat4x4<f32>,
|
|
1925
|
+
lightViewProj_B: mat4x4<f32>,
|
|
1926
|
+
lightViewProj_C: mat4x4<f32>,
|
|
1927
|
+
lightViewProj_D: mat4x4<f32>,
|
|
1928
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
1929
|
+
cascadeCount: f32,
|
|
1930
|
+
cascadeBlend: f32,
|
|
1931
|
+
depthBias: f32,
|
|
1932
|
+
normalBias: f32,
|
|
1933
|
+
directionalShadowFilter: vec4<f32>,
|
|
1934
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
1935
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
1936
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
1937
|
+
temporalProjection: vec4<f32>,
|
|
1938
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
1939
|
+
ssrParams: vec4<f32>,
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
struct CloudResolveParams {
|
|
1943
|
+
layer: vec4<f32>,
|
|
1944
|
+
field: vec4<f32>,
|
|
1945
|
+
wind: vec4<f32>,
|
|
1946
|
+
sunDirection: vec4<f32>,
|
|
1947
|
+
sunRadiance: vec4<f32>,
|
|
1948
|
+
integration: vec4<u32>,
|
|
1949
|
+
shadowOrigin: vec4<f32>,
|
|
1950
|
+
shadowRight: vec4<f32>,
|
|
1951
|
+
shadowUp: vec4<f32>,
|
|
1952
|
+
shadowProjection: vec4<f32>,
|
|
1953
|
+
temporal: vec4<f32>,
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
1957
|
+
@group(1) @binding(0) var sceneColor: texture_2d<f32>;
|
|
1958
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
1959
|
+
@group(1) @binding(2) var<uniform> cloud: CloudResolveParams;
|
|
1960
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
1961
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
1962
|
+
@group(1) @binding(5) var currentRadiance: texture_2d<f32>;
|
|
1963
|
+
@group(1) @binding(6) var currentTransmittance: texture_2d<f32>;
|
|
1964
|
+
@group(1) @binding(7) var currentDepth: texture_2d<f32>;
|
|
1965
|
+
@group(1) @binding(8) var previousRadiance: texture_2d<f32>;
|
|
1966
|
+
@group(1) @binding(9) var previousTransmittance: texture_2d<f32>;
|
|
1967
|
+
@group(1) @binding(10) var previousDepth: texture_2d<f32>;
|
|
1968
|
+
|
|
1969
|
+
struct CloudResolveInput {
|
|
1970
|
+
@builtin(position) position: vec4<f32>,
|
|
1971
|
+
@location(0) uv: vec2<f32>,
|
|
1972
|
+
};
|
|
1973
|
+
|
|
1974
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
1975
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
1976
|
+
let world = view.inverseViewProj * ndc;
|
|
1977
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
@vertex
|
|
1981
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudResolveInput {
|
|
1982
|
+
var positions = array<vec2<f32>, 3>(
|
|
1983
|
+
vec2<f32>(-1.0, -1.0),
|
|
1984
|
+
vec2<f32>(3.0, -1.0),
|
|
1985
|
+
vec2<f32>(-1.0, 3.0),
|
|
1986
|
+
);
|
|
1987
|
+
var output: CloudResolveInput;
|
|
1988
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
1989
|
+
output.uv = vec2<f32>(
|
|
1990
|
+
positions[index].x * 0.5 + 0.5,
|
|
1991
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
1992
|
+
);
|
|
1993
|
+
return output;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
@fragment
|
|
1997
|
+
fn fs_main(input: CloudResolveInput) -> @location(0) vec4<f32> {
|
|
1998
|
+
let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
|
|
1999
|
+
// The resolve owns the full-resolution foreground test. Reject the current
|
|
2000
|
+
// low-resolution transport before any history or radiance is composited;
|
|
2001
|
+
// gating history alone still lets a newly sampled cloud bleed over geometry.
|
|
2002
|
+
let sceneDepthValue = textureLoad(sceneDepth, vec2<i32>(input.position.xy), 0);
|
|
2003
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(sceneDepthValue, 0.0, 1.0));
|
|
2004
|
+
let sceneDistance = length(sceneWorld - view.cameraPos);
|
|
2005
|
+
let transport = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
|
|
2006
|
+
let transportT = textureSampleLevel(currentTransmittance, sceneSampler, input.uv, 0.0);
|
|
2007
|
+
// The transport depth is an rgba16float color attachment, not the scene's
|
|
2008
|
+
// depth texture. Sample it with the filtering color sampler; binding the
|
|
2009
|
+
// non-filtering scene-depth sampler here makes the shader contract depend on
|
|
2010
|
+
// an incompatible sampler/texture pair on strict WebGPU implementations.
|
|
2011
|
+
let cloudDepth = textureSampleLevel(currentDepth, sceneSampler, input.uv, 0.0);
|
|
2012
|
+
let currentValid = transport.a > 0.5 && cloudDepth.a > 0.5;
|
|
2013
|
+
let cloudDistance = length(cloudDepth.xyz - view.cameraPos);
|
|
2014
|
+
let foregroundAccepted = sceneDepthValue >= 0.999999 ||
|
|
2015
|
+
cloudDistance <= sceneDistance + max(0.05, sceneDistance * 0.01);
|
|
2016
|
+
if (!currentValid || !foregroundAccepted) { return scene; }
|
|
2017
|
+
|
|
2018
|
+
let previousClip = view.temporalPreviousViewProj * vec4<f32>(
|
|
2019
|
+
cloudDepth.xyz + cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w),
|
|
2020
|
+
1.0,
|
|
2021
|
+
);
|
|
2022
|
+
let previousNdc = previousClip.xyz / max(abs(previousClip.w), 0.00001);
|
|
2023
|
+
let previousUv = vec2<f32>(
|
|
2024
|
+
previousNdc.x * 0.5 + 0.5,
|
|
2025
|
+
1.0 - (previousNdc.y * 0.5 + 0.5),
|
|
2026
|
+
);
|
|
2027
|
+
let inBounds = all(previousUv >= vec2<f32>(0.0)) && all(previousUv <= vec2<f32>(1.0));
|
|
2028
|
+
let historyUv = clamp(previousUv, vec2<f32>(0.0), vec2<f32>(1.0));
|
|
2029
|
+
let historyDepth = textureSampleLevel(previousDepth, sceneSampler, historyUv, 0.0);
|
|
2030
|
+
let previous = textureSampleLevel(previousRadiance, sceneSampler, historyUv, 0.0);
|
|
2031
|
+
let previousTransport = textureSampleLevel(previousTransmittance, sceneSampler, historyUv, 0.0);
|
|
2032
|
+
let dims = vec2<f32>(textureDimensions(previousRadiance));
|
|
2033
|
+
let texel = 1.0 / max(dims, vec2<f32>(1.0));
|
|
2034
|
+
// Clamp reprojected history against the current low-resolution transport
|
|
2035
|
+
// neighborhood. Using only previous-frame neighbors (and including the
|
|
2036
|
+
// previous center in both extrema) is a no-op and lets stale bright cloud
|
|
2037
|
+
// pixels leak across a newly exposed edge.
|
|
2038
|
+
let c0 = transport.rgb;
|
|
2039
|
+
let c1 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
|
|
2040
|
+
let c2 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
|
|
2041
|
+
let c3 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
|
|
2042
|
+
let c4 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
|
|
2043
|
+
let minRadiance = min(c0, min(min(c1, c2), min(c3, c4)));
|
|
2044
|
+
let maxRadiance = max(c0, max(max(c1, c2), max(c3, c4)));
|
|
2045
|
+
let t1 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
|
|
2046
|
+
let t2 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
|
|
2047
|
+
let t3 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
|
|
2048
|
+
let t4 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
|
|
2049
|
+
let minTransport = min(transportT.r, min(min(t1, t2), min(t3, t4)));
|
|
2050
|
+
let maxTransport = max(transportT.r, max(max(t1, t2), max(t3, t4)));
|
|
2051
|
+
let worldTolerance = max(0.05, distance(cloudDepth.xyz, view.cameraPos) * 0.02);
|
|
2052
|
+
let depthAccepted = historyDepth.a > 0.5 && distance(historyDepth.xyz, cloudDepth.xyz) <= worldTolerance;
|
|
2053
|
+
let accepted = cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && inBounds && depthAccepted;
|
|
2054
|
+
let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.92), accepted);
|
|
2055
|
+
let stableHistory = clamp(previous.rgb, minRadiance, maxRadiance);
|
|
2056
|
+
let stableTransport = clamp(previousTransport.r, minTransport, maxTransport);
|
|
2057
|
+
let radiance = mix(transport.rgb, stableHistory, historyWeight);
|
|
2058
|
+
let transmittance = mix(transportT.r, stableTransport, historyWeight);
|
|
2059
|
+
return vec4<f32>(scene.rgb * transmittance + radiance, transmittance);
|
|
2060
|
+
}
|
|
2061
|
+
`
|
|
2062
|
+
);
|
|
2063
|
+
function makeCloudViewParams(frame, cache, mode = 0) {
|
|
2064
|
+
const params = new ArrayBuffer(CLOUD_VIEW_PARAMS_BYTES);
|
|
2065
|
+
const floats = new Float32Array(params);
|
|
2066
|
+
const integers = new Uint32Array(params);
|
|
2067
|
+
const profile = CLOUD_QUALITY_PROFILES[frame.params?.quality ?? "medium"];
|
|
2068
|
+
const authored = frame.params;
|
|
2069
|
+
floats[0] = authored?.baseHeight ?? 0;
|
|
2070
|
+
floats[1] = authored?.thickness ?? 1;
|
|
2071
|
+
floats[2] = authored?.scale ?? 1;
|
|
2072
|
+
floats[3] = authored?.density ?? 0;
|
|
2073
|
+
floats[4] = authored?.coverage ?? 1;
|
|
2074
|
+
floats[5] = frame.timeSeconds;
|
|
2075
|
+
floats[6] = mode === 1 ? authored?.shadowRange ?? 0 : cloudViewDistanceForQuality(authored?.quality ?? "medium");
|
|
2076
|
+
floats[7] = authored?.seed ?? 0;
|
|
2077
|
+
floats[8] = authored?.wind[0] ?? 0;
|
|
2078
|
+
floats[9] = authored?.wind[1] ?? 0;
|
|
2079
|
+
floats[10] = authored?.wind[2] ?? 0;
|
|
2080
|
+
floats[12] = frame.sunDirection?.[0] ?? 0;
|
|
2081
|
+
floats[13] = frame.sunDirection?.[1] ?? -1;
|
|
2082
|
+
floats[14] = frame.sunDirection?.[2] ?? 0;
|
|
2083
|
+
floats[15] = 0.2;
|
|
2084
|
+
floats[16] = frame.sunRadiance?.[0] ?? 0;
|
|
2085
|
+
floats[17] = frame.sunRadiance?.[1] ?? 0;
|
|
2086
|
+
floats[18] = frame.sunRadiance?.[2] ?? 0;
|
|
2087
|
+
integers[20] = cache.resolution;
|
|
2088
|
+
integers[21] = Math.max(1, Math.min(64, Math.floor(profile.viewSteps)));
|
|
2089
|
+
integers[22] = Math.max(1, Math.min(64, Math.floor(profile.shadowSteps)));
|
|
2090
|
+
integers[23] = mode;
|
|
2091
|
+
const projection = frame.shadowProjection;
|
|
2092
|
+
if (projection !== void 0) {
|
|
2093
|
+
floats[24] = projection.origin[0];
|
|
2094
|
+
floats[25] = projection.origin[1];
|
|
2095
|
+
floats[26] = projection.origin[2];
|
|
2096
|
+
floats[28] = projection.right[0];
|
|
2097
|
+
floats[29] = projection.right[1];
|
|
2098
|
+
floats[30] = projection.right[2];
|
|
2099
|
+
floats[32] = projection.up[0];
|
|
2100
|
+
floats[33] = projection.up[1];
|
|
2101
|
+
floats[34] = projection.up[2];
|
|
2102
|
+
floats[36] = projection.range;
|
|
2103
|
+
floats[37] = projection.lowSun ? 0 : 1;
|
|
2104
|
+
floats[38] = projection.lowSun ? 1 : 0;
|
|
2105
|
+
floats[39] = projection.texelSize;
|
|
2106
|
+
}
|
|
2107
|
+
const temporal = frame.temporal;
|
|
2108
|
+
floats[40] = temporal !== void 0 && temporal.reset === false ? 1 : 0;
|
|
2109
|
+
floats[41] = temporal?.reset === true ? 1 : 0;
|
|
2110
|
+
floats[42] = temporal === void 0 ? 0 : profile.historyWeight;
|
|
2111
|
+
floats[43] = temporal?.history?.temporal.timeSeconds ?? frame.timeSeconds;
|
|
2112
|
+
return new Uint8Array(params);
|
|
2113
|
+
}
|
|
2114
|
+
function planCloudDensity(frame, packedCache, context) {
|
|
2115
|
+
const cacheCount = packedCache.length;
|
|
2116
|
+
const params = new Uint32Array([cacheCount]);
|
|
2117
|
+
const inputTarget = context.targets.find((target) => target.name === "motion-input");
|
|
2118
|
+
const outputTarget = context.targets.find((target) => target.name === "motion-output");
|
|
2119
|
+
const historyRadiancePrevious = context.targets.find(
|
|
2120
|
+
(target) => target.name === "cloud-history-radiance-previous"
|
|
2121
|
+
);
|
|
2122
|
+
const historyTransmittancePrevious = context.targets.find(
|
|
2123
|
+
(target) => target.name === "cloud-history-transmittance-previous"
|
|
2124
|
+
);
|
|
2125
|
+
const historyDepthPrevious = context.targets.find(
|
|
2126
|
+
(target) => target.name === "cloud-history-depth-previous"
|
|
2127
|
+
);
|
|
2128
|
+
const historyRadianceCurrent = context.targets.find(
|
|
2129
|
+
(target) => target.name === "cloud-history-radiance-current"
|
|
2130
|
+
);
|
|
2131
|
+
const historyTransmittanceCurrent = context.targets.find(
|
|
2132
|
+
(target) => target.name === "cloud-history-transmittance-current"
|
|
2133
|
+
);
|
|
2134
|
+
const historyDepthCurrent = context.targets.find(
|
|
2135
|
+
(target) => target.name === "cloud-history-depth-current"
|
|
2136
|
+
);
|
|
2137
|
+
const hasHistoryTargets = historyRadiancePrevious !== void 0 && historyTransmittancePrevious !== void 0 && historyDepthPrevious !== void 0 && historyRadianceCurrent !== void 0 && historyTransmittanceCurrent !== void 0 && historyDepthCurrent !== void 0;
|
|
2138
|
+
const shadowTarget = context.targets.find((target) => target.name === "cloud-shadow");
|
|
2139
|
+
const canShadow = shadowTarget !== void 0 && shadowTarget.sampleCount === 1 && frame.shadowProjection !== void 0 && hasHistoryTargets;
|
|
2140
|
+
const canCompose = inputTarget !== void 0 && outputTarget !== void 0 && inputTarget.sampleCount === 1 && outputTarget.sampleCount === 1 && hasHistoryTargets;
|
|
2141
|
+
const transportReads = ["motion-input", { key: "depth", sampleType: "depth" }];
|
|
2142
|
+
const transportCacheReads = [
|
|
2143
|
+
"motion-input",
|
|
2144
|
+
"cloud-shadow",
|
|
2145
|
+
{ key: "depth", sampleType: "depth" }
|
|
2146
|
+
];
|
|
2147
|
+
const historyReads = [
|
|
2148
|
+
"motion-input",
|
|
2149
|
+
"cloud-history-radiance-previous",
|
|
2150
|
+
"cloud-history-transmittance-previous",
|
|
2151
|
+
"cloud-history-depth-previous",
|
|
2152
|
+
{ key: "depth", sampleType: "depth" }
|
|
2153
|
+
];
|
|
2154
|
+
const historyAdditionalTextures = [
|
|
2155
|
+
"cloud-history-radiance-previous",
|
|
2156
|
+
"cloud-history-transmittance-previous",
|
|
2157
|
+
"cloud-history-depth-previous"
|
|
2158
|
+
];
|
|
2159
|
+
const viewParams = makeCloudViewParams(frame, frame.cache);
|
|
2160
|
+
const shadowParams = canShadow ? makeCloudViewParams(frame, frame.cache, 1) : void 0;
|
|
2161
|
+
const transportUsesShadowCache = canCompose && canShadow;
|
|
2162
|
+
return {
|
|
2163
|
+
resources: [
|
|
2164
|
+
{
|
|
2165
|
+
kind: "compute-program",
|
|
2166
|
+
name: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
2167
|
+
program: {
|
|
2168
|
+
wgsl: CLOUD_DENSITY_COMPUTE_WGSL,
|
|
2169
|
+
entryPoints: ["cloud_density_cache"],
|
|
2170
|
+
bindings: [
|
|
2171
|
+
{
|
|
2172
|
+
label: "forgeax.cloud-layer.cache",
|
|
2173
|
+
entries: [
|
|
2174
|
+
{ binding: 0, visibility: COMPUTE_STAGE, buffer: { type: "read-only-storage" } },
|
|
2175
|
+
{ binding: 1, visibility: COMPUTE_STAGE, buffer: { type: "storage" } },
|
|
2176
|
+
{ binding: 2, visibility: COMPUTE_STAGE, buffer: { type: "uniform" } }
|
|
2177
|
+
]
|
|
2178
|
+
}
|
|
2179
|
+
]
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
kind: "buffer",
|
|
2184
|
+
name: CLOUD_LAYER_DENSITY_CACHE,
|
|
2185
|
+
size: packedCache.byteLength,
|
|
2186
|
+
usage: ["storage", "copy-src"],
|
|
2187
|
+
data: packedCache
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
kind: "buffer",
|
|
2191
|
+
name: CLOUD_LAYER_DENSITY_OUTPUT,
|
|
2192
|
+
size: packedCache.byteLength,
|
|
2193
|
+
usage: ["storage", "copy-src"]
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
kind: "buffer",
|
|
2197
|
+
name: "cloud-layer-density-params",
|
|
2198
|
+
size: params.byteLength,
|
|
2199
|
+
usage: ["uniform"],
|
|
2200
|
+
data: params
|
|
2201
|
+
},
|
|
2202
|
+
{
|
|
2203
|
+
kind: "compute-bindings",
|
|
2204
|
+
name: CLOUD_LAYER_DENSITY_BINDINGS,
|
|
2205
|
+
program: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
2206
|
+
entries: [
|
|
2207
|
+
{ binding: 0, resource: CLOUD_LAYER_DENSITY_CACHE },
|
|
2208
|
+
{ binding: 1, resource: CLOUD_LAYER_DENSITY_OUTPUT },
|
|
2209
|
+
{ binding: 2, resource: "cloud-layer-density-params" }
|
|
2210
|
+
]
|
|
2211
|
+
},
|
|
2212
|
+
{
|
|
2213
|
+
kind: "fullscreen-program",
|
|
2214
|
+
name: "cloud-layer-view",
|
|
2215
|
+
source: CLOUD_VIEW_FULLSCREEN_WGSL,
|
|
2216
|
+
usesView: true,
|
|
2217
|
+
storageBindings: [8],
|
|
2218
|
+
reads: historyReads,
|
|
2219
|
+
params: {
|
|
2220
|
+
byteSize: viewParams.byteLength,
|
|
2221
|
+
defaultValue: viewParams
|
|
2222
|
+
}
|
|
2223
|
+
},
|
|
2224
|
+
...shadowParams === void 0 ? [] : [
|
|
2225
|
+
{
|
|
2226
|
+
kind: "fullscreen-program",
|
|
2227
|
+
name: "cloud-layer-shadow",
|
|
2228
|
+
source: CLOUD_VIEW_FULLSCREEN_WGSL,
|
|
2229
|
+
usesView: true,
|
|
2230
|
+
storageBindings: [8],
|
|
2231
|
+
reads: historyReads,
|
|
2232
|
+
params: {
|
|
2233
|
+
byteSize: shadowParams.byteLength,
|
|
2234
|
+
defaultValue: shadowParams
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
],
|
|
2238
|
+
...canCompose ? [
|
|
2239
|
+
{
|
|
2240
|
+
kind: "fullscreen-program",
|
|
2241
|
+
name: "cloud-layer-transport",
|
|
2242
|
+
source: transportUsesShadowCache ? CLOUD_TRANSPORT_FULLSCREEN_WGSL : CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL,
|
|
2243
|
+
fragmentEntryPoint: "fs_transport",
|
|
2244
|
+
usesView: true,
|
|
2245
|
+
storageBindings: [8],
|
|
2246
|
+
reads: transportUsesShadowCache ? transportCacheReads : transportReads,
|
|
2247
|
+
params: {
|
|
2248
|
+
byteSize: viewParams.byteLength,
|
|
2249
|
+
defaultValue: viewParams
|
|
2250
|
+
}
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
kind: "fullscreen-program",
|
|
2254
|
+
name: "cloud-layer-resolve",
|
|
2255
|
+
source: CLOUD_RESOLVE_FULLSCREEN_WGSL,
|
|
2256
|
+
usesView: true,
|
|
2257
|
+
reads: [
|
|
2258
|
+
"motion-input",
|
|
2259
|
+
"cloud-history-radiance-current",
|
|
2260
|
+
"cloud-history-transmittance-current",
|
|
2261
|
+
"cloud-history-depth-current",
|
|
2262
|
+
"cloud-history-radiance-previous",
|
|
2263
|
+
"cloud-history-transmittance-previous",
|
|
2264
|
+
"cloud-history-depth-previous",
|
|
2265
|
+
{ key: "depth", sampleType: "depth" }
|
|
2266
|
+
],
|
|
2267
|
+
params: {
|
|
2268
|
+
byteSize: viewParams.byteLength,
|
|
2269
|
+
defaultValue: viewParams
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
] : [],
|
|
2273
|
+
...canCompose ? [
|
|
2274
|
+
{
|
|
2275
|
+
kind: "graphics-program",
|
|
2276
|
+
name: "cloud-layer-transport-pipeline",
|
|
2277
|
+
program: {
|
|
2278
|
+
shader: "cloud-layer-transport",
|
|
2279
|
+
vertexLayout: "none",
|
|
2280
|
+
colorFormats: [
|
|
2281
|
+
historyRadianceCurrent.format,
|
|
2282
|
+
historyTransmittanceCurrent.format,
|
|
2283
|
+
historyDepthCurrent.format
|
|
2284
|
+
],
|
|
2285
|
+
sampleCount: 1,
|
|
2286
|
+
renderState: {
|
|
2287
|
+
depthWriteEnabled: false,
|
|
2288
|
+
depthCompare: "always"
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
kind: "graphics-bindings",
|
|
2294
|
+
name: "cloud-layer-transport-view-bindings",
|
|
2295
|
+
program: "cloud-layer-transport-pipeline",
|
|
2296
|
+
values: { group: 0, view: true }
|
|
2297
|
+
},
|
|
2298
|
+
{
|
|
2299
|
+
kind: "graphics-bindings",
|
|
2300
|
+
name: "cloud-layer-transport-bindings",
|
|
2301
|
+
program: "cloud-layer-transport-pipeline",
|
|
2302
|
+
values: {
|
|
2303
|
+
group: 1,
|
|
2304
|
+
fullscreen: true,
|
|
2305
|
+
shader: "cloud-layer-transport",
|
|
2306
|
+
input: "motion-input",
|
|
2307
|
+
depth: "depth",
|
|
2308
|
+
additionalTextures: transportUsesShadowCache ? ["cloud-shadow"] : [],
|
|
2309
|
+
storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT]
|
|
2310
|
+
},
|
|
2311
|
+
logicalTargets: { input: "motion-input" }
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
kind: "graphics-program",
|
|
2315
|
+
name: "cloud-layer-resolve-pipeline",
|
|
2316
|
+
program: {
|
|
2317
|
+
shader: "cloud-layer-resolve",
|
|
2318
|
+
vertexLayout: "none",
|
|
2319
|
+
colorFormats: [outputTarget.format],
|
|
2320
|
+
sampleCount: 1,
|
|
2321
|
+
renderState: {
|
|
2322
|
+
depthWriteEnabled: false,
|
|
2323
|
+
depthCompare: "always"
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
kind: "graphics-bindings",
|
|
2329
|
+
name: "cloud-layer-resolve-view-bindings",
|
|
2330
|
+
program: "cloud-layer-resolve-pipeline",
|
|
2331
|
+
values: { group: 0, view: true }
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
kind: "graphics-bindings",
|
|
2335
|
+
name: "cloud-layer-resolve-bindings",
|
|
2336
|
+
program: "cloud-layer-resolve-pipeline",
|
|
2337
|
+
values: {
|
|
2338
|
+
group: 1,
|
|
2339
|
+
fullscreen: true,
|
|
2340
|
+
shader: "cloud-layer-resolve",
|
|
2341
|
+
input: "motion-input",
|
|
2342
|
+
depth: "depth",
|
|
2343
|
+
additionalTextures: [
|
|
2344
|
+
"cloud-history-radiance-current",
|
|
2345
|
+
"cloud-history-transmittance-current",
|
|
2346
|
+
"cloud-history-depth-current",
|
|
2347
|
+
"cloud-history-radiance-previous",
|
|
2348
|
+
"cloud-history-transmittance-previous",
|
|
2349
|
+
"cloud-history-depth-previous"
|
|
2350
|
+
]
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
] : [],
|
|
2354
|
+
...canShadow ? [
|
|
2355
|
+
{
|
|
2356
|
+
kind: "graphics-program",
|
|
2357
|
+
name: "cloud-layer-shadow-pipeline",
|
|
2358
|
+
program: {
|
|
2359
|
+
shader: "cloud-layer-shadow",
|
|
2360
|
+
vertexLayout: "none",
|
|
2361
|
+
colorFormats: [shadowTarget.format],
|
|
2362
|
+
sampleCount: 1,
|
|
2363
|
+
renderState: {
|
|
2364
|
+
depthWriteEnabled: false,
|
|
2365
|
+
depthCompare: "always",
|
|
2366
|
+
blend: {
|
|
2367
|
+
color: {
|
|
2368
|
+
srcFactor: "one",
|
|
2369
|
+
dstFactor: "zero",
|
|
2370
|
+
operation: "add"
|
|
2371
|
+
},
|
|
2372
|
+
alpha: {
|
|
2373
|
+
srcFactor: "one",
|
|
2374
|
+
dstFactor: "zero",
|
|
2375
|
+
operation: "add"
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
kind: "graphics-bindings",
|
|
2383
|
+
name: "cloud-layer-shadow-view-bindings",
|
|
2384
|
+
program: "cloud-layer-shadow-pipeline",
|
|
2385
|
+
values: { group: 0, view: true }
|
|
2386
|
+
},
|
|
2387
|
+
{
|
|
2388
|
+
kind: "graphics-bindings",
|
|
2389
|
+
name: "cloud-layer-shadow-bindings",
|
|
2390
|
+
program: "cloud-layer-shadow-pipeline",
|
|
2391
|
+
values: {
|
|
2392
|
+
group: 1,
|
|
2393
|
+
fullscreen: true,
|
|
2394
|
+
shader: "cloud-layer-shadow",
|
|
2395
|
+
input: false,
|
|
2396
|
+
depth: "depth",
|
|
2397
|
+
// The shadow branch's WGSL keeps the shared depth binding
|
|
2398
|
+
// shape, but its light-space path never samples scene depth.
|
|
2399
|
+
// Bind the renderer-owned far-depth fallback instead of
|
|
2400
|
+
// introducing a read-before-main graph dependency.
|
|
2401
|
+
depthFallback: true,
|
|
2402
|
+
additionalTextures: historyAdditionalTextures,
|
|
2403
|
+
storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT]
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
] : []
|
|
2407
|
+
],
|
|
2408
|
+
passes: context.caps.compute && context.caps.storageBuffer ? [
|
|
2409
|
+
{
|
|
2410
|
+
kind: "compute",
|
|
2411
|
+
name: "cloud-layer-density-cache",
|
|
2412
|
+
program: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
2413
|
+
bindings: CLOUD_LAYER_DENSITY_BINDINGS,
|
|
2414
|
+
dispatches: [
|
|
2415
|
+
{
|
|
2416
|
+
kind: "direct",
|
|
2417
|
+
entryPoint: "cloud_density_cache",
|
|
2418
|
+
workgroups: [Math.max(1, Math.ceil(cacheCount / CLOUD_WORKGROUP_SIZE))]
|
|
2419
|
+
}
|
|
2420
|
+
]
|
|
2421
|
+
},
|
|
2422
|
+
...canShadow ? [
|
|
2423
|
+
{
|
|
2424
|
+
kind: "raster",
|
|
2425
|
+
name: "cloud-layer-shadow",
|
|
2426
|
+
colorAttachments: [
|
|
2427
|
+
{
|
|
2428
|
+
target: "cloud-shadow",
|
|
2429
|
+
loadOp: "clear",
|
|
2430
|
+
storeOp: "store"
|
|
2431
|
+
}
|
|
2432
|
+
],
|
|
2433
|
+
sampledTargets: [
|
|
2434
|
+
"cloud-history-radiance-previous",
|
|
2435
|
+
"cloud-history-transmittance-previous",
|
|
2436
|
+
"cloud-history-depth-previous"
|
|
2437
|
+
],
|
|
2438
|
+
draws: [
|
|
2439
|
+
{
|
|
2440
|
+
program: "cloud-layer-shadow-pipeline",
|
|
2441
|
+
bindings: [
|
|
2442
|
+
"cloud-layer-shadow-view-bindings",
|
|
2443
|
+
"cloud-layer-shadow-bindings"
|
|
2444
|
+
],
|
|
2445
|
+
vertexData: [],
|
|
2446
|
+
vertexLayout: "none",
|
|
2447
|
+
draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
|
|
2448
|
+
}
|
|
2449
|
+
]
|
|
2450
|
+
}
|
|
2451
|
+
] : [],
|
|
2452
|
+
...canCompose ? [
|
|
2453
|
+
{
|
|
2454
|
+
kind: "raster",
|
|
2455
|
+
name: "cloud-layer-transport",
|
|
2456
|
+
colorAttachments: [
|
|
2457
|
+
{
|
|
2458
|
+
target: "cloud-history-radiance-current",
|
|
2459
|
+
loadOp: "clear",
|
|
2460
|
+
storeOp: "store"
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
target: "cloud-history-transmittance-current",
|
|
2464
|
+
loadOp: "clear",
|
|
2465
|
+
storeOp: "store"
|
|
2466
|
+
},
|
|
2467
|
+
{
|
|
2468
|
+
target: "cloud-history-depth-current",
|
|
2469
|
+
loadOp: "clear",
|
|
2470
|
+
storeOp: "store"
|
|
2471
|
+
}
|
|
2472
|
+
],
|
|
2473
|
+
sampledTargets: [
|
|
2474
|
+
"motion-input",
|
|
2475
|
+
"depth",
|
|
2476
|
+
...transportUsesShadowCache ? ["cloud-shadow"] : []
|
|
2477
|
+
],
|
|
2478
|
+
draws: [
|
|
2479
|
+
{
|
|
2480
|
+
program: "cloud-layer-transport-pipeline",
|
|
2481
|
+
bindings: [
|
|
2482
|
+
"cloud-layer-transport-view-bindings",
|
|
2483
|
+
"cloud-layer-transport-bindings"
|
|
2484
|
+
],
|
|
2485
|
+
vertexData: [],
|
|
2486
|
+
vertexLayout: "none",
|
|
2487
|
+
draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
|
|
2488
|
+
}
|
|
2489
|
+
]
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
kind: "raster",
|
|
2493
|
+
name: "cloud-layer-resolve",
|
|
2494
|
+
colorAttachments: [
|
|
2495
|
+
{
|
|
2496
|
+
target: "motion-output",
|
|
2497
|
+
loadOp: "clear",
|
|
2498
|
+
storeOp: "store"
|
|
2499
|
+
}
|
|
2500
|
+
],
|
|
2501
|
+
sampledTargets: [
|
|
2502
|
+
"motion-input",
|
|
2503
|
+
"depth",
|
|
2504
|
+
"cloud-history-radiance-current",
|
|
2505
|
+
"cloud-history-transmittance-current",
|
|
2506
|
+
"cloud-history-depth-current",
|
|
2507
|
+
"cloud-history-radiance-previous",
|
|
2508
|
+
"cloud-history-transmittance-previous",
|
|
2509
|
+
"cloud-history-depth-previous"
|
|
2510
|
+
],
|
|
2511
|
+
draws: [
|
|
2512
|
+
{
|
|
2513
|
+
program: "cloud-layer-resolve-pipeline",
|
|
2514
|
+
bindings: [
|
|
2515
|
+
"cloud-layer-resolve-view-bindings",
|
|
2516
|
+
"cloud-layer-resolve-bindings"
|
|
2517
|
+
],
|
|
2518
|
+
vertexData: [],
|
|
2519
|
+
vertexLayout: "none",
|
|
2520
|
+
draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
|
|
2521
|
+
}
|
|
2522
|
+
]
|
|
2523
|
+
}
|
|
2524
|
+
] : []
|
|
2525
|
+
] : []
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function createCloudLayerFeature(options = {}) {
|
|
2529
|
+
let cachedSourceKey;
|
|
2530
|
+
let cached;
|
|
2531
|
+
let packedCache;
|
|
2532
|
+
let generation = 0;
|
|
2533
|
+
const histories = new CloudHistoryStore();
|
|
2534
|
+
let temporalResetCount = 0;
|
|
2535
|
+
let lastTemporalReset;
|
|
2536
|
+
return Object.freeze({
|
|
2537
|
+
identity: CLOUD_LAYER_FEATURE_IDENTITY,
|
|
2538
|
+
placement: "scene",
|
|
2539
|
+
requiredCapabilities: ["compute", "storageBuffer", "rgba16floatRenderable"],
|
|
2540
|
+
// Cloud programs are generated by the feature and have no user-authored
|
|
2541
|
+
// manifest entry to await. Use the immediate module path so first-frame
|
|
2542
|
+
// feature admission does not publish a transient pending-shader failure;
|
|
2543
|
+
// pipeline creation remains the validation boundary for this WGSL.
|
|
2544
|
+
shaderModuleMode: "immediate",
|
|
2545
|
+
extract: (context) => {
|
|
2546
|
+
if (options.enabled === false) {
|
|
2547
|
+
histories.reset();
|
|
2548
|
+
return ok({
|
|
2549
|
+
params: void 0,
|
|
2550
|
+
cache: void 0,
|
|
2551
|
+
sourceKey: void 0,
|
|
2552
|
+
timeSeconds: 0,
|
|
2553
|
+
sunDirection: void 0,
|
|
2554
|
+
sunRadiance: void 0,
|
|
2555
|
+
shadowProjection: void 0,
|
|
2556
|
+
temporal: void 0,
|
|
2557
|
+
generation,
|
|
2558
|
+
inspection: inspectCloudLayer({ authored: false })
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
const world = context.worlds[context.owner];
|
|
2562
|
+
if (world === void 0) {
|
|
2563
|
+
histories.reset();
|
|
2564
|
+
return ok({
|
|
2565
|
+
params: void 0,
|
|
2566
|
+
cache: void 0,
|
|
2567
|
+
sourceKey: void 0,
|
|
2568
|
+
timeSeconds: 0,
|
|
2569
|
+
sunDirection: void 0,
|
|
2570
|
+
sunRadiance: void 0,
|
|
2571
|
+
shadowProjection: void 0,
|
|
2572
|
+
temporal: void 0,
|
|
2573
|
+
generation,
|
|
2574
|
+
inspection: inspectCloudLayer({ authored: false })
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
const cloud = context.frame?.cloudLayer;
|
|
2578
|
+
const capability = context.caps === void 0 ? void 0 : cloudCapabilitiesFromRhi(context.caps);
|
|
2579
|
+
if (cloud === void 0) {
|
|
2580
|
+
histories.reset();
|
|
2581
|
+
return ok({
|
|
2582
|
+
params: void 0,
|
|
2583
|
+
cache: void 0,
|
|
2584
|
+
sourceKey: void 0,
|
|
2585
|
+
timeSeconds: 0,
|
|
2586
|
+
sunDirection: void 0,
|
|
2587
|
+
sunRadiance: void 0,
|
|
2588
|
+
shadowProjection: void 0,
|
|
2589
|
+
temporal: void 0,
|
|
2590
|
+
generation,
|
|
2591
|
+
inspection: inspectCloudLayer({
|
|
2592
|
+
authored: false,
|
|
2593
|
+
...capability === void 0 ? {} : { capability }
|
|
2594
|
+
})
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
const formationKey = cloudLayerFormationKey(cloud.params);
|
|
2598
|
+
if (cached === void 0 || cachedSourceKey !== formationKey) {
|
|
2599
|
+
cached = buildCloudDensityCache(cloud.params);
|
|
2600
|
+
packedCache = packCloudDensityCache(cached);
|
|
2601
|
+
cachedSourceKey = formationKey;
|
|
2602
|
+
generation += 1;
|
|
2603
|
+
}
|
|
2604
|
+
const view = context.frame?.view;
|
|
2605
|
+
const shadowProjection = view === void 0 || cloud.sunDirection === void 0 ? void 0 : createCloudShadowProjection({
|
|
2606
|
+
center: view.shadowAnchor ?? view.cameraPosition,
|
|
2607
|
+
sunDirection: cloud.sunDirection,
|
|
2608
|
+
range: cloud.params.shadowRange,
|
|
2609
|
+
resolution: cloudShadowResolutionForQuality(cloud.params.quality)
|
|
2610
|
+
});
|
|
2611
|
+
const temporalSignature = view === void 0 ? void 0 : {
|
|
2612
|
+
sourceKey: cloud.sourceKey,
|
|
2613
|
+
viewId: view.identity,
|
|
2614
|
+
authoringGeneration: generation,
|
|
2615
|
+
cloudShadowRevision: shadowProjection?.revision ?? 0,
|
|
2616
|
+
cameraRevision: view.cameraRevision,
|
|
2617
|
+
deviceGeneration: view.deviceGeneration,
|
|
2618
|
+
width: view.width,
|
|
2619
|
+
height: view.height,
|
|
2620
|
+
timeSeconds: cloud.worldTimeSeconds,
|
|
2621
|
+
quality: cloud.params.quality,
|
|
2622
|
+
...view.sceneDepthVersion === void 0 ? {} : { sceneDepthVersion: view.sceneDepthVersion }
|
|
2623
|
+
};
|
|
2624
|
+
const temporal = temporalSignature === void 0 ? void 0 : (() => {
|
|
2625
|
+
const decision = histories.begin(temporalSignature, {
|
|
2626
|
+
...view?.cameraCut === void 0 ? {} : { cameraCut: view.cameraCut },
|
|
2627
|
+
...view?.recovery === void 0 ? {} : { recovery: view.recovery }
|
|
2628
|
+
});
|
|
2629
|
+
const history = decision.history ?? createCloudHistory(temporalSignature, generation, false);
|
|
2630
|
+
return Object.freeze({
|
|
2631
|
+
signature: temporalSignature,
|
|
2632
|
+
history,
|
|
2633
|
+
reset: decision.reset,
|
|
2634
|
+
reasons: decision.reasons
|
|
2635
|
+
});
|
|
2636
|
+
})();
|
|
2637
|
+
const resourceFacts = inspectCloudLayerResources({
|
|
2638
|
+
generation,
|
|
2639
|
+
cache: cached,
|
|
2640
|
+
...shadowProjection === void 0 ? {} : { shadow: shadowProjection },
|
|
2641
|
+
...temporal?.history === void 0 ? {} : { history: temporal.history }
|
|
2642
|
+
});
|
|
2643
|
+
const temporalReset = temporal?.reasons.at(-1);
|
|
2644
|
+
if (temporal?.reset === true) temporalResetCount += 1;
|
|
2645
|
+
if (temporalReset !== void 0) lastTemporalReset = temporalReset;
|
|
2646
|
+
return ok({
|
|
2647
|
+
params: cloud.params,
|
|
2648
|
+
cache: cached,
|
|
2649
|
+
sourceKey: cloud.sourceKey,
|
|
2650
|
+
timeSeconds: cloud.worldTimeSeconds,
|
|
2651
|
+
sunDirection: cloud.sunDirection,
|
|
2652
|
+
sunRadiance: cloud.sunRadiance,
|
|
2653
|
+
shadowProjection,
|
|
2654
|
+
temporal,
|
|
2655
|
+
generation,
|
|
2656
|
+
inspection: inspectCloudLayer({
|
|
2657
|
+
authored: true,
|
|
2658
|
+
sourceKey: cloud.sourceKey,
|
|
2659
|
+
generation,
|
|
2660
|
+
resourceFacts,
|
|
2661
|
+
...shadowProjection === void 0 ? {} : { shadowRevision: shadowProjection.revision },
|
|
2662
|
+
...temporal === void 0 ? {} : {
|
|
2663
|
+
temporalResets: temporalResetCount,
|
|
2664
|
+
...lastTemporalReset === void 0 ? {} : { lastTemporalReset }
|
|
2665
|
+
},
|
|
2666
|
+
...capability === void 0 ? {} : { capability },
|
|
2667
|
+
budget: {
|
|
2668
|
+
quality: cloud.params.quality,
|
|
2669
|
+
reason: "physical GPU timing and residency receipts are unavailable until a prepared adapter submits them"
|
|
2670
|
+
}
|
|
2671
|
+
})
|
|
2672
|
+
});
|
|
2673
|
+
},
|
|
2674
|
+
onFrameSubmitted: (frame) => {
|
|
2675
|
+
if (frame.temporal?.history !== void 0) {
|
|
2676
|
+
histories.commit(
|
|
2677
|
+
frame.temporal.signature,
|
|
2678
|
+
createCloudHistory(frame.temporal.signature, frame.generation, true)
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
},
|
|
2682
|
+
onFrameAborted: (frame) => {
|
|
2683
|
+
},
|
|
2684
|
+
plan: (frame, context) => {
|
|
2685
|
+
if (frame.cache === void 0 || packedCache === void 0 || frame.params === void 0) {
|
|
2686
|
+
return ok({ resources: [], passes: [] });
|
|
2687
|
+
}
|
|
2688
|
+
return ok(planCloudDensity(frame, packedCache, context));
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
var ERROR_POLICY = {
|
|
2693
|
+
"surface-abi-missing": {
|
|
2694
|
+
expected: "a producer-published single-layer-medium ABI is present",
|
|
2695
|
+
hint: "compose, reflect, and publish the Surface model before preparing the draw"
|
|
2696
|
+
},
|
|
2697
|
+
"surface-pass-missing": {
|
|
2698
|
+
expected: "the published model contains both nearest-layer and color passes",
|
|
2699
|
+
hint: "recook the complete model publication; do not synthesize a missing pass"
|
|
2700
|
+
},
|
|
2701
|
+
"surface-producer-not-ready": {
|
|
2702
|
+
expected: "scene-index, resources, and dynamic input facts are ready for the published model",
|
|
2703
|
+
hint: "repair the owning producer publication and retry; capability fallback does not cover stale assets"
|
|
2704
|
+
},
|
|
2705
|
+
"surface-generation-stale": {
|
|
2706
|
+
expected: "prepared Surface facts belong to the current device generation",
|
|
2707
|
+
hint: "rebuild the prepared surface resources for the current device generation"
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
function failure(code, pass, owner, actual) {
|
|
2711
|
+
return {
|
|
2712
|
+
code,
|
|
2713
|
+
expected: ERROR_POLICY[code].expected,
|
|
2714
|
+
hint: ERROR_POLICY[code].hint,
|
|
2715
|
+
pass,
|
|
2716
|
+
owner,
|
|
2717
|
+
...actual === void 0 ? {} : { actual }
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
function capabilityFallback(caps) {
|
|
2721
|
+
if (!caps.compute) return "compute";
|
|
2722
|
+
if (!caps.storageBuffer) return "storageBuffer";
|
|
2723
|
+
if (!caps.indirectDrawing) return "indirectDrawing";
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
function admitSingleLayerMediumSubmission(input) {
|
|
2727
|
+
const firstPass = "nearest-layer";
|
|
2728
|
+
const surface = input.abi.surface;
|
|
2729
|
+
if (surface === void 0 || surface.model !== "single-layer-medium") {
|
|
2730
|
+
return err(failure("surface-abi-missing", firstPass, "surface-producer", surface));
|
|
2731
|
+
}
|
|
2732
|
+
if (!surface.passes.includes("nearest-layer") || !surface.passes.includes("color")) {
|
|
2733
|
+
return err(failure("surface-pass-missing", firstPass, "surface-producer", surface.passes));
|
|
2734
|
+
}
|
|
2735
|
+
if (input.deviceGeneration !== input.preparedDeviceGeneration) {
|
|
2736
|
+
return err(
|
|
2737
|
+
failure("surface-generation-stale", firstPass, "render-generation", {
|
|
2738
|
+
deviceGeneration: input.deviceGeneration,
|
|
2739
|
+
preparedDeviceGeneration: input.preparedDeviceGeneration
|
|
2740
|
+
})
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
const dynamicRequired = surface.dynamicInput !== void 0;
|
|
2744
|
+
if (!input.sceneIndexReady || !input.resourcesReady || dynamicRequired && !input.dynamicInputReady) {
|
|
2745
|
+
return err(
|
|
2746
|
+
failure("surface-producer-not-ready", firstPass, "surface-producer", {
|
|
2747
|
+
sceneIndexReady: input.sceneIndexReady,
|
|
2748
|
+
resourcesReady: input.resourcesReady,
|
|
2749
|
+
dynamicInputReady: input.dynamicInputReady
|
|
2750
|
+
})
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
const fallback = capabilityFallback(input.caps);
|
|
2754
|
+
const admitted = fallback === void 0 ? [
|
|
2755
|
+
{ pass: firstPass, lane: "gpu-driven" },
|
|
2756
|
+
{ pass: "color", lane: "gpu-driven" }
|
|
2757
|
+
] : [
|
|
2758
|
+
{ pass: firstPass, lane: "direct", reason: fallback },
|
|
2759
|
+
{ pass: "color", lane: "direct", reason: fallback }
|
|
2760
|
+
];
|
|
2761
|
+
return ok({
|
|
2762
|
+
model: "single-layer-medium",
|
|
2763
|
+
lane: fallback === void 0 ? "gpu-driven" : "direct",
|
|
2764
|
+
passes: admitted,
|
|
2765
|
+
deviceGeneration: input.deviceGeneration
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
function invalidStyle(input, component, field, value, expected) {
|
|
2769
|
+
return err(new PointsLinesInvalidStyleError({ entity: input.entity, component, field, value, expected }));
|
|
2770
|
+
}
|
|
2771
|
+
function topologyMismatch(input, submesh, expected, actual) {
|
|
2772
|
+
return err(new PointsLinesTopologyMismatchError({ entity: input.entity, submesh, expected, actual }));
|
|
2773
|
+
}
|
|
2774
|
+
function checkStyle(input) {
|
|
2775
|
+
const hasPoints = input.points !== void 0;
|
|
2776
|
+
const hasLines = input.lines !== void 0;
|
|
2777
|
+
if (hasPoints && hasLines) {
|
|
2778
|
+
return invalidStyle(
|
|
2779
|
+
input,
|
|
2780
|
+
"Points/Lines",
|
|
2781
|
+
"components",
|
|
2782
|
+
"Points + Lines",
|
|
2783
|
+
"exactly one style component"
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
if (!hasPoints && !hasLines) {
|
|
2787
|
+
return invalidStyle(input, "Points/Lines", "components", void 0, "one style component");
|
|
2788
|
+
}
|
|
2789
|
+
if (hasPoints) {
|
|
2790
|
+
const sizePx = input.points?.sizePx ?? 4;
|
|
2791
|
+
const shapeValue = input.points?.shape ?? PointShapeValue.square;
|
|
2792
|
+
const shape = pointShapeFromU32(shapeValue);
|
|
2793
|
+
if (!Number.isFinite(sizePx) || sizePx <= 0) {
|
|
2794
|
+
return invalidStyle(input, "Points", "sizePx", sizePx, "finite sizePx > 0");
|
|
2795
|
+
}
|
|
2796
|
+
if (shape === void 0) {
|
|
2797
|
+
return invalidStyle(input, "Points", "shape", shapeValue, "shape is 'square' or 'circle'");
|
|
2798
|
+
}
|
|
2799
|
+
return ok({ component: "Points", shape, sizePx });
|
|
2800
|
+
}
|
|
2801
|
+
const widthPx = input.lines?.widthPx ?? 1;
|
|
2802
|
+
if (!Number.isFinite(widthPx) || widthPx <= 0) {
|
|
2803
|
+
return invalidStyle(input, "Lines", "widthPx", widthPx, "finite widthPx > 0");
|
|
2804
|
+
}
|
|
2805
|
+
return ok({ component: "Lines", widthPx });
|
|
2806
|
+
}
|
|
2807
|
+
function checkTopology(input, component) {
|
|
2808
|
+
const expected = component === "Points" ? "point-list" : "line-list";
|
|
2809
|
+
let pointCount = 0;
|
|
2810
|
+
let segmentCount = 0;
|
|
2811
|
+
const acceptedSubmeshes = [];
|
|
2812
|
+
let sawNonEmpty = false;
|
|
2813
|
+
for (const [submeshIndex, submesh] of input.mesh.submeshes.entries()) {
|
|
2814
|
+
const indexed = input.mesh.indices !== void 0 && submesh.indexCount > 0;
|
|
2815
|
+
const elementCount = indexed ? submesh.indexCount : submesh.vertexCount;
|
|
2816
|
+
if (elementCount === 0) continue;
|
|
2817
|
+
sawNonEmpty = true;
|
|
2818
|
+
if (component === "Lines" && submesh.topology === "line-strip") {
|
|
2819
|
+
return err(
|
|
2820
|
+
new PointsLinesStyleUnsupportedError({
|
|
2821
|
+
lane: "admission",
|
|
2822
|
+
field: "topology",
|
|
2823
|
+
member: "line-strip",
|
|
2824
|
+
supported: ["line-list"]
|
|
2825
|
+
})
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
if (submesh.topology !== expected) {
|
|
2829
|
+
return topologyMismatch(input, submeshIndex, expected, submesh.topology);
|
|
2830
|
+
}
|
|
2831
|
+
if (component === "Lines" && elementCount % 2 !== 0) {
|
|
2832
|
+
return topologyMismatch(input, submeshIndex, "line-list pairs", "line-list odd tail");
|
|
2833
|
+
}
|
|
2834
|
+
acceptedSubmeshes.push(submeshIndex);
|
|
2835
|
+
if (component === "Points") pointCount += elementCount;
|
|
2836
|
+
else segmentCount += elementCount / 2;
|
|
2837
|
+
}
|
|
2838
|
+
if (!sawNonEmpty) return topologyMismatch(input, 0, expected, "empty-range");
|
|
2839
|
+
return ok({ pointCount, segmentCount, submeshes: acceptedSubmeshes });
|
|
2840
|
+
}
|
|
2841
|
+
function checkMaterial(input) {
|
|
2842
|
+
const passes = input.material.passes ?? [];
|
|
2843
|
+
const unlitModules = /* @__PURE__ */ new Set(["forgeax_material::unlit", "forgeax::default-unlit"]);
|
|
2844
|
+
if (passes.length !== 1 || passes[0] === void 0) {
|
|
2845
|
+
const pass2 = passes.find(
|
|
2846
|
+
(candidate) => candidate.name === "deferred" || candidate.name === "shadow-caster"
|
|
2847
|
+
) ?? passes[0];
|
|
2848
|
+
return err(
|
|
2849
|
+
new PointsLinesMaterialUnsupportedError({
|
|
2850
|
+
entity: input.entity,
|
|
2851
|
+
material: input.materialId ?? "<anonymous>",
|
|
2852
|
+
pass: pass2?.name ?? "<empty>",
|
|
2853
|
+
module: pass2?.program.module ?? "<empty>",
|
|
2854
|
+
reason: "points-lines admission requires one unlit forward pass"
|
|
2855
|
+
})
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
const pass = passes[0];
|
|
2859
|
+
const lightMode = pass.renderState?.tags?.LightMode;
|
|
2860
|
+
if (!unlitModules.has(pass.program.module) || pass.name !== "forward" || lightMode === "ShadowCaster") {
|
|
2861
|
+
return err(
|
|
2862
|
+
new PointsLinesMaterialUnsupportedError({
|
|
2863
|
+
entity: input.entity,
|
|
2864
|
+
material: input.materialId ?? "<anonymous>",
|
|
2865
|
+
pass: pass.name,
|
|
2866
|
+
module: pass.program.module,
|
|
2867
|
+
reason: "points-lines admission requires an engine-owned unlit forward pass"
|
|
2868
|
+
})
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
return ok(void 0);
|
|
2872
|
+
}
|
|
2873
|
+
function admitPointsLines(input) {
|
|
2874
|
+
const style = checkStyle(input);
|
|
2875
|
+
if (!style.ok) return style;
|
|
2876
|
+
const topology = checkTopology(input, style.value.component);
|
|
2877
|
+
if (!topology.ok) return topology;
|
|
2878
|
+
const material = checkMaterial(input);
|
|
2879
|
+
if (!material.ok) return material;
|
|
2880
|
+
if (style.value.component === "Points" && input.limits?.maxPoints !== void 0) {
|
|
2881
|
+
if (topology.value.pointCount > input.limits.maxPoints) {
|
|
2882
|
+
return err(
|
|
2883
|
+
new PointsLinesBudgetExceededError({
|
|
2884
|
+
lane: "admission",
|
|
2885
|
+
requested: topology.value.pointCount,
|
|
2886
|
+
limit: input.limits.maxPoints,
|
|
2887
|
+
unit: "points"
|
|
2888
|
+
})
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
if (style.value.component === "Lines" && input.limits?.maxSegments !== void 0) {
|
|
2893
|
+
if (topology.value.segmentCount > input.limits.maxSegments) {
|
|
2894
|
+
return err(
|
|
2895
|
+
new PointsLinesBudgetExceededError({
|
|
2896
|
+
lane: "admission",
|
|
2897
|
+
requested: topology.value.segmentCount,
|
|
2898
|
+
limit: input.limits.maxSegments,
|
|
2899
|
+
unit: "segments"
|
|
2900
|
+
})
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return ok({ ...style.value, ...topology.value });
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/point-shadow-inspection.ts
|
|
2908
|
+
function inspectPointShadow(snapshots, shadowAtlasCapacity) {
|
|
2909
|
+
const requested = snapshots.length;
|
|
2910
|
+
const admitted = snapshots.reduce(
|
|
2911
|
+
(count, snapshot) => snapshot.shadowAtlasLayer >= 0 && snapshot.shadowAtlasLayer < shadowAtlasCapacity ? count + 1 : count,
|
|
2912
|
+
0
|
|
2913
|
+
);
|
|
2914
|
+
const status = requested === 0 ? "inactive" : requested > shadowAtlasCapacity || admitted !== requested ? "over-budget" : "ready";
|
|
2915
|
+
return Object.freeze({
|
|
2916
|
+
status,
|
|
2917
|
+
requested,
|
|
2918
|
+
admitted,
|
|
2919
|
+
// A point shadow only samples an atlas after it owns an atlas layer. Keep
|
|
2920
|
+
// these names explicit for AI diagnostics while deriving both from the
|
|
2921
|
+
// one layer projection above.
|
|
2922
|
+
shadowed: admitted,
|
|
2923
|
+
shadowAtlasOccupancy: admitted,
|
|
2924
|
+
shadowAtlasCapacity
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
var SSR_FORMAT_PROFILE = "r32float-mip-sampled-storage";
|
|
2928
|
+
var SSR_FORMAT_STAGES = Object.freeze([
|
|
2929
|
+
"texture-create",
|
|
2930
|
+
"mip-view",
|
|
2931
|
+
"sampled-storage-bind-group",
|
|
2932
|
+
"pipeline-bind",
|
|
2933
|
+
"finish",
|
|
2934
|
+
"submit",
|
|
2935
|
+
"completion",
|
|
2936
|
+
"readback"
|
|
2937
|
+
]);
|
|
2938
|
+
var ZERO_WORK = Object.freeze({
|
|
2939
|
+
attachmentCount: 0,
|
|
2940
|
+
passCount: 0,
|
|
2941
|
+
bindingCount: 0,
|
|
2942
|
+
resourceCount: 0,
|
|
2943
|
+
historyCount: 0,
|
|
2944
|
+
temporalDemand: 0
|
|
2945
|
+
});
|
|
2946
|
+
var ADMITTED_WORK = Object.freeze({
|
|
2947
|
+
attachmentCount: 1,
|
|
2948
|
+
passCount: 1,
|
|
2949
|
+
bindingCount: 1,
|
|
2950
|
+
resourceCount: 1,
|
|
2951
|
+
historyCount: 0,
|
|
2952
|
+
temporalDemand: 0
|
|
2953
|
+
});
|
|
2954
|
+
var ZERO_BUDGET = Object.freeze({
|
|
2955
|
+
probeExecutions: 0,
|
|
2956
|
+
resetCount: 0,
|
|
2957
|
+
rebuildCount: 0
|
|
2958
|
+
});
|
|
2959
|
+
var ADMITTED_BUDGET = Object.freeze({
|
|
2960
|
+
probeExecutions: 1,
|
|
2961
|
+
resetCount: 0,
|
|
2962
|
+
rebuildCount: 0
|
|
2963
|
+
});
|
|
2964
|
+
function identityFieldMismatch(expected, actual) {
|
|
2965
|
+
for (const field of ["sourceHead", "sourceTree", "lockSha256", "buildSha256"]) {
|
|
2966
|
+
if (expected[field] !== actual[field]) return field;
|
|
2967
|
+
}
|
|
2968
|
+
return void 0;
|
|
2969
|
+
}
|
|
2970
|
+
function blocked(identity, failure3, generation = 0) {
|
|
2971
|
+
return Object.freeze({
|
|
2972
|
+
status: "fallback-only",
|
|
2973
|
+
identity,
|
|
2974
|
+
generation,
|
|
2975
|
+
work: ZERO_WORK,
|
|
2976
|
+
budget: ZERO_BUDGET,
|
|
2977
|
+
failure: failure3
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
function failure2(code, owner, hint, detail = {}) {
|
|
2981
|
+
const action = recoveryActionFor(code);
|
|
2982
|
+
return Object.freeze({
|
|
2983
|
+
code,
|
|
2984
|
+
expected: "all producer, format, and temporal receipts share one admitted identity",
|
|
2985
|
+
hint,
|
|
2986
|
+
detail: Object.freeze({ owner, action, ...detail })
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
function recoveryActionFor(code) {
|
|
2990
|
+
switch (code) {
|
|
2991
|
+
case "ssr-reflection-fallback-unavailable":
|
|
2992
|
+
case "ssr-receipt-identity-mismatch":
|
|
2993
|
+
return "rebuild";
|
|
2994
|
+
case "ssr-not-requested":
|
|
2995
|
+
case "ssr-format-unavailable":
|
|
2996
|
+
case "ssr-temporal-unavailable":
|
|
2997
|
+
case "ssr-receipt-stale":
|
|
2998
|
+
return "retry";
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
function receiptFailure(input, receipt, owner) {
|
|
3002
|
+
const field = identityFieldMismatch(input.identity, receipt.identity);
|
|
3003
|
+
if (field === void 0) return void 0;
|
|
3004
|
+
return blocked(
|
|
3005
|
+
input.identity,
|
|
3006
|
+
failure2("ssr-receipt-identity-mismatch", owner, "rebuild the owner receipt for this identity", {
|
|
3007
|
+
identityField: field
|
|
3008
|
+
})
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
function validFallback(receipt) {
|
|
3012
|
+
return receipt !== void 0 && receipt.candidateVisible === false && Number.isFinite(receipt.coverage) && receipt.coverage >= 0 && receipt.coverage <= 1 && (receipt.sourceKey === void 0 || receipt.sourceKey.length > 0) && (receipt.extent === void 0 || receipt.extent.length === 3 && receipt.extent.every((value) => Number.isFinite(value) && value > 0)) && Number.isInteger(receipt.sourceGeneration) && receipt.sourceGeneration >= 0 && Number.isInteger(receipt.projectionGeneration) && receipt.projectionGeneration >= 0 && Number.isInteger(receipt.deviceGeneration) && receipt.deviceGeneration >= 0 && receipt.brdfSignature === "standard-pbr-ibl-v1" && (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg") || receipt.source === "skylight" && (receipt.state === "active" || receipt.state === "lkg") || receipt.source === "neutral" && receipt.state === "neutral");
|
|
3013
|
+
}
|
|
3014
|
+
function validFormat(receipt) {
|
|
3015
|
+
if (receipt?.profile !== SSR_FORMAT_PROFILE || receipt.verdict !== "admitted" || receipt.evidence !== "real" || !Number.isInteger(receipt.deviceGeneration) || receipt.deviceGeneration < 0) {
|
|
3016
|
+
return false;
|
|
3017
|
+
}
|
|
3018
|
+
if (!SSR_FORMAT_STAGES.every(
|
|
3019
|
+
(stage) => receipt.stages.some((entry) => entry.stage === stage && entry.verdict === "admitted")
|
|
3020
|
+
)) {
|
|
3021
|
+
return false;
|
|
3022
|
+
}
|
|
3023
|
+
return validateR32FloatReceipt(receipt).ok;
|
|
3024
|
+
}
|
|
3025
|
+
function validTemporal(receipt) {
|
|
3026
|
+
return receipt?.successfulSubmit === true && Number.isInteger(receipt.generation) && receipt.generation >= 0;
|
|
3027
|
+
}
|
|
3028
|
+
function admitSsrM0(input) {
|
|
3029
|
+
if (!input.requested) {
|
|
3030
|
+
return blocked(
|
|
3031
|
+
input.identity,
|
|
3032
|
+
failure2("ssr-not-requested", "consumer", "request SSR before evaluating M0 admission")
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
3035
|
+
if (input.reflectionFallback === void 0) {
|
|
3036
|
+
return blocked(
|
|
3037
|
+
input.identity,
|
|
3038
|
+
failure2("ssr-reflection-fallback-unavailable", "producer", "use LKG, Skylight, or neutral")
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
const fallbackIdentityFailure = receiptFailure(input, input.reflectionFallback, "producer");
|
|
3042
|
+
if (fallbackIdentityFailure !== void 0) return fallbackIdentityFailure;
|
|
3043
|
+
if (!validFallback(input.reflectionFallback)) {
|
|
3044
|
+
return blocked(
|
|
3045
|
+
input.identity,
|
|
3046
|
+
failure2(
|
|
3047
|
+
"ssr-reflection-fallback-unavailable",
|
|
3048
|
+
"producer",
|
|
3049
|
+
"inspect and rebuild the producer receipt"
|
|
3050
|
+
),
|
|
3051
|
+
input.reflectionFallback.projectionGeneration
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
if (input.format === void 0) {
|
|
3055
|
+
return blocked(
|
|
3056
|
+
input.identity,
|
|
3057
|
+
failure2("ssr-format-unavailable", "format", "probe the exact format profile for this device"),
|
|
3058
|
+
input.reflectionFallback.projectionGeneration
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
const formatIdentityFailure = receiptFailure(input, input.format, "format");
|
|
3062
|
+
if (formatIdentityFailure !== void 0) return formatIdentityFailure;
|
|
3063
|
+
if (!validFormat(input.format)) {
|
|
3064
|
+
return blocked(
|
|
3065
|
+
input.identity,
|
|
3066
|
+
failure2("ssr-format-unavailable", "format", "complete the full r32float profile probe"),
|
|
3067
|
+
input.reflectionFallback.projectionGeneration
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
const temporalIdentityFailure = input.temporal === void 0 ? void 0 : receiptFailure(input, input.temporal, "temporal");
|
|
3071
|
+
if (temporalIdentityFailure !== void 0) return temporalIdentityFailure;
|
|
3072
|
+
if (input.temporal !== void 0 && !validTemporal(input.temporal)) {
|
|
3073
|
+
return blocked(
|
|
3074
|
+
input.identity,
|
|
3075
|
+
failure2("ssr-temporal-unavailable", "temporal", "inspect and rebuild the temporal receipt"),
|
|
3076
|
+
input.reflectionFallback.projectionGeneration
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
if (input.previousGeneration !== void 0 && input.previousGeneration !== input.reflectionFallback.projectionGeneration) {
|
|
3080
|
+
return blocked(
|
|
3081
|
+
input.identity,
|
|
3082
|
+
failure2(
|
|
3083
|
+
"ssr-receipt-stale",
|
|
3084
|
+
"consumer",
|
|
3085
|
+
"discard stale completion and reinspect the owners",
|
|
3086
|
+
{
|
|
3087
|
+
generation: input.reflectionFallback.projectionGeneration
|
|
3088
|
+
}
|
|
3089
|
+
),
|
|
3090
|
+
input.reflectionFallback.projectionGeneration
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
return Object.freeze({
|
|
3094
|
+
status: "admitted",
|
|
3095
|
+
identity: input.identity,
|
|
3096
|
+
generation: input.reflectionFallback.projectionGeneration,
|
|
3097
|
+
work: ADMITTED_WORK,
|
|
3098
|
+
budget: ADMITTED_BUDGET
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
function projectSsrDependencies(input) {
|
|
3102
|
+
const identity = input.identity;
|
|
3103
|
+
const reflectionFallback = identity === void 0 || input.reflectionFallback?.identity === void 0 ? void 0 : input.reflectionFallback;
|
|
3104
|
+
const format = identity === void 0 || input.format === void 0 ? void 0 : { ...input.format, identity };
|
|
3105
|
+
const temporal = identity === void 0 || input.temporal === void 0 ? void 0 : { identity, ...input.temporal };
|
|
3106
|
+
const admission = identity === void 0 ? blocked(
|
|
3107
|
+
void 0,
|
|
3108
|
+
failure2(
|
|
3109
|
+
input.requested ? "ssr-receipt-identity-mismatch" : "ssr-not-requested",
|
|
3110
|
+
"consumer",
|
|
3111
|
+
input.requested ? "bind the exact source, tree, lock, and build identity before admission" : "request SSR before evaluating M0 admission",
|
|
3112
|
+
input.requested ? { identityField: "sourceHead" } : {}
|
|
3113
|
+
)
|
|
3114
|
+
) : admitSsrM0({
|
|
3115
|
+
requested: input.requested,
|
|
3116
|
+
identity,
|
|
3117
|
+
...reflectionFallback === void 0 ? {} : { reflectionFallback },
|
|
3118
|
+
...format === void 0 ? {} : { format },
|
|
3119
|
+
...temporal === void 0 ? {} : { temporal },
|
|
3120
|
+
...input.previousGeneration === void 0 ? {} : { previousGeneration: input.previousGeneration }
|
|
3121
|
+
});
|
|
3122
|
+
return Object.freeze({
|
|
3123
|
+
status: admission.status,
|
|
3124
|
+
identity,
|
|
3125
|
+
requested: input.requested,
|
|
3126
|
+
reflectionFallback: input.reflectionFallback,
|
|
3127
|
+
format,
|
|
3128
|
+
temporal,
|
|
3129
|
+
admission,
|
|
3130
|
+
work: admission.work,
|
|
3131
|
+
budget: admission.budget,
|
|
3132
|
+
failure: admission.failure
|
|
3133
|
+
});
|
|
3134
|
+
}
|
|
3135
|
+
function zeroSsrAdmissionWork() {
|
|
3136
|
+
return ZERO_WORK;
|
|
3137
|
+
}
|
|
3138
|
+
function resolveSsrAdmissionGeneration(previousGeneration, nextGeneration) {
|
|
3139
|
+
return {
|
|
3140
|
+
changed: previousGeneration !== void 0 && nextGeneration !== void 0 && previousGeneration !== nextGeneration,
|
|
3141
|
+
generation: nextGeneration
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
var ZERO_SPATIAL_WORK = Object.freeze({
|
|
3145
|
+
attachmentCount: 0,
|
|
3146
|
+
passCount: 0,
|
|
3147
|
+
bindingCount: 0,
|
|
3148
|
+
resourceCount: 0,
|
|
3149
|
+
historyCount: 0,
|
|
3150
|
+
temporalDemand: 0
|
|
3151
|
+
});
|
|
3152
|
+
var ADMITTED_SPATIAL_WORK = Object.freeze({
|
|
3153
|
+
attachmentCount: 3,
|
|
3154
|
+
passCount: 4,
|
|
3155
|
+
bindingCount: 4,
|
|
3156
|
+
resourceCount: 3,
|
|
3157
|
+
historyCount: 0,
|
|
3158
|
+
temporalDemand: 0
|
|
3159
|
+
});
|
|
3160
|
+
function configRange(field, viewRange, maxDistance) {
|
|
3161
|
+
switch (field) {
|
|
3162
|
+
case "maxDistance":
|
|
3163
|
+
return { min: 0, minInclusive: false, max: viewRange, maxInclusive: true };
|
|
3164
|
+
case "thickness":
|
|
3165
|
+
return { min: 0, minInclusive: false, max: maxDistance, maxInclusive: true };
|
|
3166
|
+
case "maxRoughness":
|
|
3167
|
+
return { min: 0, minInclusive: true, max: 1, maxInclusive: true };
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function configError(field, value, range) {
|
|
3171
|
+
const left = range.minInclusive ? "[" : "(";
|
|
3172
|
+
const right = range.maxInclusive ? "]" : ")";
|
|
3173
|
+
return Object.freeze({
|
|
3174
|
+
code: "ssr-config-invalid",
|
|
3175
|
+
expected: `finite ${field} in ${left}${range.min}, ${range.max}${right}`,
|
|
3176
|
+
hint: `set ${field} to a finite value inside the reported range before retrying`,
|
|
3177
|
+
detail: Object.freeze({ field, value, range: Object.freeze(range) })
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
function validateScreenSpaceReflection(config, input) {
|
|
3181
|
+
const viewRange = input.viewRange;
|
|
3182
|
+
if (!Number.isFinite(viewRange) || viewRange <= 0) {
|
|
3183
|
+
return err(
|
|
3184
|
+
configError("maxDistance", config.maxDistance, configRange("maxDistance", viewRange, 0))
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
if (!Number.isFinite(config.maxDistance) || config.maxDistance <= 0 || config.maxDistance > viewRange) {
|
|
3188
|
+
return err(
|
|
3189
|
+
configError(
|
|
3190
|
+
"maxDistance",
|
|
3191
|
+
config.maxDistance,
|
|
3192
|
+
configRange("maxDistance", viewRange, config.maxDistance)
|
|
3193
|
+
)
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
if (!Number.isFinite(config.thickness) || config.thickness <= 0 || config.thickness > config.maxDistance) {
|
|
3197
|
+
return err(
|
|
3198
|
+
configError(
|
|
3199
|
+
"thickness",
|
|
3200
|
+
config.thickness,
|
|
3201
|
+
configRange("thickness", viewRange, config.maxDistance)
|
|
3202
|
+
)
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
if (!Number.isFinite(config.maxRoughness) || config.maxRoughness < 0 || config.maxRoughness > 1) {
|
|
3206
|
+
return err(
|
|
3207
|
+
configError(
|
|
3208
|
+
"maxRoughness",
|
|
3209
|
+
config.maxRoughness,
|
|
3210
|
+
configRange("maxRoughness", viewRange, config.maxDistance)
|
|
3211
|
+
)
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
return ok(
|
|
3215
|
+
Object.freeze({
|
|
3216
|
+
maxDistance: config.maxDistance,
|
|
3217
|
+
thickness: config.thickness,
|
|
3218
|
+
maxRoughness: config.maxRoughness
|
|
3219
|
+
})
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
function unavailable(lane, reason, required, actual) {
|
|
3223
|
+
const recovery = reason === "lane-unsupported" || reason === "projection-unsupported" ? "select a perspective clustered or deferred Standard lane" : reason === "reflection-fallback-unavailable" ? "repair the submitted reflection fallback owner receipt" : reason === "temporal-unavailable" ? "submit the matching temporal-v1 producer receipt" : reason === "recovery-unavailable" ? "complete device and producer recovery before retrying" : "restore the required scene, capability, or format input before retrying";
|
|
3224
|
+
return Object.freeze({
|
|
3225
|
+
code: "ssr-unavailable",
|
|
3226
|
+
expected: "SSR spatial admission has a complete Standard input and capability set",
|
|
3227
|
+
hint: recovery,
|
|
3228
|
+
detail: Object.freeze({
|
|
3229
|
+
lane,
|
|
3230
|
+
reason,
|
|
3231
|
+
required: Object.freeze([...required]),
|
|
3232
|
+
actual: Object.freeze({ ...actual }),
|
|
3233
|
+
recovery
|
|
3234
|
+
})
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
function unavailableResult(lane, viewRange, failure3) {
|
|
3238
|
+
return Object.freeze({
|
|
3239
|
+
status: "fallback-only",
|
|
3240
|
+
lane,
|
|
3241
|
+
config: void 0,
|
|
3242
|
+
viewRange,
|
|
3243
|
+
work: ZERO_SPATIAL_WORK,
|
|
3244
|
+
failure: failure3
|
|
3245
|
+
});
|
|
3246
|
+
}
|
|
3247
|
+
function admitSsrSpatial(input) {
|
|
3248
|
+
const camera = input.camera;
|
|
3249
|
+
const environment = input.environment;
|
|
3250
|
+
const viewRange = camera.far - camera.near;
|
|
3251
|
+
const config = camera.screenSpaceReflection;
|
|
3252
|
+
if (config === void 0) {
|
|
3253
|
+
return Object.freeze({
|
|
3254
|
+
status: "not-requested",
|
|
3255
|
+
lane: environment.lane,
|
|
3256
|
+
config: void 0,
|
|
3257
|
+
viewRange,
|
|
3258
|
+
work: ZERO_SPATIAL_WORK
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
3261
|
+
const valid = validateScreenSpaceReflection(config, { viewRange });
|
|
3262
|
+
if (!valid.ok) {
|
|
3263
|
+
return Object.freeze({
|
|
3264
|
+
status: "fallback-only",
|
|
3265
|
+
lane: environment.lane,
|
|
3266
|
+
config,
|
|
3267
|
+
viewRange,
|
|
3268
|
+
work: ZERO_SPATIAL_WORK,
|
|
3269
|
+
failure: valid.error
|
|
3270
|
+
});
|
|
3271
|
+
}
|
|
3272
|
+
if (environment.m0.status !== "admitted") {
|
|
3273
|
+
return unavailableResult(
|
|
3274
|
+
environment.lane,
|
|
3275
|
+
viewRange,
|
|
3276
|
+
unavailable(
|
|
3277
|
+
environment.lane,
|
|
3278
|
+
"reflection-fallback-unavailable",
|
|
3279
|
+
["admitted M0 dependency receipt"],
|
|
3280
|
+
{ m0: false }
|
|
3281
|
+
)
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
if (environment.lane !== "clustered" && environment.lane !== "deferred") {
|
|
3285
|
+
return unavailableResult(
|
|
3286
|
+
environment.lane,
|
|
3287
|
+
viewRange,
|
|
3288
|
+
unavailable(environment.lane, "lane-unsupported", ["clustered or deferred Standard lane"], {
|
|
3289
|
+
lane: environment.lane
|
|
3290
|
+
})
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
if (camera.projection !== "perspective") {
|
|
3294
|
+
return unavailableResult(
|
|
3295
|
+
environment.lane,
|
|
3296
|
+
viewRange,
|
|
3297
|
+
unavailable(environment.lane, "projection-unsupported", ["perspective camera"], {
|
|
3298
|
+
projection: camera.projection
|
|
3299
|
+
})
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
if (!environment.sceneInputs) {
|
|
3303
|
+
return unavailableResult(
|
|
3304
|
+
environment.lane,
|
|
3305
|
+
viewRange,
|
|
3306
|
+
unavailable(
|
|
3307
|
+
environment.lane,
|
|
3308
|
+
"scene-input-unavailable",
|
|
3309
|
+
["deferred Standard scene/material/fallback inputs"],
|
|
3310
|
+
{ sceneInputs: false }
|
|
3311
|
+
)
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
if (!environment.temporal) {
|
|
3315
|
+
return unavailableResult(
|
|
3316
|
+
environment.lane,
|
|
3317
|
+
viewRange,
|
|
3318
|
+
unavailable(environment.lane, "temporal-unavailable", ["temporal-v1"], { temporal: false })
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
if (!environment.reflectionFallback) {
|
|
3322
|
+
return unavailableResult(
|
|
3323
|
+
environment.lane,
|
|
3324
|
+
viewRange,
|
|
3325
|
+
unavailable(
|
|
3326
|
+
environment.lane,
|
|
3327
|
+
"reflection-fallback-unavailable",
|
|
3328
|
+
["submitted BRDF-projected fallback"],
|
|
3329
|
+
{ reflectionFallback: false }
|
|
3330
|
+
)
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
if (environment.recovery === false) {
|
|
3334
|
+
return unavailableResult(
|
|
3335
|
+
environment.lane,
|
|
3336
|
+
viewRange,
|
|
3337
|
+
unavailable(environment.lane, "recovery-unavailable", ["ready device and owners"], {
|
|
3338
|
+
recovery: false
|
|
3339
|
+
})
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
3342
|
+
const capabilities = environment.capabilities;
|
|
3343
|
+
if (!capabilities.compute || !capabilities.storageTexture || !capabilities.rgba16floatRenderable) {
|
|
3344
|
+
return unavailableResult(
|
|
3345
|
+
environment.lane,
|
|
3346
|
+
viewRange,
|
|
3347
|
+
unavailable(
|
|
3348
|
+
environment.lane,
|
|
3349
|
+
"capability-unavailable",
|
|
3350
|
+
["compute", "storageTexture", "rgba16floatRenderable"],
|
|
3351
|
+
{
|
|
3352
|
+
compute: capabilities.compute,
|
|
3353
|
+
storageTexture: capabilities.storageTexture,
|
|
3354
|
+
rgba16floatRenderable: capabilities.rgba16floatRenderable
|
|
3355
|
+
}
|
|
3356
|
+
)
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
if (!capabilities.r32floatSampledStorage) {
|
|
3360
|
+
return unavailableResult(
|
|
3361
|
+
environment.lane,
|
|
3362
|
+
viewRange,
|
|
3363
|
+
unavailable(
|
|
3364
|
+
environment.lane,
|
|
3365
|
+
"format-unavailable",
|
|
3366
|
+
["r32float sampled and storage mip chain"],
|
|
3367
|
+
{ r32floatSampledStorage: false }
|
|
3368
|
+
)
|
|
3369
|
+
);
|
|
3370
|
+
}
|
|
3371
|
+
return Object.freeze({
|
|
3372
|
+
status: "admitted",
|
|
3373
|
+
lane: environment.lane,
|
|
3374
|
+
config: valid.value,
|
|
3375
|
+
viewRange,
|
|
3376
|
+
work: ADMITTED_SPATIAL_WORK
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// src/volume/inspection.ts
|
|
3381
|
+
function resourceStage2(input) {
|
|
3382
|
+
if (!input.authored || input.recovery === void 0) return "none";
|
|
3383
|
+
if (input.recovery.status === "candidate") return "candidate";
|
|
3384
|
+
if (input.recovery.status === "recovering") return "recovering";
|
|
3385
|
+
if (input.recovery.status === "degraded") return "lkg";
|
|
3386
|
+
return "accepted";
|
|
3387
|
+
}
|
|
3388
|
+
function inspectVolumetricFog(input) {
|
|
3389
|
+
const recovery = input.recovery;
|
|
3390
|
+
const status = !input.authored ? "off" : input.capability === "unavailable" ? "unavailable" : input.degraded === true || recovery?.status === "degraded" ? "degraded" : "available";
|
|
3391
|
+
return Object.freeze({
|
|
3392
|
+
ownerCount: 0,
|
|
3393
|
+
status,
|
|
3394
|
+
resourceStage: resourceStage2(input),
|
|
3395
|
+
guid: recovery?.guid,
|
|
3396
|
+
generation: recovery?.generation,
|
|
3397
|
+
digest: input.acceptedDigest,
|
|
3398
|
+
candidateGeneration: recovery?.candidateGeneration,
|
|
3399
|
+
candidateDigest: recovery?.candidateDigest,
|
|
3400
|
+
lkgGeneration: recovery?.lkgGeneration,
|
|
3401
|
+
candidateFailure: recovery?.candidateFailure,
|
|
3402
|
+
deviceEpoch: recovery?.deviceEpoch ?? 0,
|
|
3403
|
+
format: input.format,
|
|
3404
|
+
passCount: input.passCount ?? 0,
|
|
3405
|
+
sampleCount: input.sampleCount ?? 0,
|
|
3406
|
+
memoryBytes: input.memoryBytes ?? 0,
|
|
3407
|
+
resourceFacts: input.resourceFacts,
|
|
3408
|
+
selectedLight: recovery?.selectedLight,
|
|
3409
|
+
candidateSelectedLight: recovery?.candidateSelectedLight
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
var ERROR_POLICY2 = {
|
|
3413
|
+
"invalid-schema": {
|
|
3414
|
+
expected: "the dynamic input schema has one derived bounded record layout",
|
|
3415
|
+
hint: "repair the root MaterialSurface dynamicInput declaration and recook it"
|
|
3416
|
+
},
|
|
3417
|
+
"invalid-record": {
|
|
3418
|
+
expected: "each dynamic record matches the derived scalar/vector fields",
|
|
3419
|
+
hint: "write finite values using the field names and types declared by the material"
|
|
3420
|
+
},
|
|
3421
|
+
"range-overflow": {
|
|
3422
|
+
expected: "the instance range stays within the declared read-only page",
|
|
3423
|
+
hint: "reduce the range or increase the producer page budget before publication"
|
|
3424
|
+
},
|
|
3425
|
+
"domain-overflow": {
|
|
3426
|
+
expected: "the page stays within its declared number of producer domains",
|
|
3427
|
+
hint: "reuse an existing domain or publish a larger bounded declaration"
|
|
3428
|
+
},
|
|
3429
|
+
"not-uploaded": {
|
|
3430
|
+
expected: "the current page revision was uploaded before frame consumption",
|
|
3431
|
+
hint: "submit the page upload receipt and retry the same frame"
|
|
3432
|
+
},
|
|
3433
|
+
"stale-generation": {
|
|
3434
|
+
expected: "the range, upload, and device use one current page generation",
|
|
3435
|
+
hint: "discard stale addresses and rebuild the producer projection for the current device"
|
|
3436
|
+
},
|
|
3437
|
+
released: {
|
|
3438
|
+
expected: "the read-only page is still attached to its owning Render lifecycle",
|
|
3439
|
+
hint: "create a new page after releasing the old owner"
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
var DynamicInputError = class extends Error {
|
|
3443
|
+
code;
|
|
3444
|
+
expected;
|
|
3445
|
+
hint;
|
|
3446
|
+
detail;
|
|
3447
|
+
constructor(code, detail) {
|
|
3448
|
+
super(`${code}: ${ERROR_POLICY2[code].expected}`);
|
|
3449
|
+
this.name = "DynamicInputError";
|
|
3450
|
+
this.code = code;
|
|
3451
|
+
this.expected = ERROR_POLICY2[code].expected;
|
|
3452
|
+
this.hint = ERROR_POLICY2[code].hint;
|
|
3453
|
+
this.detail = Object.freeze({ ...detail });
|
|
3454
|
+
}
|
|
3455
|
+
};
|
|
3456
|
+
function alignRange(ranges, start, end) {
|
|
3457
|
+
if (start >= end) return;
|
|
3458
|
+
let index = 0;
|
|
3459
|
+
while (index < ranges.length && (ranges[index]?.byteEnd ?? 0) < start) index += 1;
|
|
3460
|
+
while (index < ranges.length) {
|
|
3461
|
+
const existing = ranges[index];
|
|
3462
|
+
if (existing === void 0 || existing.byteStart > end) break;
|
|
3463
|
+
start = Math.min(start, existing.byteStart);
|
|
3464
|
+
end = Math.max(end, existing.byteEnd);
|
|
3465
|
+
ranges.splice(index, 1);
|
|
3466
|
+
}
|
|
3467
|
+
ranges.splice(index, 0, { byteStart: start, byteEnd: end });
|
|
3468
|
+
}
|
|
3469
|
+
function fieldWidth(type) {
|
|
3470
|
+
switch (type) {
|
|
3471
|
+
case "f32":
|
|
3472
|
+
case "u32":
|
|
3473
|
+
return 1;
|
|
3474
|
+
case "vec2<f32>":
|
|
3475
|
+
return 2;
|
|
3476
|
+
case "vec3<f32>":
|
|
3477
|
+
return 3;
|
|
3478
|
+
case "vec4<f32>":
|
|
3479
|
+
return 4;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
function finiteNumber(value) {
|
|
3483
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3484
|
+
}
|
|
3485
|
+
function invalid(code, operation, expected, actual, field, page) {
|
|
3486
|
+
return new DynamicInputError(code, {
|
|
3487
|
+
operation,
|
|
3488
|
+
expected,
|
|
3489
|
+
...actual === void 0 ? {} : { actual },
|
|
3490
|
+
...field === void 0 ? {} : { field },
|
|
3491
|
+
...page === void 0 ? {} : page
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
var ReadonlyDynamicInputPage = class _ReadonlyDynamicInputPage {
|
|
3495
|
+
sourceId;
|
|
3496
|
+
pageId;
|
|
3497
|
+
schema;
|
|
3498
|
+
layout;
|
|
3499
|
+
bytes;
|
|
3500
|
+
contentRevisionValue = 1;
|
|
3501
|
+
bufferGenerationValue = 1;
|
|
3502
|
+
deviceGenerationValue = 1;
|
|
3503
|
+
uploadedRevisionValue = 0;
|
|
3504
|
+
lastUploadedBytesValue = 0;
|
|
3505
|
+
released = false;
|
|
3506
|
+
ownedRanges = /* @__PURE__ */ new WeakSet();
|
|
3507
|
+
ownedUploads = /* @__PURE__ */ new WeakSet();
|
|
3508
|
+
dirty = [
|
|
3509
|
+
{
|
|
3510
|
+
byteStart: 0,
|
|
3511
|
+
byteEnd: 0
|
|
3512
|
+
}
|
|
3513
|
+
];
|
|
3514
|
+
domains = /* @__PURE__ */ new Map();
|
|
3515
|
+
constructor(sourceId, pageId, schema, layout) {
|
|
3516
|
+
this.sourceId = sourceId;
|
|
3517
|
+
this.pageId = pageId;
|
|
3518
|
+
this.schema = schema;
|
|
3519
|
+
this.layout = layout;
|
|
3520
|
+
this.bytes = new Uint8Array(schema.maxPageBytes);
|
|
3521
|
+
this.dirty.length = 0;
|
|
3522
|
+
}
|
|
3523
|
+
static create(input) {
|
|
3524
|
+
if (input.sourceId.length === 0 || !Number.isSafeInteger(input.pageId) || input.pageId < 0) {
|
|
3525
|
+
return err(
|
|
3526
|
+
invalid(
|
|
3527
|
+
"invalid-schema",
|
|
3528
|
+
"create",
|
|
3529
|
+
"sourceId is non-empty and pageId is non-negative",
|
|
3530
|
+
input.pageId
|
|
3531
|
+
)
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3534
|
+
const layout = deriveMaterialDynamicInputLayout(input.schema);
|
|
3535
|
+
if (!layout.ok) {
|
|
3536
|
+
return err(
|
|
3537
|
+
invalid(
|
|
3538
|
+
"invalid-schema",
|
|
3539
|
+
"create",
|
|
3540
|
+
layout.error.expected,
|
|
3541
|
+
layout.error.actual,
|
|
3542
|
+
layout.error.field
|
|
3543
|
+
)
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
return ok(
|
|
3547
|
+
new _ReadonlyDynamicInputPage(input.sourceId, input.pageId, input.schema, layout.value)
|
|
3548
|
+
);
|
|
3549
|
+
}
|
|
3550
|
+
get contentRevision() {
|
|
3551
|
+
return this.contentRevisionValue;
|
|
3552
|
+
}
|
|
3553
|
+
get bufferGeneration() {
|
|
3554
|
+
return this.bufferGenerationValue;
|
|
3555
|
+
}
|
|
3556
|
+
get deviceGeneration() {
|
|
3557
|
+
return this.deviceGenerationValue;
|
|
3558
|
+
}
|
|
3559
|
+
get uploadedRevision() {
|
|
3560
|
+
return this.uploadedRevisionValue;
|
|
3561
|
+
}
|
|
3562
|
+
get lastUploadedBytes() {
|
|
3563
|
+
return this.lastUploadedBytesValue;
|
|
3564
|
+
}
|
|
3565
|
+
/** Write one complete record and mark only its byte interval dirty. */
|
|
3566
|
+
writeRecord(index, values) {
|
|
3567
|
+
const live = this.assertLive("write");
|
|
3568
|
+
if (!live.ok) return live;
|
|
3569
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= this.schema.maxRecords) {
|
|
3570
|
+
return err(
|
|
3571
|
+
invalid(
|
|
3572
|
+
"range-overflow",
|
|
3573
|
+
"write",
|
|
3574
|
+
`record index is within [0, ${this.schema.maxRecords})`,
|
|
3575
|
+
index,
|
|
3576
|
+
"index",
|
|
3577
|
+
this
|
|
3578
|
+
)
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
const recordOffset = index * this.layout.stride;
|
|
3582
|
+
const encoded = new Uint8Array(this.layout.stride);
|
|
3583
|
+
const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength);
|
|
3584
|
+
for (const field of this.layout.fields) {
|
|
3585
|
+
const value = values[field.name];
|
|
3586
|
+
if (value === void 0) {
|
|
3587
|
+
return err(
|
|
3588
|
+
invalid(
|
|
3589
|
+
"invalid-record",
|
|
3590
|
+
"write",
|
|
3591
|
+
`record contains field ${field.name}`,
|
|
3592
|
+
value,
|
|
3593
|
+
field.name,
|
|
3594
|
+
this
|
|
3595
|
+
)
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
const width = fieldWidth(field.type);
|
|
3599
|
+
const numbers = typeof value === "number" ? [value] : value;
|
|
3600
|
+
if (numbers.length !== width || numbers.some((candidate) => !finiteNumber(candidate))) {
|
|
3601
|
+
return err(
|
|
3602
|
+
invalid(
|
|
3603
|
+
"invalid-record",
|
|
3604
|
+
"write",
|
|
3605
|
+
`${field.name} is a finite ${field.type}`,
|
|
3606
|
+
value,
|
|
3607
|
+
field.name,
|
|
3608
|
+
this
|
|
3609
|
+
)
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
if (field.type === "u32" && (!Number.isInteger(numbers[0]) || numbers[0] < 0 || numbers[0] > 4294967295)) {
|
|
3613
|
+
return err(
|
|
3614
|
+
invalid(
|
|
3615
|
+
"invalid-record",
|
|
3616
|
+
"write",
|
|
3617
|
+
`${field.name} is an unsigned 32-bit integer`,
|
|
3618
|
+
value,
|
|
3619
|
+
field.name,
|
|
3620
|
+
this
|
|
3621
|
+
)
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
for (const [component, number] of numbers.entries()) {
|
|
3625
|
+
const offset = field.offset + component * 4;
|
|
3626
|
+
if (field.type === "u32") view.setUint32(offset, number, true);
|
|
3627
|
+
else view.setFloat32(offset, number, true);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
this.bytes.set(encoded, recordOffset);
|
|
3631
|
+
this.contentRevisionValue += 1;
|
|
3632
|
+
alignRange(this.dirty, recordOffset, recordOffset + this.layout.stride);
|
|
3633
|
+
return ok(this.contentRevisionValue);
|
|
3634
|
+
}
|
|
3635
|
+
/** Reserve one explicit instance range; no skin address field is involved. */
|
|
3636
|
+
reserveRange(input) {
|
|
3637
|
+
const live = this.assertLive("reserve");
|
|
3638
|
+
if (!live.ok) return live;
|
|
3639
|
+
if (input.domain.length === 0) {
|
|
3640
|
+
return err(
|
|
3641
|
+
invalid(
|
|
3642
|
+
"domain-overflow",
|
|
3643
|
+
"reserve",
|
|
3644
|
+
"domain is a non-empty identity",
|
|
3645
|
+
input.domain,
|
|
3646
|
+
"domain",
|
|
3647
|
+
this
|
|
3648
|
+
)
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3651
|
+
if (!this.domains.has(input.domain) && this.domains.size >= this.schema.maxDomains) {
|
|
3652
|
+
return err(
|
|
3653
|
+
invalid(
|
|
3654
|
+
"domain-overflow",
|
|
3655
|
+
"reserve",
|
|
3656
|
+
`at most ${this.schema.maxDomains} domains are active`,
|
|
3657
|
+
this.domains.size + 1,
|
|
3658
|
+
"domain",
|
|
3659
|
+
this
|
|
3660
|
+
)
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
if (!Number.isSafeInteger(input.instanceIndex) || input.instanceIndex < 0 || input.instanceIndex > 4294967295) {
|
|
3664
|
+
return err(
|
|
3665
|
+
invalid(
|
|
3666
|
+
"range-overflow",
|
|
3667
|
+
"reserve",
|
|
3668
|
+
"instance index is an exact unsigned 32-bit integer",
|
|
3669
|
+
input.instanceIndex,
|
|
3670
|
+
"instanceIndex",
|
|
3671
|
+
this
|
|
3672
|
+
)
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
if (input.member.worldIdentity.length === 0 || !Number.isSafeInteger(input.member.entityKey) || input.member.entityKey < 0 || input.member.entityKey > 4294967295 || !Number.isSafeInteger(input.member.drawItemIndex) || input.member.drawItemIndex < 0 || input.member.drawItemIndex > 4294967295 || !Number.isSafeInteger(input.member.instanceOrdinal) || input.member.instanceOrdinal < 0 || input.member.instanceOrdinal > 4294967295) {
|
|
3676
|
+
return err(
|
|
3677
|
+
invalid(
|
|
3678
|
+
"range-overflow",
|
|
3679
|
+
"reserve",
|
|
3680
|
+
"member is a stable World identity plus exact unsigned 32-bit entity, draw-item, and instance identities",
|
|
3681
|
+
input.member,
|
|
3682
|
+
"member",
|
|
3683
|
+
this
|
|
3684
|
+
)
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
if (!Number.isSafeInteger(input.recordStart) || !Number.isSafeInteger(input.recordCount) || input.recordStart < 0 || input.recordCount <= 0 || input.recordStart + input.recordCount > this.schema.maxRecords) {
|
|
3688
|
+
return err(
|
|
3689
|
+
invalid(
|
|
3690
|
+
"range-overflow",
|
|
3691
|
+
"reserve",
|
|
3692
|
+
"record range and instance index fit the declared page",
|
|
3693
|
+
input,
|
|
3694
|
+
"recordStart",
|
|
3695
|
+
this
|
|
3696
|
+
)
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
3699
|
+
const range = {
|
|
3700
|
+
pageId: this.pageId,
|
|
3701
|
+
sourceId: this.sourceId,
|
|
3702
|
+
domain: input.domain,
|
|
3703
|
+
byteOffset: input.recordStart * this.layout.stride,
|
|
3704
|
+
recordStart: input.recordStart,
|
|
3705
|
+
recordCount: input.recordCount,
|
|
3706
|
+
instanceIndex: input.instanceIndex,
|
|
3707
|
+
member: Object.freeze({ ...input.member }),
|
|
3708
|
+
contentRevision: this.contentRevisionValue,
|
|
3709
|
+
bufferGeneration: this.bufferGenerationValue,
|
|
3710
|
+
deviceGeneration: this.deviceGenerationValue
|
|
3711
|
+
};
|
|
3712
|
+
this.ownedRanges.add(range);
|
|
3713
|
+
this.domains.set(input.domain, {
|
|
3714
|
+
name: input.domain,
|
|
3715
|
+
start: input.recordStart,
|
|
3716
|
+
end: input.recordStart + input.recordCount
|
|
3717
|
+
});
|
|
3718
|
+
return ok(range);
|
|
3719
|
+
}
|
|
3720
|
+
/** Return detached dirty bytes for the existing resource owner to upload. */
|
|
3721
|
+
beginUpload() {
|
|
3722
|
+
const live = this.assertLive("upload");
|
|
3723
|
+
if (!live.ok) return live;
|
|
3724
|
+
const ranges = this.dirty.map((range) => ({ ...range }));
|
|
3725
|
+
const bytes = ranges.reduce((sum, range) => sum + range.byteEnd - range.byteStart, 0);
|
|
3726
|
+
const receipt = {
|
|
3727
|
+
sourceId: this.sourceId,
|
|
3728
|
+
pageId: this.pageId,
|
|
3729
|
+
contentRevision: this.contentRevisionValue,
|
|
3730
|
+
bufferGeneration: this.bufferGenerationValue,
|
|
3731
|
+
deviceGeneration: this.deviceGenerationValue,
|
|
3732
|
+
ranges,
|
|
3733
|
+
bytes
|
|
3734
|
+
};
|
|
3735
|
+
this.ownedUploads.add(receipt);
|
|
3736
|
+
return ok(receipt);
|
|
3737
|
+
}
|
|
3738
|
+
/**
|
|
3739
|
+
* Commit an upload only after the owning queue write has succeeded. A
|
|
3740
|
+
* producer that changed the page while a write was in flight leaves the
|
|
3741
|
+
* dirty bytes pending and must retry the new revision.
|
|
3742
|
+
*/
|
|
3743
|
+
commitUpload(receipt) {
|
|
3744
|
+
const live = this.assertLive("upload");
|
|
3745
|
+
if (!live.ok) return live;
|
|
3746
|
+
if (!this.ownedUploads.has(receipt) || receipt.sourceId !== this.sourceId || receipt.pageId !== this.pageId || receipt.bufferGeneration !== this.bufferGenerationValue || receipt.deviceGeneration !== this.deviceGenerationValue || receipt.contentRevision !== this.contentRevisionValue) {
|
|
3747
|
+
return err(
|
|
3748
|
+
invalid(
|
|
3749
|
+
"stale-generation",
|
|
3750
|
+
"upload",
|
|
3751
|
+
"the upload receipt still describes the current page revision and generation",
|
|
3752
|
+
receipt,
|
|
3753
|
+
"receipt",
|
|
3754
|
+
this
|
|
3755
|
+
)
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3758
|
+
this.uploadedRevisionValue = receipt.contentRevision;
|
|
3759
|
+
this.lastUploadedBytesValue = receipt.bytes;
|
|
3760
|
+
for (const range of receipt.ranges) {
|
|
3761
|
+
const index = this.dirty.findIndex(
|
|
3762
|
+
(candidate) => candidate.byteStart === range.byteStart && candidate.byteEnd === range.byteEnd
|
|
3763
|
+
);
|
|
3764
|
+
if (index >= 0) this.dirty.splice(index, 1);
|
|
3765
|
+
}
|
|
3766
|
+
return ok(true);
|
|
3767
|
+
}
|
|
3768
|
+
/** CPU-only convenience: begin and commit are still explicit in production. */
|
|
3769
|
+
upload() {
|
|
3770
|
+
const receipt = this.beginUpload();
|
|
3771
|
+
if (!receipt.ok) return receipt;
|
|
3772
|
+
const committed = this.commitUpload(receipt.value);
|
|
3773
|
+
if (!committed.ok) return committed;
|
|
3774
|
+
return receipt;
|
|
3775
|
+
}
|
|
3776
|
+
/** Consume an uploaded range after graph submission has completed. */
|
|
3777
|
+
consume(range, frameNumber) {
|
|
3778
|
+
const live = this.assertLive("consume");
|
|
3779
|
+
if (!live.ok) return live;
|
|
3780
|
+
if (!this.ownedRanges.has(range) || range.sourceId !== this.sourceId || range.pageId !== this.pageId || range.bufferGeneration !== this.bufferGenerationValue || range.deviceGeneration !== this.deviceGenerationValue || range.recordStart < 0 || range.recordCount <= 0 || range.recordStart + range.recordCount > this.schema.maxRecords) {
|
|
3781
|
+
return err(
|
|
3782
|
+
invalid(
|
|
3783
|
+
"stale-generation",
|
|
3784
|
+
"consume",
|
|
3785
|
+
"range belongs to this current page generation",
|
|
3786
|
+
range,
|
|
3787
|
+
"generation",
|
|
3788
|
+
this
|
|
3789
|
+
)
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
if (this.uploadedRevisionValue < range.contentRevision) {
|
|
3793
|
+
return err(
|
|
3794
|
+
invalid(
|
|
3795
|
+
"not-uploaded",
|
|
3796
|
+
"consume",
|
|
3797
|
+
"range content revision was uploaded before consumption",
|
|
3798
|
+
this.uploadedRevisionValue,
|
|
3799
|
+
"contentRevision",
|
|
3800
|
+
this
|
|
3801
|
+
)
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
return ok({ ...range, frameNumber, uploadedRevision: this.uploadedRevisionValue });
|
|
3805
|
+
}
|
|
3806
|
+
/** Device loss/rebuild changes physical generations while source identity stays stable. */
|
|
3807
|
+
reconfigureDevice(deviceGeneration) {
|
|
3808
|
+
const live = this.assertLive("reconfigure");
|
|
3809
|
+
if (!live.ok) return live;
|
|
3810
|
+
if (!Number.isSafeInteger(deviceGeneration) || deviceGeneration < 0) {
|
|
3811
|
+
return err(
|
|
3812
|
+
invalid(
|
|
3813
|
+
"stale-generation",
|
|
3814
|
+
"reconfigure",
|
|
3815
|
+
"deviceGeneration is a non-negative safe integer",
|
|
3816
|
+
deviceGeneration,
|
|
3817
|
+
"deviceGeneration",
|
|
3818
|
+
this
|
|
3819
|
+
)
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const rendererInitialGeneration = this.bufferGenerationValue === 1 && this.deviceGenerationValue === 1 && deviceGeneration === 0;
|
|
3823
|
+
if (deviceGeneration < this.deviceGenerationValue && !rendererInitialGeneration) {
|
|
3824
|
+
return err(
|
|
3825
|
+
invalid(
|
|
3826
|
+
"stale-generation",
|
|
3827
|
+
"reconfigure",
|
|
3828
|
+
"deviceGeneration does not precede the current device generation",
|
|
3829
|
+
deviceGeneration,
|
|
3830
|
+
"deviceGeneration",
|
|
3831
|
+
this
|
|
3832
|
+
)
|
|
3833
|
+
);
|
|
3834
|
+
}
|
|
3835
|
+
this.deviceGenerationValue = deviceGeneration;
|
|
3836
|
+
this.bufferGenerationValue += 1;
|
|
3837
|
+
this.uploadedRevisionValue = 0;
|
|
3838
|
+
alignRange(this.dirty, 0, this.schema.maxRecords * this.layout.stride);
|
|
3839
|
+
return ok(this.bufferGenerationValue);
|
|
3840
|
+
}
|
|
3841
|
+
release() {
|
|
3842
|
+
if (this.released) return ok(true);
|
|
3843
|
+
this.released = true;
|
|
3844
|
+
this.domains.clear();
|
|
3845
|
+
this.dirty.length = 0;
|
|
3846
|
+
return ok(true);
|
|
3847
|
+
}
|
|
3848
|
+
assertLive(operation) {
|
|
3849
|
+
return this.released ? err(
|
|
3850
|
+
invalid(
|
|
3851
|
+
"released",
|
|
3852
|
+
operation,
|
|
3853
|
+
ERROR_POLICY2.released.expected,
|
|
3854
|
+
void 0,
|
|
3855
|
+
void 0,
|
|
3856
|
+
this
|
|
3857
|
+
)
|
|
3858
|
+
) : ok(true);
|
|
3859
|
+
}
|
|
3860
|
+
};
|
|
3861
|
+
|
|
3862
|
+
// src/reflection/filter.ts
|
|
3863
|
+
function createProbeFilterState(input) {
|
|
3864
|
+
return {
|
|
3865
|
+
probeIndex: input.probeIndex,
|
|
3866
|
+
faceCount: input.faceCount ?? 6,
|
|
3867
|
+
mipCount: input.mipCount,
|
|
3868
|
+
cursor: 0,
|
|
3869
|
+
activeGeneration: input.activeGeneration ?? 0
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
function advanceProbeFilter(state) {
|
|
3873
|
+
const total = state.faceCount * state.mipCount;
|
|
3874
|
+
if (state.cursor >= total) return void 0;
|
|
3875
|
+
return {
|
|
3876
|
+
probeIndex: state.probeIndex,
|
|
3877
|
+
faceIndex: state.cursor % state.faceCount,
|
|
3878
|
+
mipLevel: Math.floor(state.cursor / state.faceCount)
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
function commitProbeFilterStep(state, success) {
|
|
3882
|
+
if (!success) return state;
|
|
3883
|
+
const nextCursor = state.cursor + 1;
|
|
3884
|
+
const complete = nextCursor >= state.faceCount * state.mipCount;
|
|
3885
|
+
return {
|
|
3886
|
+
...state,
|
|
3887
|
+
cursor: nextCursor,
|
|
3888
|
+
activeGeneration: complete ? state.activeGeneration + 1 : state.activeGeneration
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
function probeFilterIsSteady(state) {
|
|
3892
|
+
return state.cursor >= state.faceCount * state.mipCount;
|
|
3893
|
+
}
|
|
3894
|
+
function normalize3(vector) {
|
|
3895
|
+
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
3896
|
+
if (length === 0) return [0, 0, 1];
|
|
3897
|
+
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
3898
|
+
}
|
|
3899
|
+
function boxProjectReflectionDirection(direction, boxCenter, halfExtents, point) {
|
|
3900
|
+
const ray = normalize3(direction);
|
|
3901
|
+
for (const axis of [0, 1, 2]) {
|
|
3902
|
+
if (Math.abs(point[axis] - boxCenter[axis]) >= halfExtents[axis]) return ray;
|
|
3903
|
+
}
|
|
3904
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
3905
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
3906
|
+
const component = ray[axis] ?? 0;
|
|
3907
|
+
if (component === 0) continue;
|
|
3908
|
+
const extent = halfExtents[axis] ?? 0;
|
|
3909
|
+
const edge = component > 0 ? (boxCenter[axis] ?? 0) + extent : (boxCenter[axis] ?? 0) - extent;
|
|
3910
|
+
const candidate = (edge - (point[axis] ?? 0)) / component;
|
|
3911
|
+
if (candidate > 0) distance = Math.min(distance, candidate);
|
|
3912
|
+
}
|
|
3913
|
+
if (!Number.isFinite(distance)) return ray;
|
|
3914
|
+
return normalize3([
|
|
3915
|
+
(point[0] ?? 0) + ray[0] * distance - (boxCenter[0] ?? 0),
|
|
3916
|
+
(point[1] ?? 0) + ray[1] * distance - (boxCenter[1] ?? 0),
|
|
3917
|
+
(point[2] ?? 0) + ray[2] * distance - (boxCenter[2] ?? 0)
|
|
3918
|
+
]);
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
// src/reflection/gpu-table.ts
|
|
3922
|
+
function buildReflectionProbeTable(rows) {
|
|
3923
|
+
const ordered = rows.slice().sort((a, b) => a.index - b.index);
|
|
3924
|
+
const indices = /* @__PURE__ */ new Map();
|
|
3925
|
+
for (const row of ordered) {
|
|
3926
|
+
if (row.primitiveKey !== void 0) indices.set(row.primitiveKey, row.index);
|
|
3927
|
+
}
|
|
3928
|
+
const indexFor = (primitiveKey2) => indices.get(primitiveKey2);
|
|
3929
|
+
return {
|
|
3930
|
+
rows: Object.freeze(ordered),
|
|
3931
|
+
cpuIndexByPrimitive: indexFor,
|
|
3932
|
+
gpuIndexByPrimitive: indexFor
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
function reflectionProbeTableBytes(table) {
|
|
3936
|
+
return table.rows.length * 48;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/reflection/inspection.ts
|
|
3940
|
+
function recoveryFor(receipt, failure3) {
|
|
3941
|
+
if (failure3 !== void 0) {
|
|
3942
|
+
const code = failure3.code.toLowerCase();
|
|
3943
|
+
if (code.includes("device") || code.includes("format") || code.includes("capability")) {
|
|
3944
|
+
return "rebuild";
|
|
3945
|
+
}
|
|
3946
|
+
if (failure3.stage === "prepare" || failure3.stage === "filter") return "recapture";
|
|
3947
|
+
if (failure3.stage === "build" || failure3.stage === "encode") return "rebuild";
|
|
3948
|
+
if (failure3.stage === "finish" || failure3.stage === "submit") return "retry";
|
|
3949
|
+
if (failure3.stage === "completion") {
|
|
3950
|
+
if (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg")) {
|
|
3951
|
+
return "use-LKG";
|
|
3952
|
+
}
|
|
3953
|
+
if (receipt.source === "skylight" && (receipt.state === "active" || receipt.state === "lkg")) {
|
|
3954
|
+
return "use-Skylight";
|
|
3955
|
+
}
|
|
3956
|
+
if (receipt.source === "neutral" && receipt.state === "neutral") return "use-neutral";
|
|
3957
|
+
return "retry";
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
if (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg")) {
|
|
3961
|
+
return "use-LKG";
|
|
3962
|
+
}
|
|
3963
|
+
if (receipt.source === "skylight" || receipt.state === "lkg") return "use-Skylight";
|
|
3964
|
+
return "use-neutral";
|
|
3965
|
+
}
|
|
3966
|
+
function inspectReflectionFallback(receipt, failure3) {
|
|
3967
|
+
return {
|
|
3968
|
+
receipt,
|
|
3969
|
+
recoveryAction: recoveryFor(receipt, failure3),
|
|
3970
|
+
...failure3 === void 0 ? {} : {
|
|
3971
|
+
failureStage: failure3.stage,
|
|
3972
|
+
failureCode: failure3.code,
|
|
3973
|
+
expected: failure3.expected,
|
|
3974
|
+
...failure3.detail === void 0 ? {} : { detail: failure3.detail }
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/reflection/projection.ts
|
|
3980
|
+
var REFLECTION_PROBE_BYTES_PER_TEXEL = 8;
|
|
3981
|
+
var DEFAULT_REFLECTION_PROBE_LIMITS = Object.freeze({
|
|
3982
|
+
maxProbes: 16,
|
|
3983
|
+
maxResolution: 256,
|
|
3984
|
+
maxBytes: 64 * 1024 * 1024
|
|
3985
|
+
});
|
|
3986
|
+
function validateReflectionProbeInput(input) {
|
|
3987
|
+
for (let index = 0; index < input.halfExtents.length; index += 1) {
|
|
3988
|
+
const value = input.halfExtents[index];
|
|
3989
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) {
|
|
3990
|
+
return {
|
|
3991
|
+
ok: false,
|
|
3992
|
+
error: {
|
|
3993
|
+
code: "reflection-probe-input-invalid",
|
|
3994
|
+
field: `halfExtents[${index}]`,
|
|
3995
|
+
value,
|
|
3996
|
+
expected: "a finite number greater than zero"
|
|
3997
|
+
}
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
if (!Number.isFinite(input.priority)) {
|
|
4002
|
+
return {
|
|
4003
|
+
ok: false,
|
|
4004
|
+
error: {
|
|
4005
|
+
code: "reflection-probe-input-invalid",
|
|
4006
|
+
field: "priority",
|
|
4007
|
+
value: input.priority,
|
|
4008
|
+
expected: "a finite number"
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
if (!Number.isFinite(input.intensity) || input.intensity < 0) {
|
|
4013
|
+
return {
|
|
4014
|
+
ok: false,
|
|
4015
|
+
error: {
|
|
4016
|
+
code: "reflection-probe-input-invalid",
|
|
4017
|
+
field: "intensity",
|
|
4018
|
+
value: input.intensity,
|
|
4019
|
+
expected: "a finite number greater than or equal to zero"
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
if (!Number.isInteger(input.resolution) || input.resolution < 16 || (input.resolution & input.resolution - 1) !== 0) {
|
|
4024
|
+
return {
|
|
4025
|
+
ok: false,
|
|
4026
|
+
error: {
|
|
4027
|
+
code: "reflection-probe-input-invalid",
|
|
4028
|
+
field: "resolution",
|
|
4029
|
+
value: input.resolution,
|
|
4030
|
+
expected: "a power of two of at least 16 pixels"
|
|
4031
|
+
}
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
return { ok: true };
|
|
4035
|
+
}
|
|
4036
|
+
function estimateReflectionProbeBytes(resolution, limits = DEFAULT_REFLECTION_PROBE_LIMITS) {
|
|
4037
|
+
const maxResolution = limits.maxResolution ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxResolution;
|
|
4038
|
+
const clamped = Math.min(resolution, maxResolution);
|
|
4039
|
+
let filteredTexels = 0;
|
|
4040
|
+
for (let mip = 0; mip < 5; mip++) {
|
|
4041
|
+
const size = Math.max(1, Math.floor(clamped / 2 ** mip));
|
|
4042
|
+
filteredTexels += size * size;
|
|
4043
|
+
}
|
|
4044
|
+
return (clamped * clamped + 2 * filteredTexels) * 6 * REFLECTION_PROBE_BYTES_PER_TEXEL + 3 * clamped * clamped * 4;
|
|
4045
|
+
}
|
|
4046
|
+
function admitReflectionProbe(input, current, limits = DEFAULT_REFLECTION_PROBE_LIMITS) {
|
|
4047
|
+
const maxProbes = limits.maxProbes ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxProbes;
|
|
4048
|
+
const maxBytes = limits.maxBytes ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxBytes;
|
|
4049
|
+
const maxResolution = limits.maxResolution ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxResolution;
|
|
4050
|
+
const requested = estimateReflectionProbeBytes(input.resolution, { maxResolution });
|
|
4051
|
+
if (input.resolution > maxResolution || current.acceptedCount >= maxProbes || current.acceptedBytes + requested > maxBytes) {
|
|
4052
|
+
return {
|
|
4053
|
+
ok: false,
|
|
4054
|
+
error: {
|
|
4055
|
+
code: "reflection-probe-budget-exceeded",
|
|
4056
|
+
requested,
|
|
4057
|
+
activeCount: current.acceptedCount,
|
|
4058
|
+
activeBytes: current.acceptedBytes,
|
|
4059
|
+
maxProbes,
|
|
4060
|
+
maxBytes
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
return { ok: true, acceptedBytes: current.acceptedBytes + requested };
|
|
4065
|
+
}
|
|
4066
|
+
function resolveReflectionFallbackSource(selection, probeReady, skylightAvailable = true) {
|
|
4067
|
+
if (selection.kind === "probe" && probeReady) return "probe";
|
|
4068
|
+
if (skylightAvailable) return "skylight";
|
|
4069
|
+
return "neutral";
|
|
4070
|
+
}
|
|
4071
|
+
function reflectionFallbackCoverage(source, fact, selection) {
|
|
4072
|
+
if (source === "neutral") return 0;
|
|
4073
|
+
if (source === "skylight") return 1;
|
|
4074
|
+
if (fact === void 0 || selection.kind !== "probe") return 0;
|
|
4075
|
+
const normalized = Math.sqrt(Math.max(0, selection.normalizedDistance));
|
|
4076
|
+
return Math.max(0, Math.min(1, 1 - normalized / Math.sqrt(3)));
|
|
4077
|
+
}
|
|
4078
|
+
function reflectionFallbackSourceKey(source, selection, skylight) {
|
|
4079
|
+
if (source === "probe" && selection.kind === "probe") {
|
|
4080
|
+
return `probe:${selection.worldId}:${selection.entityKey}`;
|
|
4081
|
+
}
|
|
4082
|
+
if (source === "skylight" && skylight !== void 0) {
|
|
4083
|
+
return `skylight:${skylight.entityHandle}:${skylight.equirectHandle}`;
|
|
4084
|
+
}
|
|
4085
|
+
return source;
|
|
4086
|
+
}
|
|
4087
|
+
function reflectionFallbackProjectionSignature(input) {
|
|
4088
|
+
return JSON.stringify({
|
|
4089
|
+
renderableKey: input.renderableKey,
|
|
4090
|
+
sourceKey: input.sourceKey,
|
|
4091
|
+
source: input.source,
|
|
4092
|
+
coverage: input.coverage,
|
|
4093
|
+
extent: input.extent,
|
|
4094
|
+
brdfSignature: input.brdfSignature
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
function deriveReflectionFallbackProjection(input) {
|
|
4098
|
+
if (!Number.isFinite(input.coverage) || input.coverage < 0 || input.coverage > 1) {
|
|
4099
|
+
return {
|
|
4100
|
+
ok: false,
|
|
4101
|
+
error: {
|
|
4102
|
+
code: "reflection-fallback-input-invalid",
|
|
4103
|
+
field: "coverage",
|
|
4104
|
+
expected: "a finite number between zero and one"
|
|
4105
|
+
}
|
|
4106
|
+
};
|
|
4107
|
+
}
|
|
4108
|
+
if (input.extent?.some((value) => !Number.isFinite(value) || value <= 0)) {
|
|
4109
|
+
return {
|
|
4110
|
+
ok: false,
|
|
4111
|
+
error: {
|
|
4112
|
+
code: "reflection-fallback-input-invalid",
|
|
4113
|
+
field: "extent",
|
|
4114
|
+
expected: "three finite numbers greater than zero"
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
if (input.linearHdr.some((value) => !Number.isFinite(value))) {
|
|
4119
|
+
return {
|
|
4120
|
+
ok: false,
|
|
4121
|
+
error: {
|
|
4122
|
+
code: "reflection-fallback-input-invalid",
|
|
4123
|
+
field: "linearHdr",
|
|
4124
|
+
expected: "four finite linear HDR values"
|
|
4125
|
+
}
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
if (input.brdfSignature.length === 0) {
|
|
4129
|
+
return {
|
|
4130
|
+
ok: false,
|
|
4131
|
+
error: {
|
|
4132
|
+
code: "reflection-fallback-input-invalid",
|
|
4133
|
+
field: "brdfSignature",
|
|
4134
|
+
expected: "a non-empty BRDF signature"
|
|
4135
|
+
}
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4138
|
+
const linearHdr = input.source === "neutral" ? [0, 0, 0, 0] : [...input.linearHdr];
|
|
4139
|
+
return {
|
|
4140
|
+
ok: true,
|
|
4141
|
+
value: Object.freeze({
|
|
4142
|
+
...input.renderableKey === void 0 ? {} : { renderableKey: input.renderableKey },
|
|
4143
|
+
...input.sourceKey === void 0 ? {} : { sourceKey: input.sourceKey },
|
|
4144
|
+
source: input.source,
|
|
4145
|
+
coverage: input.coverage,
|
|
4146
|
+
...input.extent === void 0 ? {} : {
|
|
4147
|
+
extent: Object.freeze([...input.extent])
|
|
4148
|
+
},
|
|
4149
|
+
linearHdr: Object.freeze(linearHdr),
|
|
4150
|
+
brdfSignature: input.brdfSignature
|
|
4151
|
+
})
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
function contains(fact, point) {
|
|
4155
|
+
return fact.halfExtents.every((extent, axis) => {
|
|
4156
|
+
const center = fact.center[axis] ?? 0;
|
|
4157
|
+
const coordinate = point[axis] ?? 0;
|
|
4158
|
+
return Math.abs(coordinate - center) <= extent;
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
function normalizedDistance(fact, point) {
|
|
4162
|
+
let squared = 0;
|
|
4163
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
4164
|
+
const delta = (point[axis] ?? 0) - (fact.center[axis] ?? 0);
|
|
4165
|
+
const extent = fact.halfExtents[axis] ?? 1;
|
|
4166
|
+
squared += (delta / extent) ** 2;
|
|
4167
|
+
}
|
|
4168
|
+
return squared;
|
|
4169
|
+
}
|
|
4170
|
+
function isBetter(candidate, current, facts) {
|
|
4171
|
+
const candidateFact = facts.find(
|
|
4172
|
+
(fact) => fact.worldId === candidate.worldId && fact.entityKey === candidate.entityKey
|
|
4173
|
+
);
|
|
4174
|
+
const currentFact = facts.find(
|
|
4175
|
+
(fact) => fact.worldId === current.worldId && fact.entityKey === current.entityKey
|
|
4176
|
+
);
|
|
4177
|
+
if (candidateFact === void 0 || currentFact === void 0) return false;
|
|
4178
|
+
if (candidateFact.priority !== currentFact.priority)
|
|
4179
|
+
return candidateFact.priority > currentFact.priority;
|
|
4180
|
+
if (candidate.normalizedDistance !== current.normalizedDistance) {
|
|
4181
|
+
return candidate.normalizedDistance < current.normalizedDistance;
|
|
4182
|
+
}
|
|
4183
|
+
return candidate.worldId < current.worldId || candidate.worldId === current.worldId && candidate.entityKey < current.entityKey;
|
|
4184
|
+
}
|
|
4185
|
+
function selectReflectionProbe(facts, primitiveCenter) {
|
|
4186
|
+
let selected;
|
|
4187
|
+
for (const fact of facts) {
|
|
4188
|
+
if (!contains(fact, primitiveCenter)) continue;
|
|
4189
|
+
const candidate = {
|
|
4190
|
+
kind: "probe",
|
|
4191
|
+
worldId: fact.worldId,
|
|
4192
|
+
entityKey: fact.entityKey,
|
|
4193
|
+
normalizedDistance: normalizedDistance(fact, primitiveCenter)
|
|
4194
|
+
};
|
|
4195
|
+
if (selected === void 0 || isBetter(candidate, selected, facts)) selected = candidate;
|
|
4196
|
+
}
|
|
4197
|
+
return selected ?? { kind: "skylight" };
|
|
4198
|
+
}
|
|
4199
|
+
function primitiveKey(worldId, entityKey) {
|
|
4200
|
+
return `${worldId}:${entityKey}`;
|
|
4201
|
+
}
|
|
4202
|
+
var ReflectionProbeProjection = class {
|
|
4203
|
+
revision = 0;
|
|
4204
|
+
facts = [];
|
|
4205
|
+
selected = /* @__PURE__ */ new Map();
|
|
4206
|
+
signature = "";
|
|
4207
|
+
scannedPrimitives = 0;
|
|
4208
|
+
update(facts, primitives) {
|
|
4209
|
+
const nextFacts = facts.slice().sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey);
|
|
4210
|
+
const signature = JSON.stringify({ facts: nextFacts, primitives });
|
|
4211
|
+
if (signature === this.signature) return this.snapshot();
|
|
4212
|
+
this.signature = signature;
|
|
4213
|
+
this.facts = nextFacts;
|
|
4214
|
+
this.selected = /* @__PURE__ */ new Map();
|
|
4215
|
+
for (const primitive of primitives) {
|
|
4216
|
+
this.selected.set(
|
|
4217
|
+
primitive.renderableKey ?? primitiveKey(primitive.worldId, primitive.entityKey),
|
|
4218
|
+
selectReflectionProbe(nextFacts, primitive.center)
|
|
4219
|
+
);
|
|
4220
|
+
}
|
|
4221
|
+
this.scannedPrimitives = primitives.length;
|
|
4222
|
+
this.revision += 1;
|
|
4223
|
+
return this.snapshot();
|
|
4224
|
+
}
|
|
4225
|
+
snapshot() {
|
|
4226
|
+
return {
|
|
4227
|
+
revision: this.revision,
|
|
4228
|
+
facts: this.facts,
|
|
4229
|
+
selected: this.selected,
|
|
4230
|
+
scannedPrimitives: this.scannedPrimitives
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
selection(worldId, entityKey) {
|
|
4234
|
+
return this.selected.get(primitiveKey(worldId, entityKey)) ?? { kind: "skylight" };
|
|
4235
|
+
}
|
|
4236
|
+
};
|
|
4237
|
+
|
|
4238
|
+
// src/scene/visibility/inspection.ts
|
|
4239
|
+
var LOD_OCCLUSION_INSPECTION_SCHEMA = "forgeax::lod-occlusion-inspection::v2";
|
|
4240
|
+
var LOD_OCCLUSION_INSPECTION_MAX_BYTES = 16 * 1024;
|
|
4241
|
+
var MAX_INSPECTION_SAMPLES = 64;
|
|
4242
|
+
function nonNegativeInteger(name, value) {
|
|
4243
|
+
if (!Number.isInteger(value) || value < 0)
|
|
4244
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
4245
|
+
return value;
|
|
4246
|
+
}
|
|
4247
|
+
function copyError(error) {
|
|
4248
|
+
return Object.freeze({
|
|
4249
|
+
code: error.code,
|
|
4250
|
+
expected: error.expected,
|
|
4251
|
+
hint: error.hint,
|
|
4252
|
+
...error.detail === void 0 ? {} : { detail: Object.freeze({ ...error.detail }) }
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
function copyFallback(fallback) {
|
|
4256
|
+
return fallback.active ? Object.freeze({ ...fallback, error: copyError(fallback.error) }) : Object.freeze({ active: false });
|
|
4257
|
+
}
|
|
4258
|
+
function copySamples(samples) {
|
|
4259
|
+
return Object.freeze(
|
|
4260
|
+
[...samples].sort((left, right) => left.primitiveSlot - right.primitiveSlot).slice(0, MAX_INSPECTION_SAMPLES).map(
|
|
4261
|
+
(sample) => Object.freeze({
|
|
4262
|
+
primitiveSlot: nonNegativeInteger("sample.primitiveSlot", sample.primitiveSlot),
|
|
4263
|
+
level: nonNegativeInteger("sample.level", sample.level),
|
|
4264
|
+
visible: sample.visible
|
|
4265
|
+
})
|
|
4266
|
+
)
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
function copyWorldAttribution(attribution) {
|
|
4270
|
+
if (attribution === void 0) {
|
|
4271
|
+
return Object.freeze({ status: "unavailable", reason: "projection-only" });
|
|
4272
|
+
}
|
|
4273
|
+
if (attribution.status === "same-submit") {
|
|
4274
|
+
if (typeof attribution.submit.build !== "string" || attribution.submit.build.length === 0) {
|
|
4275
|
+
throw new Error("world.attribution.submit.build must be a non-empty string");
|
|
4276
|
+
}
|
|
4277
|
+
nonNegativeInteger("world.attribution.submit.frameId", attribution.submit.frameId);
|
|
4278
|
+
nonNegativeInteger(
|
|
4279
|
+
"world.attribution.submit.deviceGeneration",
|
|
4280
|
+
attribution.submit.deviceGeneration
|
|
4281
|
+
);
|
|
4282
|
+
return Object.freeze({
|
|
4283
|
+
status: "same-submit",
|
|
4284
|
+
submit: Object.freeze({ ...attribution.submit })
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
if (attribution.status !== "unavailable" || attribution.reason !== "projection-only") {
|
|
4288
|
+
throw new Error("world.attribution must declare same-submit or projection-only");
|
|
4289
|
+
}
|
|
4290
|
+
return Object.freeze({ status: "unavailable", reason: "projection-only" });
|
|
4291
|
+
}
|
|
4292
|
+
function copyRow(row) {
|
|
4293
|
+
for (const [name, value] of Object.entries({
|
|
4294
|
+
generation: row.generation,
|
|
4295
|
+
candidates: row.count.candidates,
|
|
4296
|
+
visible: row.count.visible,
|
|
4297
|
+
occluded: row.count.occluded,
|
|
4298
|
+
median: row.queryLatencyUs.median,
|
|
4299
|
+
p95: row.queryLatencyUs.p95,
|
|
4300
|
+
last: row.queryLatencyUs.last,
|
|
4301
|
+
pageUsed: row.pagePressure.used,
|
|
4302
|
+
pageCapacity: row.pagePressure.capacity
|
|
4303
|
+
}))
|
|
4304
|
+
nonNegativeInteger(`inspection.${name}`, value);
|
|
4305
|
+
if (row.view.cameraEntity < 0 || row.view.viewGeneration < 0 || row.slot.primitiveSlot < 0 || row.slot.slotGeneration < 0) {
|
|
4306
|
+
throw new Error("inspection identity values must be non-negative");
|
|
4307
|
+
}
|
|
4308
|
+
if (row.pagePressure.used > row.pagePressure.capacity) {
|
|
4309
|
+
throw new Error("inspection page pressure exceeds capacity");
|
|
4310
|
+
}
|
|
4311
|
+
return Object.freeze({
|
|
4312
|
+
root: Object.freeze({ ...row.root }),
|
|
4313
|
+
view: Object.freeze({ ...row.view }),
|
|
4314
|
+
slot: Object.freeze({ ...row.slot }),
|
|
4315
|
+
generation: row.generation,
|
|
4316
|
+
count: Object.freeze({ ...row.count }),
|
|
4317
|
+
lodHistogram: Object.freeze(
|
|
4318
|
+
row.lodHistogram.map(
|
|
4319
|
+
(histogram) => Object.freeze({
|
|
4320
|
+
level: nonNegativeInteger("histogram.level", histogram.level),
|
|
4321
|
+
count: nonNegativeInteger("histogram.count", histogram.count)
|
|
4322
|
+
})
|
|
4323
|
+
)
|
|
4324
|
+
),
|
|
4325
|
+
queryLatencyUs: Object.freeze({ ...row.queryLatencyUs }),
|
|
4326
|
+
pagePressure: Object.freeze({ ...row.pagePressure }),
|
|
4327
|
+
fallback: copyFallback(row.fallback),
|
|
4328
|
+
degradation: Object.freeze({ ...row.degradation }),
|
|
4329
|
+
samples: copySamples(row.samples)
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
function copyWorlds(worlds, fallback) {
|
|
4333
|
+
const source = worlds ?? [{ attachmentId: fallback.view.attachmentId, rows: [fallback] }];
|
|
4334
|
+
return Object.freeze(
|
|
4335
|
+
source.map(
|
|
4336
|
+
(world) => Object.freeze({
|
|
4337
|
+
attachmentId: world.attachmentId,
|
|
4338
|
+
rows: Object.freeze(world.rows.map((row) => copyRow(row))),
|
|
4339
|
+
attribution: copyWorldAttribution(world.attribution)
|
|
4340
|
+
})
|
|
4341
|
+
)
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
function inspectLodOcclusion(input) {
|
|
4345
|
+
const row = copyRow(input);
|
|
4346
|
+
const defaultBudget = createVisibilityBudget();
|
|
4347
|
+
const budget = input.budget ?? {
|
|
4348
|
+
configuredQueryBudget: defaultBudget.configuredQueryBudget,
|
|
4349
|
+
effectiveQueryBudget: defaultBudget.effectiveQueryBudget,
|
|
4350
|
+
settleSubmits: defaultBudget.settleSubmits,
|
|
4351
|
+
retestSubmits: defaultBudget.retestSubmits,
|
|
4352
|
+
expirySubmits: defaultBudget.expirySubmits
|
|
4353
|
+
};
|
|
4354
|
+
for (const [name, value] of Object.entries(budget))
|
|
4355
|
+
nonNegativeInteger(`inspection.budget.${name}`, value);
|
|
4356
|
+
const submit = input.submit ?? {
|
|
4357
|
+
frameId: input.generation,
|
|
4358
|
+
build: "unknown",
|
|
4359
|
+
deviceGeneration: 0
|
|
4360
|
+
};
|
|
4361
|
+
nonNegativeInteger("inspection.submit.frameId", submit.frameId);
|
|
4362
|
+
nonNegativeInteger("inspection.submit.deviceGeneration", submit.deviceGeneration);
|
|
4363
|
+
if (typeof submit.build !== "string" || submit.build.length === 0) {
|
|
4364
|
+
throw new Error("inspection.submit.build must be a non-empty string");
|
|
4365
|
+
}
|
|
4366
|
+
const worlds = copyWorlds(input.worlds, row);
|
|
4367
|
+
for (const world of worlds) {
|
|
4368
|
+
if (world.attribution.status === "same-submit" && (world.attribution.submit.frameId !== submit.frameId || world.attribution.submit.build !== submit.build || world.attribution.submit.deviceGeneration !== submit.deviceGeneration)) {
|
|
4369
|
+
throw new Error("world attribution must reference the parent inspection submit");
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
const inspection = Object.freeze({
|
|
4373
|
+
schema: LOD_OCCLUSION_INSPECTION_SCHEMA,
|
|
4374
|
+
...row,
|
|
4375
|
+
submit: Object.freeze({ ...submit }),
|
|
4376
|
+
budget: Object.freeze({ ...budget }),
|
|
4377
|
+
worlds
|
|
4378
|
+
});
|
|
4379
|
+
if (JSON.stringify(inspection).length > LOD_OCCLUSION_INSPECTION_MAX_BYTES) {
|
|
4380
|
+
throw new Error(`LOD occlusion inspection exceeds ${LOD_OCCLUSION_INSPECTION_MAX_BYTES} bytes`);
|
|
4381
|
+
}
|
|
4382
|
+
return inspection;
|
|
4383
|
+
}
|
|
4384
|
+
function serializeLodOcclusionInspection(inspection) {
|
|
4385
|
+
const serialized = JSON.stringify(inspection);
|
|
4386
|
+
if (serialized.length > LOD_OCCLUSION_INSPECTION_MAX_BYTES) {
|
|
4387
|
+
throw new Error(`LOD occlusion inspection exceeds ${LOD_OCCLUSION_INSPECTION_MAX_BYTES} bytes`);
|
|
4388
|
+
}
|
|
4389
|
+
return serialized;
|
|
4390
|
+
}
|
|
4391
|
+
function consumeLodOcclusionInspection(inspection) {
|
|
4392
|
+
if (!inspection.fallback.active) return { action: "none" };
|
|
4393
|
+
const { reason, error } = inspection.fallback;
|
|
4394
|
+
const action = error.code.startsWith("asset-") || reason === "producer-failed" ? "rebuild" : reason === "query-failed" || reason === "page-exhausted" || reason === "device-loss" ? "retry" : "cold-cook";
|
|
4395
|
+
return { action, sourceKey: inspection.root.sourceKey, reason };
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
// src/ssr/inspection.ts
|
|
4399
|
+
var SSR_INSPECTION_MAX_PASSES = 64;
|
|
4400
|
+
function nonNegativeSafeInteger(name, value) {
|
|
4401
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4402
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
4403
|
+
}
|
|
4404
|
+
return value;
|
|
4405
|
+
}
|
|
4406
|
+
function copyHistory(history) {
|
|
4407
|
+
nonNegativeSafeInteger("SSR inspection history.bytes", history.bytes);
|
|
4408
|
+
nonNegativeSafeInteger("SSR inspection history.resetCount", history.resetCount);
|
|
4409
|
+
return Object.freeze({
|
|
4410
|
+
state: history.state,
|
|
4411
|
+
bytes: history.bytes,
|
|
4412
|
+
resetCount: history.resetCount
|
|
4413
|
+
});
|
|
4414
|
+
}
|
|
4415
|
+
function copyCoverage(coverage) {
|
|
4416
|
+
const result = {
|
|
4417
|
+
hitCount: coverage?.hitCount ?? null,
|
|
4418
|
+
fallbackCount: coverage?.fallbackCount ?? null,
|
|
4419
|
+
excludedCount: coverage?.excludedCount ?? null
|
|
4420
|
+
};
|
|
4421
|
+
for (const [key, value] of Object.entries(result)) {
|
|
4422
|
+
if (value !== null) nonNegativeSafeInteger(`SSR inspection coverage.${key}`, value);
|
|
4423
|
+
}
|
|
4424
|
+
return Object.freeze(result);
|
|
4425
|
+
}
|
|
4426
|
+
function copyPassRoster(passRoster) {
|
|
4427
|
+
return Object.freeze(
|
|
4428
|
+
[...passRoster ?? []].slice(0, SSR_INSPECTION_MAX_PASSES).map((name, index) => {
|
|
4429
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
4430
|
+
throw new Error(`SSR inspection passRoster[${index}] must be a non-empty string`);
|
|
4431
|
+
}
|
|
4432
|
+
return name;
|
|
4433
|
+
})
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
function projectSsrSpatialInspection(admission, projection = {}) {
|
|
4437
|
+
const config = admission.config;
|
|
4438
|
+
return Object.freeze({
|
|
4439
|
+
status: projection.status ?? admission.status,
|
|
4440
|
+
lane: admission.lane,
|
|
4441
|
+
config: config === void 0 ? void 0 : Object.freeze({
|
|
4442
|
+
maxDistance: config.maxDistance,
|
|
4443
|
+
thickness: config.thickness,
|
|
4444
|
+
maxRoughness: config.maxRoughness
|
|
4445
|
+
}),
|
|
4446
|
+
viewRange: admission.viewRange,
|
|
4447
|
+
coverage: copyCoverage(projection.coverage),
|
|
4448
|
+
history: copyHistory(
|
|
4449
|
+
projection.history ?? {
|
|
4450
|
+
state: "not-owned",
|
|
4451
|
+
bytes: 0,
|
|
4452
|
+
resetCount: 0
|
|
4453
|
+
}
|
|
4454
|
+
),
|
|
4455
|
+
work: Object.freeze({ ...admission.work }),
|
|
4456
|
+
passRoster: copyPassRoster(projection.passRoster),
|
|
4457
|
+
fallbackSource: projection.fallbackSource,
|
|
4458
|
+
failure: projection.failure ?? admission.failure
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4461
|
+
function serializeSsrSpatialInspection(inspection) {
|
|
4462
|
+
return JSON.stringify(inspection);
|
|
4463
|
+
}
|
|
4464
|
+
var DynamicGeometryError = class extends Error {
|
|
4465
|
+
code;
|
|
4466
|
+
expected;
|
|
4467
|
+
hint;
|
|
4468
|
+
detail;
|
|
4469
|
+
constructor(code, expected, hint, detail = {}) {
|
|
4470
|
+
super(`${code}: ${expected}`);
|
|
4471
|
+
this.name = "DynamicGeometryError";
|
|
4472
|
+
this.code = code;
|
|
4473
|
+
this.expected = expected;
|
|
4474
|
+
this.hint = hint;
|
|
4475
|
+
this.detail = Object.freeze({ code, ...detail });
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
function sameCandidateCredential(candidate, committed) {
|
|
4479
|
+
return candidate.candidateId === committed.candidateId && candidate.owner === committed.owner && candidate.world === committed.world && candidate.generation === committed.generation && candidate.entity === committed.entity && candidate.physicsEntity === committed.physicsEntity && candidate.gpuReady === committed.gpuReady && candidate.meshBytes === committed.meshBytes && candidate.revision === committed.revision && candidate.topologyRevision === committed.topologyRevision && candidate.materialIdentity === committed.materialIdentity && candidate.meshHandle === committed.meshHandle && candidate.fixedStep === committed.fixedStep;
|
|
4480
|
+
}
|
|
4481
|
+
function cloneTypedArray(value) {
|
|
4482
|
+
return new value.constructor(value);
|
|
4483
|
+
}
|
|
4484
|
+
function cloneMesh(mesh) {
|
|
4485
|
+
const attributes = Object.fromEntries(
|
|
4486
|
+
Object.entries(mesh.attributes).map(([key, value]) => [
|
|
4487
|
+
key,
|
|
4488
|
+
value instanceof ArrayBuffer ? value.slice(0) : value === void 0 ? void 0 : cloneTypedArray(value)
|
|
4489
|
+
])
|
|
4490
|
+
);
|
|
4491
|
+
return Object.freeze({
|
|
4492
|
+
...mesh,
|
|
4493
|
+
vertices: new Float32Array(mesh.vertices),
|
|
4494
|
+
...mesh.indices === void 0 ? {} : { indices: cloneTypedArray(mesh.indices) },
|
|
4495
|
+
attributes,
|
|
4496
|
+
...mesh.aabb === void 0 ? {} : { aabb: new Float32Array(mesh.aabb) },
|
|
4497
|
+
submeshes: Object.freeze(mesh.submeshes.map((submesh) => Object.freeze({ ...submesh }))),
|
|
4498
|
+
materialSlots: Object.freeze(mesh.materialSlots.map((slot) => Object.freeze({ ...slot })))
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
function cloneCandidate(candidate) {
|
|
4502
|
+
return Object.freeze({ ...candidate, mesh: cloneMesh(candidate.mesh) });
|
|
4503
|
+
}
|
|
4504
|
+
function validateMesh(mesh) {
|
|
4505
|
+
if (mesh.kind !== "mesh") return "MeshAsset.kind must be mesh";
|
|
4506
|
+
if (!(mesh.vertices instanceof Float32Array) || mesh.vertices.length === 0) {
|
|
4507
|
+
return "MeshAsset.vertices must be a non-empty Float32Array";
|
|
4508
|
+
}
|
|
4509
|
+
if (mesh.indices !== void 0 && !(mesh.indices instanceof Uint16Array || mesh.indices instanceof Uint32Array)) {
|
|
4510
|
+
return "MeshAsset.indices must be Uint16Array or Uint32Array";
|
|
4511
|
+
}
|
|
4512
|
+
if (mesh.submeshes.length === 0)
|
|
4513
|
+
return "MeshAsset.submeshes must contain at least one topology range";
|
|
4514
|
+
if (mesh.submeshes.some(
|
|
4515
|
+
(submesh) => !Number.isSafeInteger(submesh.materialSlot) || submesh.materialSlot < 0 || submesh.materialSlot >= mesh.materialSlots.length
|
|
4516
|
+
)) {
|
|
4517
|
+
return "MeshAsset.materialSlots must cover every referenced materialSlot";
|
|
4518
|
+
}
|
|
4519
|
+
if (mesh.vertices.some((value) => !Number.isFinite(value)))
|
|
4520
|
+
return "MeshAsset.vertices must be finite";
|
|
4521
|
+
return void 0;
|
|
4522
|
+
}
|
|
4523
|
+
function meshByteSize(mesh) {
|
|
4524
|
+
let bytes = mesh.vertices.byteLength + (mesh.indices?.byteLength ?? 0);
|
|
4525
|
+
for (const value of Object.values(mesh.attributes)) {
|
|
4526
|
+
if (value instanceof ArrayBuffer) bytes += value.byteLength;
|
|
4527
|
+
else if (value !== void 0 && ArrayBuffer.isView(value)) bytes += value.byteLength;
|
|
4528
|
+
}
|
|
4529
|
+
if (mesh.aabb !== void 0) bytes += mesh.aabb.byteLength;
|
|
4530
|
+
return bytes;
|
|
4531
|
+
}
|
|
4532
|
+
function createDynamicGeometryLifecycle(maxCandidates = 32, maxMeshBytes = 64 * 1024 * 1024) {
|
|
4533
|
+
const records = /* @__PURE__ */ new Map();
|
|
4534
|
+
const owner = {};
|
|
4535
|
+
let sequence = 0;
|
|
4536
|
+
let currentGeneration = 0;
|
|
4537
|
+
let disposed = false;
|
|
4538
|
+
let historyInvalidations = 0;
|
|
4539
|
+
let latestRevision;
|
|
4540
|
+
let latestFrameId;
|
|
4541
|
+
let retired = 0;
|
|
4542
|
+
let invalidated = 0;
|
|
4543
|
+
let meshBytes = 0;
|
|
4544
|
+
const latestRevisionByWorld = /* @__PURE__ */ new WeakMap();
|
|
4545
|
+
const latestTopologyRevisionByWorld = /* @__PURE__ */ new WeakMap();
|
|
4546
|
+
const publishedReceipts = /* @__PURE__ */ new Map();
|
|
4547
|
+
const failure3 = (code, expected, hint, detail = {}) => err(new DynamicGeometryError(code, expected, hint, detail));
|
|
4548
|
+
const current = (candidate) => {
|
|
4549
|
+
const record = records.get(candidate.candidateId);
|
|
4550
|
+
if (record === void 0 || candidate.owner !== owner || record.candidate.owner !== candidate.owner || record.candidate.world !== candidate.world || record.candidate.generation !== candidate.generation)
|
|
4551
|
+
return void 0;
|
|
4552
|
+
return record;
|
|
4553
|
+
};
|
|
4554
|
+
return {
|
|
4555
|
+
prepare(input, generation) {
|
|
4556
|
+
if (disposed)
|
|
4557
|
+
return failure3("dynamic-geometry-disposed", "renderer is alive", "rebuild the Renderer");
|
|
4558
|
+
if (input.world === null || typeof input.world !== "object") {
|
|
4559
|
+
return failure3(
|
|
4560
|
+
"dynamic-geometry-world-not-attached",
|
|
4561
|
+
"candidate belongs to an attached World",
|
|
4562
|
+
"attach the World before preparing geometry"
|
|
4563
|
+
);
|
|
4564
|
+
}
|
|
4565
|
+
if (!Number.isInteger(input.revision) || input.revision < 0) {
|
|
4566
|
+
return failure3(
|
|
4567
|
+
"dynamic-geometry-invalid",
|
|
4568
|
+
"geometry revision is a non-negative integer",
|
|
4569
|
+
"advance the consumer revision monotonically",
|
|
4570
|
+
{ revision: input.revision }
|
|
4571
|
+
);
|
|
4572
|
+
}
|
|
4573
|
+
if (generation !== currentGeneration && currentGeneration !== 0) {
|
|
4574
|
+
return failure3(
|
|
4575
|
+
"dynamic-geometry-generation-mismatch",
|
|
4576
|
+
"candidate uses the active renderer generation",
|
|
4577
|
+
"discard stale generation work and prepare again",
|
|
4578
|
+
{ generation, actual: currentGeneration }
|
|
4579
|
+
);
|
|
4580
|
+
}
|
|
4581
|
+
if (records.size >= maxCandidates) {
|
|
4582
|
+
return failure3(
|
|
4583
|
+
"dynamic-geometry-budget-exceeded",
|
|
4584
|
+
"candidate count remains within the bounded renderer budget",
|
|
4585
|
+
"cancel or retire an earlier candidate before retrying",
|
|
4586
|
+
{ actual: records.size }
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4589
|
+
const invalid2 = validateMesh(input.mesh);
|
|
4590
|
+
if (invalid2 !== void 0)
|
|
4591
|
+
return failure3(
|
|
4592
|
+
"dynamic-geometry-invalid",
|
|
4593
|
+
"MeshAsset satisfies the standard geometry contract",
|
|
4594
|
+
invalid2
|
|
4595
|
+
);
|
|
4596
|
+
const candidateBytes = meshByteSize(input.mesh);
|
|
4597
|
+
if (!Number.isSafeInteger(candidateBytes) || candidateBytes < 0 || meshBytes > maxMeshBytes - candidateBytes) {
|
|
4598
|
+
return failure3(
|
|
4599
|
+
"dynamic-geometry-budget-exceeded",
|
|
4600
|
+
"candidate mesh bytes remain within the bounded renderer resource budget",
|
|
4601
|
+
"cancel or retire an earlier candidate before retrying",
|
|
4602
|
+
{ actual: { meshBytes, candidateBytes, maxMeshBytes } }
|
|
4603
|
+
);
|
|
4604
|
+
}
|
|
4605
|
+
const candidateId = `mesh:${generation}:${++sequence}:${input.revision}`;
|
|
4606
|
+
const candidate = Object.freeze({
|
|
4607
|
+
candidateId,
|
|
4608
|
+
generation,
|
|
4609
|
+
owner,
|
|
4610
|
+
world: input.world,
|
|
4611
|
+
...input.entity === void 0 ? {} : { entity: input.entity },
|
|
4612
|
+
...input.physicsEntity === void 0 ? {} : { physicsEntity: input.physicsEntity },
|
|
4613
|
+
gpuReady: input.meshHandle !== void 0,
|
|
4614
|
+
meshBytes: candidateBytes,
|
|
4615
|
+
revision: input.revision,
|
|
4616
|
+
topologyRevision: input.topologyRevision ?? input.revision,
|
|
4617
|
+
...input.materialIdentity === void 0 ? {} : { materialIdentity: input.materialIdentity },
|
|
4618
|
+
...input.meshHandle === void 0 ? {} : { meshHandle: input.meshHandle },
|
|
4619
|
+
...input.fixedStep === void 0 ? {} : { fixedStep: input.fixedStep },
|
|
4620
|
+
mesh: cloneMesh(input.mesh),
|
|
4621
|
+
state: "prepared"
|
|
4622
|
+
});
|
|
4623
|
+
records.set(candidateId, {
|
|
4624
|
+
world: input.world,
|
|
4625
|
+
candidate,
|
|
4626
|
+
historyInvalidated: false
|
|
4627
|
+
});
|
|
4628
|
+
meshBytes += candidateBytes;
|
|
4629
|
+
currentGeneration = generation;
|
|
4630
|
+
return ok(cloneCandidate(candidate));
|
|
4631
|
+
},
|
|
4632
|
+
accept(candidate, ordering) {
|
|
4633
|
+
if (disposed)
|
|
4634
|
+
return failure3("dynamic-geometry-disposed", "renderer is alive", "rebuild the Renderer");
|
|
4635
|
+
const record = current(candidate);
|
|
4636
|
+
if (record === void 0)
|
|
4637
|
+
return failure3(
|
|
4638
|
+
"dynamic-geometry-candidate-not-found",
|
|
4639
|
+
"candidate is owned by this renderer",
|
|
4640
|
+
"discard the stale credential"
|
|
4641
|
+
);
|
|
4642
|
+
if (candidate.generation !== currentGeneration) {
|
|
4643
|
+
return failure3(
|
|
4644
|
+
"dynamic-geometry-generation-mismatch",
|
|
4645
|
+
"candidate generation matches the active renderer generation",
|
|
4646
|
+
"prepare against the current renderer generation",
|
|
4647
|
+
{ candidateId: candidate.candidateId }
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4650
|
+
if (record.candidate.state !== "prepared") {
|
|
4651
|
+
return failure3(
|
|
4652
|
+
"dynamic-geometry-candidate-state",
|
|
4653
|
+
"candidate is prepared exactly once before acceptance",
|
|
4654
|
+
"retain the accepted or published receipt",
|
|
4655
|
+
{ candidateId: candidate.candidateId }
|
|
4656
|
+
);
|
|
4657
|
+
}
|
|
4658
|
+
if (!sameCandidateCredential(candidate, record.candidate)) {
|
|
4659
|
+
return failure3(
|
|
4660
|
+
"dynamic-geometry-receipt-mismatch",
|
|
4661
|
+
"acceptance uses the exact prepared candidate credential",
|
|
4662
|
+
"discard the altered credential and use the value returned by prepare",
|
|
4663
|
+
{ candidateId: candidate.candidateId }
|
|
4664
|
+
);
|
|
4665
|
+
}
|
|
4666
|
+
if (ordering !== void 0) {
|
|
4667
|
+
if (ordering.world !== record.world || !Number.isInteger(ordering.fixedStep) || ordering.fixedStep < 0) {
|
|
4668
|
+
return failure3(
|
|
4669
|
+
"dynamic-geometry-ordering-required",
|
|
4670
|
+
"acceptance ordering names the candidate World and a non-negative fixed step",
|
|
4671
|
+
"submit the PhysicsWorld publication ordering for the same attached World",
|
|
4672
|
+
{ candidateId: candidate.candidateId, actual: ordering }
|
|
4673
|
+
);
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
const newest = [...records.values()].filter(
|
|
4677
|
+
(entry) => entry.candidate.candidateId !== candidate.candidateId && entry.world === record.world && entry.candidate.entity === record.candidate.entity && (entry.candidate.state === "prepared" || entry.candidate.state === "accepted")
|
|
4678
|
+
).map((entry) => entry.candidate.revision).concat(latestRevisionByWorld.get(record.world)?.get(record.candidate.entity) ?? -1).sort((a, b) => b - a)[0];
|
|
4679
|
+
if (newest !== void 0 && record.candidate.revision <= newest) {
|
|
4680
|
+
return failure3(
|
|
4681
|
+
"dynamic-geometry-stale",
|
|
4682
|
+
"accepted geometry revision is newer than the active candidate",
|
|
4683
|
+
"advance topology revision before accepting",
|
|
4684
|
+
{ candidateId: candidate.candidateId }
|
|
4685
|
+
);
|
|
4686
|
+
}
|
|
4687
|
+
const accepted = Object.freeze({
|
|
4688
|
+
...record.candidate,
|
|
4689
|
+
...ordering === void 0 ? {} : { fixedStep: ordering.fixedStep },
|
|
4690
|
+
state: "accepted"
|
|
4691
|
+
});
|
|
4692
|
+
const previousTopologies = [...records.values()].filter(
|
|
4693
|
+
(entry) => entry.candidate.candidateId !== candidate.candidateId && entry.world === record.world && entry.candidate.entity === record.candidate.entity
|
|
4694
|
+
).map((entry) => entry.candidate.topologyRevision);
|
|
4695
|
+
const previousLatestTopology = latestTopologyRevisionByWorld.get(record.world)?.get(record.candidate.entity);
|
|
4696
|
+
const newestTopology = previousTopologies.concat(previousLatestTopology === void 0 ? [] : [previousLatestTopology]).sort((a, b) => b - a)[0];
|
|
4697
|
+
if (newestTopology !== void 0 && accepted.topologyRevision < newestTopology) {
|
|
4698
|
+
return failure3(
|
|
4699
|
+
"dynamic-geometry-stale",
|
|
4700
|
+
"accepted topology revision is not older than the active geometry history",
|
|
4701
|
+
"advance topology revision before accepting a replacement candidate",
|
|
4702
|
+
{ candidateId: candidate.candidateId }
|
|
4703
|
+
);
|
|
4704
|
+
}
|
|
4705
|
+
if (newestTopology !== void 0 && accepted.topologyRevision > newestTopology) {
|
|
4706
|
+
historyInvalidations += 1;
|
|
4707
|
+
for (const previous of records.values()) {
|
|
4708
|
+
if (previous.world !== record.world || previous.candidate.entity !== record.candidate.entity || previous.candidate.candidateId === candidate.candidateId || previous.candidate.topologyRevision >= accepted.topologyRevision)
|
|
4709
|
+
continue;
|
|
4710
|
+
if (!previous.historyInvalidated) {
|
|
4711
|
+
previous.historyInvalidated = true;
|
|
4712
|
+
invalidated += 1;
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
record.candidate = accepted;
|
|
4717
|
+
return ok(cloneCandidate(accepted));
|
|
4718
|
+
},
|
|
4719
|
+
cancel(candidate, completion) {
|
|
4720
|
+
const record = current(candidate);
|
|
4721
|
+
if (record === void 0)
|
|
4722
|
+
return failure3(
|
|
4723
|
+
"dynamic-geometry-candidate-not-found",
|
|
4724
|
+
"candidate is owned by this renderer",
|
|
4725
|
+
"discard the stale credential"
|
|
4726
|
+
);
|
|
4727
|
+
if (!sameCandidateCredential(candidate, record.candidate))
|
|
4728
|
+
return failure3(
|
|
4729
|
+
"dynamic-geometry-receipt-mismatch",
|
|
4730
|
+
"cancellation uses the exact prepared candidate credential",
|
|
4731
|
+
"discard the altered credential and use the value returned by prepare",
|
|
4732
|
+
{ candidateId: candidate.candidateId }
|
|
4733
|
+
);
|
|
4734
|
+
if (record.candidate.state !== "prepared" && record.candidate.state !== "accepted") {
|
|
4735
|
+
return failure3(
|
|
4736
|
+
"dynamic-geometry-candidate-state",
|
|
4737
|
+
"only prepared or accepted geometry can be cancelled once",
|
|
4738
|
+
"retire published geometry or await the existing terminal cleanup"
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
record.candidate = Object.freeze({ ...record.candidate, state: "cancelled" });
|
|
4742
|
+
const release = () => {
|
|
4743
|
+
if (records.get(candidate.candidateId) !== record) return;
|
|
4744
|
+
records.delete(candidate.candidateId);
|
|
4745
|
+
meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
|
|
4746
|
+
};
|
|
4747
|
+
if (completion === void 0) release();
|
|
4748
|
+
else void completion.then(release, release);
|
|
4749
|
+
return ok(void 0);
|
|
4750
|
+
},
|
|
4751
|
+
retire(candidate) {
|
|
4752
|
+
const record = current(candidate);
|
|
4753
|
+
if (record === void 0)
|
|
4754
|
+
return failure3(
|
|
4755
|
+
"dynamic-geometry-candidate-not-found",
|
|
4756
|
+
"retirement uses a published candidate owned by this renderer",
|
|
4757
|
+
"discard the stale credential and do not evict any unrelated mesh",
|
|
4758
|
+
{ candidateId: candidate.candidateId }
|
|
4759
|
+
);
|
|
4760
|
+
if (!sameCandidateCredential(candidate, record.candidate))
|
|
4761
|
+
return failure3(
|
|
4762
|
+
"dynamic-geometry-receipt-mismatch",
|
|
4763
|
+
"retirement uses the exact published candidate credential",
|
|
4764
|
+
"discard the altered credential and use the value returned by publish",
|
|
4765
|
+
{ candidateId: candidate.candidateId }
|
|
4766
|
+
);
|
|
4767
|
+
if (record.candidate.state !== "published") {
|
|
4768
|
+
return failure3(
|
|
4769
|
+
"dynamic-geometry-candidate-state",
|
|
4770
|
+
"only published geometry can enter receipt-bound retirement",
|
|
4771
|
+
"publish the candidate from a submitted frame first"
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
record.candidate = Object.freeze({ ...record.candidate, state: "retired" });
|
|
4775
|
+
publishedReceipts.delete(candidate.candidateId);
|
|
4776
|
+
retired += 1;
|
|
4777
|
+
return ok(void 0);
|
|
4778
|
+
},
|
|
4779
|
+
finalizeRetirement(candidate) {
|
|
4780
|
+
const record = current(candidate);
|
|
4781
|
+
if (record === void 0)
|
|
4782
|
+
return failure3(
|
|
4783
|
+
"dynamic-geometry-candidate-not-found",
|
|
4784
|
+
"retirement finalization uses a candidate retained by this lifecycle",
|
|
4785
|
+
"discard the stale credential or finish the current receipt-bound retirement"
|
|
4786
|
+
);
|
|
4787
|
+
if (!sameCandidateCredential(candidate, record.candidate))
|
|
4788
|
+
return failure3(
|
|
4789
|
+
"dynamic-geometry-receipt-mismatch",
|
|
4790
|
+
"retirement finalization uses the exact published candidate credential",
|
|
4791
|
+
"discard the altered credential and use the value returned by publication",
|
|
4792
|
+
{ candidateId: candidate.candidateId }
|
|
4793
|
+
);
|
|
4794
|
+
if (record.candidate.state !== "retired")
|
|
4795
|
+
return failure3(
|
|
4796
|
+
"dynamic-geometry-candidate-state",
|
|
4797
|
+
"only a retired candidate can release its deferred budget",
|
|
4798
|
+
"retire the published candidate before finalizing its receipt-bound cleanup",
|
|
4799
|
+
{ candidateId: candidate.candidateId }
|
|
4800
|
+
);
|
|
4801
|
+
records.delete(candidate.candidateId);
|
|
4802
|
+
meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
|
|
4803
|
+
return ok(void 0);
|
|
4804
|
+
},
|
|
4805
|
+
publishFrame(frame, worlds, fixedStep, canPublish, recordStageLane) {
|
|
4806
|
+
if (disposed || frame.deviceGeneration !== currentGeneration) return [];
|
|
4807
|
+
const receipts = [];
|
|
4808
|
+
for (const record of records.values()) {
|
|
4809
|
+
const canRepeatPublished = record.candidate.state === "published" && // Attached Renderer publication revalidates the entity binding in
|
|
4810
|
+
// `canPublish`, so a history-invalidated candidate can still record
|
|
4811
|
+
// a real draw for its live entity. Detached lifecycle publication
|
|
4812
|
+
// remains one-shot once that history is invalidated.
|
|
4813
|
+
(canPublish !== void 0 || !record.historyInvalidated);
|
|
4814
|
+
if (record.candidate.state !== "accepted" && !canRepeatPublished) continue;
|
|
4815
|
+
if (worlds !== void 0 && !worlds.includes(record.world)) continue;
|
|
4816
|
+
if (record.candidate.fixedStep !== void 0 && (fixedStep === void 0 || record.candidate.fixedStep > fixedStep)) {
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4819
|
+
if (canPublish !== void 0 && !canPublish(record.candidate)) continue;
|
|
4820
|
+
const consumedLane = canPublish === void 0 ? void 0 : recordStageLane?.(record.candidate);
|
|
4821
|
+
record.candidate = Object.freeze({ ...record.candidate, state: "published" });
|
|
4822
|
+
const receipt = Object.freeze({
|
|
4823
|
+
candidateId: record.candidate.candidateId,
|
|
4824
|
+
generation: record.candidate.generation,
|
|
4825
|
+
revision: record.candidate.revision,
|
|
4826
|
+
topologyRevision: record.candidate.topologyRevision,
|
|
4827
|
+
...record.candidate.fixedStep === void 0 ? {} : { fixedStep: record.candidate.fixedStep },
|
|
4828
|
+
frameId: frame.frameId,
|
|
4829
|
+
deviceGeneration: frame.deviceGeneration,
|
|
4830
|
+
recordStageConsumed: canPublish !== void 0,
|
|
4831
|
+
...consumedLane === void 0 ? {} : { recordStageLane: consumedLane },
|
|
4832
|
+
...fixedStep === void 0 ? {} : { publicationFixedStep: fixedStep },
|
|
4833
|
+
frame: Object.freeze({ ...frame })
|
|
4834
|
+
});
|
|
4835
|
+
receipts.push(receipt);
|
|
4836
|
+
publishedReceipts.set(record.candidate.candidateId, receipt);
|
|
4837
|
+
latestRevision = record.candidate.revision;
|
|
4838
|
+
latestFrameId = frame.frameId;
|
|
4839
|
+
if (!latestRevisionByWorld.has(record.world))
|
|
4840
|
+
latestRevisionByWorld.set(record.world, /* @__PURE__ */ new Map());
|
|
4841
|
+
latestRevisionByWorld.get(record.world)?.set(record.candidate.entity, record.candidate.revision);
|
|
4842
|
+
if (!latestTopologyRevisionByWorld.has(record.world))
|
|
4843
|
+
latestTopologyRevisionByWorld.set(record.world, /* @__PURE__ */ new Map());
|
|
4844
|
+
latestTopologyRevisionByWorld.get(record.world)?.set(record.candidate.entity, record.candidate.topologyRevision);
|
|
4845
|
+
}
|
|
4846
|
+
return receipts;
|
|
4847
|
+
},
|
|
4848
|
+
invalidateWorld(world, completion) {
|
|
4849
|
+
for (const [id, record] of records) {
|
|
4850
|
+
if (record.world !== world) continue;
|
|
4851
|
+
if (record.candidate.state === "cancelled" || record.candidate.state === "retired")
|
|
4852
|
+
continue;
|
|
4853
|
+
publishedReceipts.delete(id);
|
|
4854
|
+
record.candidate = Object.freeze({ ...record.candidate, state: "cancelled" });
|
|
4855
|
+
const release = () => {
|
|
4856
|
+
if (records.get(id) !== record) return;
|
|
4857
|
+
records.delete(id);
|
|
4858
|
+
meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
|
|
4859
|
+
};
|
|
4860
|
+
if (completion === void 0) release();
|
|
4861
|
+
else void completion.then(release, release);
|
|
4862
|
+
invalidated += 1;
|
|
4863
|
+
}
|
|
4864
|
+
latestRevisionByWorld.delete(world);
|
|
4865
|
+
latestTopologyRevisionByWorld.delete(world);
|
|
4866
|
+
},
|
|
4867
|
+
invalidateGeneration(generation) {
|
|
4868
|
+
if (generation === currentGeneration) return;
|
|
4869
|
+
currentGeneration = generation;
|
|
4870
|
+
const invalidatedWorlds = /* @__PURE__ */ new Set();
|
|
4871
|
+
for (const [id, record] of records) {
|
|
4872
|
+
if (record.candidate.generation !== generation) {
|
|
4873
|
+
records.delete(id);
|
|
4874
|
+
publishedReceipts.delete(id);
|
|
4875
|
+
meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
|
|
4876
|
+
invalidatedWorlds.add(record.world);
|
|
4877
|
+
invalidated += 1;
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
for (const world of invalidatedWorlds) {
|
|
4881
|
+
latestRevisionByWorld.delete(world);
|
|
4882
|
+
latestTopologyRevisionByWorld.delete(world);
|
|
4883
|
+
}
|
|
4884
|
+
},
|
|
4885
|
+
receipt(candidate) {
|
|
4886
|
+
const receipt = publishedReceipts.get(candidate.candidateId);
|
|
4887
|
+
const record = records.get(candidate.candidateId);
|
|
4888
|
+
if (record === void 0 || !sameCandidateCredential(candidate, record.candidate)) {
|
|
4889
|
+
return void 0;
|
|
4890
|
+
}
|
|
4891
|
+
return receipt?.generation === candidate.generation ? receipt : void 0;
|
|
4892
|
+
},
|
|
4893
|
+
inspect() {
|
|
4894
|
+
let prepared = 0;
|
|
4895
|
+
let accepted = 0;
|
|
4896
|
+
let published = 0;
|
|
4897
|
+
for (const record of records.values()) {
|
|
4898
|
+
if (record.candidate.state === "prepared") prepared += 1;
|
|
4899
|
+
else if (record.candidate.state === "accepted") accepted += 1;
|
|
4900
|
+
else if (record.candidate.state === "published") published += 1;
|
|
4901
|
+
}
|
|
4902
|
+
return Object.freeze({
|
|
4903
|
+
generation: currentGeneration,
|
|
4904
|
+
prepared,
|
|
4905
|
+
accepted,
|
|
4906
|
+
published,
|
|
4907
|
+
retired,
|
|
4908
|
+
...latestRevision === void 0 ? {} : { latestRevision },
|
|
4909
|
+
...latestFrameId === void 0 ? {} : { latestFrameId },
|
|
4910
|
+
historyInvalidations,
|
|
4911
|
+
invalidated,
|
|
4912
|
+
meshBytes,
|
|
4913
|
+
maxMeshBytes
|
|
4914
|
+
});
|
|
4915
|
+
},
|
|
4916
|
+
dispose() {
|
|
4917
|
+
disposed = true;
|
|
4918
|
+
records.clear();
|
|
4919
|
+
publishedReceipts.clear();
|
|
4920
|
+
meshBytes = 0;
|
|
4921
|
+
}
|
|
4922
|
+
};
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
export { BARREL_DISTORTION_FEATURE_IDENTITY, BARREL_DISTORTION_POST_PROCESS_ID, BARREL_DISTORTION_WGSL, BARREL_DISTORTION_WGSL_COORDINATE, CLOUD_DENSITY_COMPUTE_WGSL, CLOUD_HISTORY_FULLSCREEN_WGSL, CLOUD_HISTORY_SURFACE_COUNT, CLOUD_LAYER_DENSITY_BINDINGS, CLOUD_LAYER_DENSITY_CACHE, CLOUD_LAYER_DENSITY_OUTPUT, CLOUD_LAYER_DENSITY_PROGRAM, CLOUD_LAYER_FEATURE_IDENTITY, CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL, CLOUD_VIEW_FULLSCREEN_WGSL, CLOUD_VIEW_PARAMS_BYTES, CloudHistoryStore, DEFAULT_REFLECTION_PROBE_LIMITS, DynamicGeometryError, DynamicInputError, LOD_OCCLUSION_INSPECTION_MAX_BYTES, LOD_OCCLUSION_INSPECTION_SCHEMA, ReadonlyDynamicInputPage, ReflectionProbeProjection, SSR_FORMAT_PROFILE, SSR_FORMAT_STAGES, acceptCloudLayerGeneration, admitPointsLines, admitReflectionProbe, admitSingleLayerMediumSubmission, admitSsrM0, admitSsrSpatial, advanceProbeFilter, applyCloudSolarTransmittance, boxProjectReflectionDirection, buildCloudDensityCache, buildReflectionProbeTable, cloudCapabilitiesFromRhi, cloudTemporalResetReasons, cloudTemporalSignature, commitProbeFilterStep, compositeCloudRadiance, consumeLodOcclusionInspection, createBarrelDistortionRenderFeature, createCloudHistory, createCloudLayerFeature, createCloudShadowProjection, createDynamicGeometryLifecycle, createProbeFilterState, deriveReflectionFallbackProjection, estimateReflectionProbeBytes, evaluateCloudDensity, inspectCloudLayer, inspectCloudLayerResources, inspectLodOcclusion, inspectPointShadow, inspectReflectionFallback, inspectVolumetricFog, integrateCloudCameraPath, integrateCloudInterior, integrateCloudPath, integrateCloudSolarColumn, intersectCloudLayer, probeFilterIsSteady, projectCloudShadowUv, projectSsrDependencies, projectSsrSpatialInspection, reconstructCloudDensityCache, reflectionFallbackCoverage, reflectionFallbackProjectionSignature, reflectionFallbackSourceKey, reflectionProbeTableBytes, reprojectCloudHistory, resolveReflectionFallbackSource, resolveSsrAdmissionGeneration, retainCloudLayerAfterFailure, sameCandidateCredential, sampleCloudDensity, sampleCloudShadow, selectReflectionProbe, serializeLodOcclusionInspection, serializeSsrSpatialInspection, snapshotCloudDensityCache, validateReflectionProbeInput, validateScreenSpaceReflection, zeroSsrAdmissionWork };
|
|
4926
|
+
//# sourceMappingURL=chunk-ODZWR6JQ.mjs.map
|
|
4927
|
+
//# sourceMappingURL=chunk-ODZWR6JQ.mjs.map
|