@forgeax/engine-render 0.1.33 → 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 +1010 -124
- 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/host-contract.d.ts +3 -3
- package/dist/assembly/host-contract.d.ts.map +1 -1
- 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/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/webgpu-pbr-ready.d.ts +1 -0
- package/dist/assembly/webgpu-pbr-ready.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 +1 -1
- 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/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
- package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
- package/dist/{chunk-ZDQWGQW7.mjs → chunk-HEB564WF.mjs} +4 -4
- package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-HEB564WF.mjs.map} +1 -1
- package/dist/{chunk-VOZWDM3L.mjs → chunk-MKDIT2EM.mjs} +1307 -188
- package/dist/chunk-MKDIT2EM.mjs.map +1 -0
- package/dist/{chunk-LEAXOMYJ.mjs → chunk-OCDLEI6I.mjs} +3 -3
- package/dist/{chunk-LEAXOMYJ.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-3RTCJTH4.mjs → chunk-SHFUFJ5N.mjs} +184 -13
- package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
- package/dist/{chunk-OO3DRYK7.mjs → chunk-UI5SFODX.mjs} +176 -96
- package/dist/chunk-UI5SFODX.mjs.map +1 -0
- package/dist/{chunk-PLAXOOX5.mjs → chunk-UINHLCZA.mjs} +561 -79
- package/dist/chunk-UINHLCZA.mjs.map +1 -0
- package/dist/{chunk-NB2S7ZRX.mjs → chunk-WEX45HKH.mjs} +127 -386
- 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/barrel-distortion.d.ts +13 -0
- package/dist/components/barrel-distortion.d.ts.map +1 -0
- 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/construct-renderer.d.ts +1 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +6759 -2875
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/dynamic-geometry.d.ts +5 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/errors/cloud.d.ts +59 -0
- package/dist/errors/cloud.d.ts.map +1 -0
- package/dist/errors/render.d.ts +18 -4
- 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 +5 -1
- package/dist/extract/contracts.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 +6 -2
- package/dist/features/error-reporting.d.ts.map +1 -1
- package/dist/features/host.d.ts +10 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/plan.d.ts +8 -0
- package/dist/features/plan.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.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 +47 -3
- 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 +26 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +5 -0
- 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 +27 -7
- 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 +135 -1
- 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 +48 -100
- 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.map +1 -1
- package/dist/pipeline-builder.d.ts +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- 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 +37 -3
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts +10 -61
- 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/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 +22 -3
- 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 +1 -1
- 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 +42 -4
- 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 +103 -2
- 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 +16 -9
- 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 +19 -14
- 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 +12 -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/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/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 +14 -2
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +9 -4
- 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/package.json +22 -21
- 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__/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__/factory-contract.integration.test.ts +295 -45
- package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
- 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-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 +6 -5
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
- 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-scene.unit.test.ts +119 -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 +97 -305
- 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__/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 +13 -7
- 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-graphics-capability.unit.test.ts +66 -2
- package/src/__tests__/probe-bindings.unit.test.ts +582 -1
- package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
- 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 +7 -0
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
- 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-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 +20 -20
- 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 +26 -10
- package/src/__tests__/resource-allocation.unit.test.ts +123 -0
- 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 +15 -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-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.integration.test.ts +182 -1
- 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__/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 +4 -2
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
- 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 +113 -9
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
- 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/host-contract.ts +3 -3
- 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/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 +4 -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 +3 -3
- package/src/assembly/webgpu-pbr-ready.ts +9 -1
- package/src/assembly/webgpu-ready-per-pass.ts +104 -0
- package/src/assembly/webgpu-ready.ts +115 -61
- 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 +214 -252
- package/src/assembly/webgpu-vertex-layouts.ts +1 -1
- package/src/barrel-distortion.ts +247 -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/barrel-distortion.ts +46 -0
- 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/sprite-instances.ts +1 -1
- package/src/construct-renderer.ts +1 -1
- package/src/dynamic-geometry.ts +12 -1
- package/src/errors/cloud.ts +103 -0
- package/src/errors/render.ts +44 -7
- package/src/examples/single-layer-medium-surface.ts +59 -0
- package/src/extract/camera.ts +15 -1
- package/src/extract/contracts.ts +5 -0
- package/src/features/barrel-distortion.ts +172 -0
- package/src/features/error-reporting.ts +26 -6
- package/src/features/host.ts +115 -14
- package/src/features/plan.ts +8 -0
- package/src/features/render-graph-contribution.ts +9 -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 +33 -25
- package/src/gpu-driven/prepared-draw.ts +12 -4
- package/src/gpu-driven/production-raster-material.ts +1894 -0
- package/src/gpu-driven/production-raster.ts +825 -784
- 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 +164 -10
- package/src/gpu-scene.ts +75 -84
- package/src/hdrp-buffers.ts +4 -3
- package/src/ibl/skylight-bind-group.ts +49 -7
- package/src/index.ts +196 -9
- package/src/inspection-types.ts +162 -0
- package/src/instance-buffer-cache.ts +4 -4
- package/src/instances.ts +149 -569
- 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 +90 -24
- package/src/pipeline-builder.ts +1 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
- 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 +1 -1
- 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 +39 -3
- package/src/record/frame.ts +217 -1084
- package/src/record/gpu-draw-receipts.ts +38 -0
- package/src/record/gpu-timing.ts +48 -5
- package/src/record/main-pass-geometry.ts +251 -82
- package/src/record/main-pass-material.ts +66 -4
- package/src/record/main-pass-sprite-draws.ts +7 -65
- package/src/record/main-pass.ts +156 -20
- 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 +39 -3
- package/src/record/shadow-pass.ts +30 -1
- package/src/record/skybox-post-pass.ts +28 -4
- package/src/record/typed-frame-graph.ts +770 -134
- package/src/record/view-ubo.ts +26 -3
- package/src/record/volume-params.ts +225 -0
- package/src/recovery/render-system-candidate.ts +235 -42
- package/src/reflection/record-owner.ts +7 -3
- package/src/render-contract.ts +48 -4
- package/src/render-graph-primitives.ts +7 -1
- package/src/render-pipeline.ts +130 -0
- package/src/render-system-extract-tail.ts +57 -61
- package/src/render-system-extract.ts +74 -76
- 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 +347 -363
- package/src/scene/probe-blend.ts +96 -4
- package/src/scene/render-scene.ts +349 -280
- 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/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/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 +70 -2
- package/src/volume/component.ts +10 -9
- package/src/volume/extract.ts +11 -5
- package/src/volume/inspection.ts +3 -0
- package/src/volume/passes.ts +87 -4
- package/src/volume/resources.ts +2 -1
- package/dist/chunk-3RTCJTH4.mjs.map +0 -1
- package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
- package/dist/chunk-NNQL7CB3.mjs +0 -1713
- package/dist/chunk-NNQL7CB3.mjs.map +0 -1
- package/dist/chunk-OO3DRYK7.mjs.map +0 -1
- package/dist/chunk-PA5DRJNI.mjs.map +0 -1
- package/dist/chunk-PLAXOOX5.mjs.map +0 -1
- package/dist/chunk-VOZWDM3L.mjs.map +0 -1
|
@@ -0,0 +1,1810 @@
|
|
|
1
|
+
import { ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import type { RenderError } from '../errors/render';
|
|
3
|
+
import type { RenderFeaturePlan, RenderFeaturePlanContext } from '../features/plan';
|
|
4
|
+
import type { RenderFeature, RenderFeatureExtractContext } from '../features/types';
|
|
5
|
+
import { buildCloudDensityCache, CLOUD_VERTICAL_CELLS, type CloudDensityCache } from './density';
|
|
6
|
+
import {
|
|
7
|
+
type CloudLayerInspection,
|
|
8
|
+
cloudCapabilitiesFromRhi,
|
|
9
|
+
inspectCloudLayer,
|
|
10
|
+
} from './inspection';
|
|
11
|
+
import {
|
|
12
|
+
CLOUD_EXTINCTION_COEFFICIENT,
|
|
13
|
+
CLOUD_QUALITY_PROFILES,
|
|
14
|
+
cloudLayerFormationKey,
|
|
15
|
+
cloudShadowResolutionForQuality,
|
|
16
|
+
cloudViewDistanceForQuality,
|
|
17
|
+
type ValidatedCloudLayer,
|
|
18
|
+
} from './parameters';
|
|
19
|
+
import { inspectCloudLayerResources } from './resources';
|
|
20
|
+
import { type CloudShadowProjection, createCloudShadowProjection } from './shadow';
|
|
21
|
+
import {
|
|
22
|
+
CloudHistoryStore,
|
|
23
|
+
type CloudTemporalFrame,
|
|
24
|
+
type CloudTemporalSignature,
|
|
25
|
+
createCloudHistory,
|
|
26
|
+
} from './temporal';
|
|
27
|
+
|
|
28
|
+
export const CLOUD_LAYER_FEATURE_IDENTITY = 'forgeax.cloud-layer';
|
|
29
|
+
export const CLOUD_LAYER_DENSITY_PROGRAM = 'cloud-layer-density';
|
|
30
|
+
export const CLOUD_LAYER_DENSITY_BINDINGS = 'cloud-layer-density-bindings';
|
|
31
|
+
export const CLOUD_LAYER_DENSITY_CACHE = 'cloud-layer-density-cache';
|
|
32
|
+
export const CLOUD_LAYER_DENSITY_OUTPUT = 'cloud-layer-density-output';
|
|
33
|
+
|
|
34
|
+
const COMPUTE_STAGE = 0x4;
|
|
35
|
+
const CLOUD_WORKGROUP_SIZE = 64;
|
|
36
|
+
// 24 vec4/u32 lanes: authored field, sun, integration mode and the
|
|
37
|
+
// renderer-owned stable light-space shadow projection. The same POD is used by
|
|
38
|
+
// the display and shadow entry points so they cannot drift in their density or
|
|
39
|
+
// cache addressing contract.
|
|
40
|
+
// The final lane carries temporal validity/reset/blend. Keep the payload
|
|
41
|
+
// explicitly 16-byte aligned so the public schema, prepared UBO and WGSL stay
|
|
42
|
+
// one contract.
|
|
43
|
+
export const CLOUD_VIEW_PARAMS_BYTES = 176;
|
|
44
|
+
|
|
45
|
+
/** R8 cache bytes are packed into little-endian u32 storage elements. */
|
|
46
|
+
function packCloudDensityCache(cache: CloudDensityCache): Uint32Array {
|
|
47
|
+
const packed = new Uint32Array(Math.max(1, Math.ceil(cache.formationData.byteLength / 4)));
|
|
48
|
+
for (let index = 0; index < cache.formationData.byteLength; index += 1) {
|
|
49
|
+
const wordIndex = index >>> 2;
|
|
50
|
+
packed[wordIndex] =
|
|
51
|
+
(packed[wordIndex] ?? 0) | ((cache.formationData[index] ?? 0) << ((index & 3) * 8));
|
|
52
|
+
}
|
|
53
|
+
return packed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Minimal copy compute; one invocation owns one packed u32 cache element. */
|
|
57
|
+
export const CLOUD_DENSITY_COMPUTE_WGSL = /* wgsl */ `
|
|
58
|
+
struct CloudCacheParams { count: u32, }
|
|
59
|
+
@group(0) @binding(0) var<storage, read> sourceDensity: array<u32>;
|
|
60
|
+
@group(0) @binding(1) var<storage, read_write> cachedDensity: array<u32>;
|
|
61
|
+
@group(0) @binding(2) var<uniform> params: CloudCacheParams;
|
|
62
|
+
@compute @workgroup_size(64)
|
|
63
|
+
fn cloud_density_cache(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
64
|
+
if (id.x >= params.count) { return; }
|
|
65
|
+
cachedDensity[id.x] = sourceDensity[id.x];
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Cloud view transport. The View group is the renderer-owned camera/light
|
|
71
|
+
* source of truth; group(1) carries frame-local cloud parameters, depth and
|
|
72
|
+
* the bounded packed 3D cache. Camera, solar-column and cloud-interior
|
|
73
|
+
* integration all call cloud_density(), so wind only changes the sample
|
|
74
|
+
* coordinate and never the authored field.
|
|
75
|
+
*/
|
|
76
|
+
export const CLOUD_VIEW_FULLSCREEN_WGSL = /* wgsl */ `
|
|
77
|
+
const CLOUD_VERTICAL_CELLS: f32 = ${CLOUD_VERTICAL_CELLS};
|
|
78
|
+
const CLOUD_VERTICAL_NOISE_CELLS: f32 = 2.0;
|
|
79
|
+
|
|
80
|
+
struct CloudCameraView {
|
|
81
|
+
worldViewProj: mat4x4<f32>,
|
|
82
|
+
lightDir: vec3<f32>,
|
|
83
|
+
lightColor: vec3<f32>,
|
|
84
|
+
cameraPos: vec3<f32>,
|
|
85
|
+
lightViewProj_A: mat4x4<f32>,
|
|
86
|
+
inverseViewProj: mat4x4<f32>,
|
|
87
|
+
lightViewProj_B: mat4x4<f32>,
|
|
88
|
+
lightViewProj_C: mat4x4<f32>,
|
|
89
|
+
lightViewProj_D: mat4x4<f32>,
|
|
90
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
91
|
+
cascadeCount: f32,
|
|
92
|
+
cascadeBlend: f32,
|
|
93
|
+
depthBias: f32,
|
|
94
|
+
normalBias: f32,
|
|
95
|
+
directionalShadowFilter: vec4<f32>,
|
|
96
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
97
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
98
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
99
|
+
temporalProjection: vec4<f32>,
|
|
100
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
101
|
+
ssrParams: vec4<f32>,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
struct CloudViewParams {
|
|
105
|
+
layer: vec4<f32>, // baseHeight, thickness, scale, density
|
|
106
|
+
field: vec4<f32>, // coverage, timeSeconds, maxDistance, seed
|
|
107
|
+
wind: vec4<f32>, // xyz m/s
|
|
108
|
+
sunDirection: vec4<f32>,
|
|
109
|
+
sunRadiance: vec4<f32>,
|
|
110
|
+
integration: vec4<u32>, // cacheResolution, viewSteps, solarSteps, reserved
|
|
111
|
+
shadowOrigin: vec4<f32>,
|
|
112
|
+
shadowRight: vec4<f32>,
|
|
113
|
+
shadowUp: vec4<f32>,
|
|
114
|
+
shadowProjection: vec4<f32>, // range, valid, lowSun, texelSize
|
|
115
|
+
temporal: vec4<f32>, // valid, reset, historyWeight, reserved
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
119
|
+
@group(1) @binding(0) var sceneColor: texture_2d<f32>;
|
|
120
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
121
|
+
@group(1) @binding(2) var<uniform> cloud: CloudViewParams;
|
|
122
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
123
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
124
|
+
@group(1) @binding(5) var previousRadiance: texture_2d<f32>;
|
|
125
|
+
@group(1) @binding(6) var previousTransmittance: texture_2d<f32>;
|
|
126
|
+
@group(1) @binding(7) var previousDepth: texture_2d<f32>;
|
|
127
|
+
@group(1) @binding(8) var<storage, read> densityCache: array<u32>;
|
|
128
|
+
|
|
129
|
+
struct CloudViewOut {
|
|
130
|
+
@builtin(position) position: vec4<f32>,
|
|
131
|
+
@location(0) uv: vec2<f32>,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
fn cloud_hash3(cell: vec3<i32>, seed: u32) -> f32 {
|
|
135
|
+
var h = bitcast<u32>(cell.x) * 0x45d9f3bu;
|
|
136
|
+
h = (h ^ (bitcast<u32>(cell.y) * 0x119de1f3u)) * 0x45d9f3bu;
|
|
137
|
+
h = (h ^ (bitcast<u32>(cell.z) * 0x3449fu)) * 0x45d9f3bu;
|
|
138
|
+
h = h ^ seed;
|
|
139
|
+
h = (h ^ (h >> 16u)) * 0x45d9f3bu;
|
|
140
|
+
h = (h ^ (h >> 13u)) * 0x27d4eb2du;
|
|
141
|
+
return f32(h ^ (h >> 16u)) / 4294967296.0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn cloud_wrap_cell(value: i32, period: i32) -> i32 {
|
|
145
|
+
let wrapped = value % period;
|
|
146
|
+
return select(wrapped, wrapped + period, wrapped < 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn cloud_hash3_periodic(
|
|
150
|
+
cell: vec3<i32>,
|
|
151
|
+
seed: u32,
|
|
152
|
+
periodX: i32,
|
|
153
|
+
periodY: i32,
|
|
154
|
+
periodZ: i32,
|
|
155
|
+
) -> f32 {
|
|
156
|
+
return cloud_hash3(
|
|
157
|
+
vec3<i32>(
|
|
158
|
+
cloud_wrap_cell(cell.x, periodX),
|
|
159
|
+
cloud_wrap_cell(cell.y, periodY),
|
|
160
|
+
cloud_wrap_cell(cell.z, periodZ),
|
|
161
|
+
),
|
|
162
|
+
seed,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn cloud_value_noise(
|
|
167
|
+
position: vec3<f32>,
|
|
168
|
+
seed: u32,
|
|
169
|
+
periodX: i32,
|
|
170
|
+
periodY: i32,
|
|
171
|
+
periodZ: i32,
|
|
172
|
+
) -> f32 {
|
|
173
|
+
let cell = vec3<i32>(floor(position));
|
|
174
|
+
let fraction = fract(position);
|
|
175
|
+
let smoothFraction = fraction * fraction * (3.0 - 2.0 * fraction);
|
|
176
|
+
let c000 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 0), seed, periodX, periodY, periodZ);
|
|
177
|
+
let c100 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 0), seed, periodX, periodY, periodZ);
|
|
178
|
+
let c010 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 0), seed, periodX, periodY, periodZ);
|
|
179
|
+
let c110 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 0), seed, periodX, periodY, periodZ);
|
|
180
|
+
let c001 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 1), seed, periodX, periodY, periodZ);
|
|
181
|
+
let c101 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 1), seed, periodX, periodY, periodZ);
|
|
182
|
+
let c011 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 1), seed, periodX, periodY, periodZ);
|
|
183
|
+
let c111 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 1), seed, periodX, periodY, periodZ);
|
|
184
|
+
let x00 = mix(c000, c100, smoothFraction.x);
|
|
185
|
+
let x10 = mix(c010, c110, smoothFraction.x);
|
|
186
|
+
let x01 = mix(c001, c101, smoothFraction.x);
|
|
187
|
+
let x11 = mix(c011, c111, smoothFraction.x);
|
|
188
|
+
return mix(mix(x00, x10, smoothFraction.y), mix(x01, x11, smoothFraction.y), smoothFraction.z);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn cloud_cellular_noise(position: vec3<f32>, seed: u32, periodXZ: i32, periodY: i32) -> f32 {
|
|
192
|
+
let cell = vec3<i32>(floor(position));
|
|
193
|
+
let fraction = fract(position);
|
|
194
|
+
var nearest = 1e9;
|
|
195
|
+
var dz: i32 = -1;
|
|
196
|
+
loop {
|
|
197
|
+
var dy: i32 = -1;
|
|
198
|
+
loop {
|
|
199
|
+
var dx: i32 = -1;
|
|
200
|
+
loop {
|
|
201
|
+
let random = cloud_hash3_periodic(
|
|
202
|
+
cell + vec3<i32>(dx, dy, dz),
|
|
203
|
+
seed ^ 0x9e3779b9u,
|
|
204
|
+
periodXZ,
|
|
205
|
+
periodY,
|
|
206
|
+
periodXZ,
|
|
207
|
+
);
|
|
208
|
+
let point = vec3<f32>(
|
|
209
|
+
f32(dx) + fract(random * 17.0) - fraction.x,
|
|
210
|
+
f32(dy) + fract(random * 31.0) - fraction.y,
|
|
211
|
+
f32(dz) + fract(random * 47.0) - fraction.z,
|
|
212
|
+
);
|
|
213
|
+
nearest = min(nearest, dot(point, point));
|
|
214
|
+
if (dx >= 1) { break; }
|
|
215
|
+
dx = dx + 1;
|
|
216
|
+
}
|
|
217
|
+
if (dy >= 1) { break; }
|
|
218
|
+
dy = dy + 1;
|
|
219
|
+
}
|
|
220
|
+
if (dz >= 1) { break; }
|
|
221
|
+
dz = dz + 1;
|
|
222
|
+
}
|
|
223
|
+
return 1.0 - clamp(sqrt(nearest) * 1.25, 0.0, 1.0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn cloud_weather_field(position: vec3<f32>, seed: u32) -> f32 {
|
|
227
|
+
return smoothstep(
|
|
228
|
+
0.34,
|
|
229
|
+
0.66,
|
|
230
|
+
cloud_value_noise(
|
|
231
|
+
vec3<f32>(position.x * 2.0, 0.37, position.z * 2.0),
|
|
232
|
+
seed + 17041u,
|
|
233
|
+
2,
|
|
234
|
+
1,
|
|
235
|
+
2,
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn cloud_formation_field(position: vec3<f32>, seed: u32) -> vec3<f32> {
|
|
241
|
+
let macroWarpX = cloud_value_noise(
|
|
242
|
+
vec3<f32>(position.x * 2.0, position.y * CLOUD_VERTICAL_NOISE_CELLS + 3.0, position.z * 2.0),
|
|
243
|
+
seed + 41023u,
|
|
244
|
+
2,
|
|
245
|
+
2,
|
|
246
|
+
2,
|
|
247
|
+
) - 0.5;
|
|
248
|
+
let macroWarpZ = cloud_value_noise(
|
|
249
|
+
vec3<f32>(position.x * 2.0 + 5.0, position.y * CLOUD_VERTICAL_NOISE_CELLS - 7.0, position.z * 2.0),
|
|
250
|
+
seed + 41023u,
|
|
251
|
+
2,
|
|
252
|
+
2,
|
|
253
|
+
2,
|
|
254
|
+
) - 0.5;
|
|
255
|
+
let bodyX = position.x + macroWarpX * 0.3;
|
|
256
|
+
let bodyZ = position.z + macroWarpZ * 0.3;
|
|
257
|
+
var noise = 0.0;
|
|
258
|
+
var weight = 0.0;
|
|
259
|
+
var amplitude = 0.5;
|
|
260
|
+
var frequency = 1.0;
|
|
261
|
+
for (var octave = 0u; octave < 4u; octave = octave + 1u) {
|
|
262
|
+
let tilePeriod = 4.0 * frequency;
|
|
263
|
+
noise = noise + cloud_value_noise(
|
|
264
|
+
vec3<f32>(bodyX * tilePeriod, position.y * frequency * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * tilePeriod),
|
|
265
|
+
seed + octave * 1013u,
|
|
266
|
+
i32(tilePeriod),
|
|
267
|
+
i32(frequency * CLOUD_VERTICAL_NOISE_CELLS),
|
|
268
|
+
i32(tilePeriod),
|
|
269
|
+
) * amplitude;
|
|
270
|
+
weight = weight + amplitude;
|
|
271
|
+
amplitude = amplitude * 0.4;
|
|
272
|
+
frequency = frequency * 2.0;
|
|
273
|
+
}
|
|
274
|
+
let broad = noise / max(0.0001, weight);
|
|
275
|
+
let cells = cloud_cellular_noise(
|
|
276
|
+
vec3<f32>(bodyX * 4.0, position.y * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * 4.0),
|
|
277
|
+
seed + 5011u,
|
|
278
|
+
4,
|
|
279
|
+
i32(CLOUD_VERTICAL_NOISE_CELLS),
|
|
280
|
+
);
|
|
281
|
+
// Keep the broad field responsible for the cloud silhouette and use the
|
|
282
|
+
// cellular term as bounded breakup. A broad-dominant mix reads as fewer,
|
|
283
|
+
// wider formations instead of evenly spaced foam balls.
|
|
284
|
+
let base = clamp(broad * 0.85 + cells * 0.15 + 0.15, 0.0, 1.0);
|
|
285
|
+
let weather = cloud_weather_field(position, seed);
|
|
286
|
+
let detailPosition = position * 3.0;
|
|
287
|
+
let warpX = cloud_value_noise(
|
|
288
|
+
vec3<f32>(detailPosition.x * 2.0, detailPosition.y * 2.0 + 11.0, detailPosition.z * 2.0),
|
|
289
|
+
seed + 29011u,
|
|
290
|
+
2,
|
|
291
|
+
2,
|
|
292
|
+
2,
|
|
293
|
+
) - 0.5;
|
|
294
|
+
let warpZ = cloud_value_noise(
|
|
295
|
+
vec3<f32>(detailPosition.x * 2.0 + 7.0, detailPosition.y * 2.0 - 5.0, detailPosition.z * 2.0),
|
|
296
|
+
seed + 29011u,
|
|
297
|
+
2,
|
|
298
|
+
2,
|
|
299
|
+
2,
|
|
300
|
+
) - 0.5;
|
|
301
|
+
let erosion = cloud_cellular_noise(
|
|
302
|
+
vec3<f32>(
|
|
303
|
+
(detailPosition.x + warpX * 0.22) * 12.0,
|
|
304
|
+
detailPosition.y * 3.0,
|
|
305
|
+
(detailPosition.z + warpZ * 0.22) * 12.0,
|
|
306
|
+
),
|
|
307
|
+
seed + 7919u,
|
|
308
|
+
12,
|
|
309
|
+
3,
|
|
310
|
+
);
|
|
311
|
+
return vec3<f32>(weather, base, erosion);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn cloud_height_profile(height: f32, body: f32, weather: f32) -> f32 {
|
|
315
|
+
// A body-dependent crown breaks the fixed horizontal slab silhouette while
|
|
316
|
+
// keeping the profile smooth enough for the bounded ray step budget.
|
|
317
|
+
let lowerEdge = 0.05 + (1.0 - body) * 0.1;
|
|
318
|
+
let crownEdge = min(0.96, 0.6 + body * 0.3 + weather * 0.08);
|
|
319
|
+
let lower = smoothstep(lowerEdge, min(1.0, lowerEdge + 0.12), height);
|
|
320
|
+
let upper = 1.0 - smoothstep(crownEdge, min(1.0, crownEdge + 0.14), height);
|
|
321
|
+
return lower * upper;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn cloud_compose_density(height: f32, formation: vec3<f32>, coverage: f32) -> f32 {
|
|
325
|
+
let threshold = 1.0 - coverage * (0.45 + formation.x * 0.55);
|
|
326
|
+
let covered = smoothstep(0.0, 0.4, (formation.y - threshold) / max(0.001, 1.0 - threshold));
|
|
327
|
+
let shaped = covered * cloud_height_profile(height, formation.y, formation.x);
|
|
328
|
+
let erosion = (1.0 - formation.z) * 0.18;
|
|
329
|
+
return clamp((shaped - erosion) / (1.0 - erosion), 0.0, 1.0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fn cloud_analytic_density(position: vec3<f32>) -> f32 {
|
|
333
|
+
let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
|
|
334
|
+
if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
|
|
335
|
+
let advected = position + cloud.wind.xyz * cloud.field.y;
|
|
336
|
+
let advectedHeight = height +
|
|
337
|
+
cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
|
|
338
|
+
let p = vec3<f32>(
|
|
339
|
+
advected.x * cloud.layer.z,
|
|
340
|
+
fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
|
|
341
|
+
advected.z * cloud.layer.z,
|
|
342
|
+
);
|
|
343
|
+
let formation = cloud_formation_field(p, u32(round(cloud.field.w)));
|
|
344
|
+
return cloud_compose_density(height, formation, cloud.field.x) * cloud.layer.w;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
fn cloud_cache_byte(index: u32) -> f32 {
|
|
348
|
+
let word = densityCache[index >> 2u];
|
|
349
|
+
let shift = (index & 3u) * 8u;
|
|
350
|
+
return f32((word >> shift) & 255u) / 255.0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
fn cloud_cache_component(plane: u32, index: u32, stride: u32) -> f32 {
|
|
354
|
+
return cloud_cache_byte(plane * stride + index);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn cloud_cache_trilinear(
|
|
358
|
+
plane: u32,
|
|
359
|
+
resolution: u32,
|
|
360
|
+
x0: u32,
|
|
361
|
+
x1: u32,
|
|
362
|
+
y0: u32,
|
|
363
|
+
y1: u32,
|
|
364
|
+
z0: u32,
|
|
365
|
+
z1: u32,
|
|
366
|
+
fx: f32,
|
|
367
|
+
fy: f32,
|
|
368
|
+
fz: f32,
|
|
369
|
+
) -> f32 {
|
|
370
|
+
let stride = resolution * resolution * resolution;
|
|
371
|
+
let layer = resolution * resolution;
|
|
372
|
+
let c000 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x0, stride);
|
|
373
|
+
let c100 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x1, stride);
|
|
374
|
+
let c010 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x0, stride);
|
|
375
|
+
let c110 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x1, stride);
|
|
376
|
+
let c001 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x0, stride);
|
|
377
|
+
let c101 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x1, stride);
|
|
378
|
+
let c011 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x0, stride);
|
|
379
|
+
let c111 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x1, stride);
|
|
380
|
+
let x00 = mix(c000, c100, fx);
|
|
381
|
+
let x10 = mix(c010, c110, fx);
|
|
382
|
+
let x01 = mix(c001, c101, fx);
|
|
383
|
+
let x11 = mix(c011, c111, fx);
|
|
384
|
+
return mix(mix(x00, x10, fy), mix(x01, x11, fy), fz);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
fn cloud_density(position: vec3<f32>) -> f32 {
|
|
388
|
+
let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
|
|
389
|
+
if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
|
|
390
|
+
let resolution = cloud.integration.x;
|
|
391
|
+
if (resolution < 4u) { return max(cloud_analytic_density(position), 0.0); }
|
|
392
|
+
let periodCoordinate = fract((position.xz + cloud.wind.xz * cloud.field.y) * cloud.layer.z);
|
|
393
|
+
let x = fract(periodCoordinate.x - 0.5 / f32(resolution)) * f32(resolution);
|
|
394
|
+
let z = fract(periodCoordinate.y - 0.5 / f32(resolution)) * f32(resolution);
|
|
395
|
+
let advectedHeight = height +
|
|
396
|
+
cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
|
|
397
|
+
let y = fract(advectedHeight - 0.5 / f32(resolution)) * f32(resolution);
|
|
398
|
+
let x0 = u32(floor(x));
|
|
399
|
+
let y0 = u32(floor(y));
|
|
400
|
+
let z0 = u32(floor(z));
|
|
401
|
+
let x1 = (x0 + 1u) % resolution;
|
|
402
|
+
let y1 = (y0 + 1u) % resolution;
|
|
403
|
+
let z1 = (z0 + 1u) % resolution;
|
|
404
|
+
let fx = x - f32(x0);
|
|
405
|
+
let fy = y - f32(y0);
|
|
406
|
+
let fz = z - f32(z0);
|
|
407
|
+
let weather = cloud_cache_trilinear(0u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
|
|
408
|
+
let body = cloud_cache_trilinear(1u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
|
|
409
|
+
// Repeat only the independent erosion plane: fine boundary structure with
|
|
410
|
+
// the same eight packed reads and the same cache allocation.
|
|
411
|
+
let detail = fract(vec3<f32>(periodCoordinate.x, advectedHeight, periodCoordinate.y) * 3.0 - vec3<f32>(0.5 / f32(resolution))) * f32(resolution);
|
|
412
|
+
let d0 = vec3<u32>(floor(detail));
|
|
413
|
+
let d1 = (d0 + vec3<u32>(1u)) % vec3<u32>(resolution);
|
|
414
|
+
let df = fract(detail);
|
|
415
|
+
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);
|
|
416
|
+
return cloud_compose_density(height, vec3<f32>(weather, body, erosion), cloud.field.x) * cloud.layer.w;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
fn cloud_layer_interval(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> vec2<f32> {
|
|
420
|
+
if (abs(direction.y) < 0.000001) {
|
|
421
|
+
if (origin.y < cloud.layer.x || origin.y > cloud.layer.x + cloud.layer.y) {
|
|
422
|
+
return vec2<f32>(1.0, 0.0);
|
|
423
|
+
}
|
|
424
|
+
return vec2<f32>(0.0, maxDistance);
|
|
425
|
+
}
|
|
426
|
+
let lower = (cloud.layer.x - origin.y) / direction.y;
|
|
427
|
+
let upper = (cloud.layer.x + cloud.layer.y - origin.y) / direction.y;
|
|
428
|
+
return vec2<f32>(
|
|
429
|
+
max(0.0, min(lower, upper)),
|
|
430
|
+
min(maxDistance, max(lower, upper)),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fn cloud_solar_transmittance(position: vec3<f32>) -> f32 {
|
|
435
|
+
let sunDirection = normalize(-view.lightDir);
|
|
436
|
+
let solarDistance = select(cloud.field.z, cloud.shadowProjection.x, cloud.shadowProjection.x > 0.0);
|
|
437
|
+
let interval = cloud_layer_interval(position, sunDirection, solarDistance);
|
|
438
|
+
if (interval.y <= interval.x) { return 1.0; }
|
|
439
|
+
let steps = min(cloud.integration.z, 64u);
|
|
440
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
441
|
+
var opticalDepth = 0.0;
|
|
442
|
+
let solarJitter = cloud_solar_jitter(position);
|
|
443
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
444
|
+
if (index >= steps) { break; }
|
|
445
|
+
let samplePosition = position + sunDirection * (interval.x + (f32(index) + solarJitter) * stepLength);
|
|
446
|
+
opticalDepth = opticalDepth + cloud_density(samplePosition) * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
447
|
+
}
|
|
448
|
+
return exp(-max(opticalDepth, 0.0));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn cloud_henyey_greenstein(cosTheta: f32, g: f32) -> f32 {
|
|
452
|
+
let denominator = max(0.0001, 1.0 + g * g - 2.0 * g * cosTheta);
|
|
453
|
+
return (1.0 - g * g) / (denominator * sqrt(denominator)) * 0.0795774715;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
fn cloud_phase(cosTheta: f32) -> f32 {
|
|
457
|
+
// A bounded dual lobe gives a readable forward highlight and a small
|
|
458
|
+
// back-scatter rim without another solar march.
|
|
459
|
+
return cloud_henyey_greenstein(cosTheta, 0.55) * 0.8 +
|
|
460
|
+
cloud_henyey_greenstein(cosTheta, -0.2) * 0.2;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Reuse the same remaining-path optical depth for two bounded scattering
|
|
464
|
+
// orders. This adds arithmetic only; no extra density or light-ray samples.
|
|
465
|
+
fn cloud_incident_light(solarT: f32, cosTheta: f32, height: f32) -> vec3<f32> {
|
|
466
|
+
let direct = solarT * cloud_phase(cosTheta);
|
|
467
|
+
let multiple = (0.5 * pow(solarT, 0.5) + 0.25 * pow(solarT, 0.25)) * 0.0795774715;
|
|
468
|
+
let skyFill = mix(0.025, 0.075, smoothstep(0.1, 0.85, height));
|
|
469
|
+
// Match volume-integrate: the shared DirectionalLight value uses the
|
|
470
|
+
// unnormalized phase convention. Convert the normalized HG basis once.
|
|
471
|
+
return cloud.sunRadiance.xyz *
|
|
472
|
+
(vec3<f32>((direct + multiple) * 12.5663706144) + vec3<f32>(0.72, 0.84, 1.0) * skyFill);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn cloud_ray_jitter(pixel: vec2<f32>) -> f32 {
|
|
476
|
+
// A stable per-pixel offset removes view-step contour bands without adding
|
|
477
|
+
// a frame-varying noise source that would fight temporal reprojection.
|
|
478
|
+
return fract(sin(dot(pixel, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn cloud_solar_jitter(position: vec3<f32>) -> f32 {
|
|
482
|
+
// Dither the light-column samples in world space so low shadow-step
|
|
483
|
+
// profiles do not paint parallel contour bands across cloud bases.
|
|
484
|
+
return fract(sin(dot(position.xz, vec2<f32>(19.193, 47.117))) * 15731.743);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
488
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
489
|
+
let world = view.inverseViewProj * ndc;
|
|
490
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
@vertex
|
|
494
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudViewOut {
|
|
495
|
+
var positions = array<vec2<f32>, 3>(
|
|
496
|
+
vec2<f32>(-1.0, -1.0),
|
|
497
|
+
vec2<f32>(3.0, -1.0),
|
|
498
|
+
vec2<f32>(-1.0, 3.0),
|
|
499
|
+
);
|
|
500
|
+
var output: CloudViewOut;
|
|
501
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
502
|
+
// WebGPU clip-space Y grows upward while sampled framebuffer rows and the
|
|
503
|
+
// depth texture use a top-left origin. Keep this one top-left UV for color,
|
|
504
|
+
// depth and inverse-projection reconstruction; flipping only the final
|
|
505
|
+
// color would leave the camera ray and depth cut mirrored.
|
|
506
|
+
output.uv = vec2<f32>(
|
|
507
|
+
positions[index].x * 0.5 + 0.5,
|
|
508
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
509
|
+
);
|
|
510
|
+
return output;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
@fragment
|
|
514
|
+
fn fs_main(input: CloudViewOut) -> @location(0) vec4<f32> {
|
|
515
|
+
// The shadow producer uses this same production density/cache contract but
|
|
516
|
+
// rasterizes a camera-independent, texel-snapped light-space map. It does
|
|
517
|
+
// not consume scene colour; the generic fullscreen host still supplies the
|
|
518
|
+
// input binding to keep the feature binding ABI single and validated.
|
|
519
|
+
if (cloud.integration.w == 1u) {
|
|
520
|
+
let projection = cloud.shadowProjection;
|
|
521
|
+
if (projection.y < 0.5 || projection.z > 0.5 || projection.x <= 0.0) {
|
|
522
|
+
return vec4<f32>(1.0);
|
|
523
|
+
}
|
|
524
|
+
let offset = (input.uv - vec2<f32>(0.5)) * projection.x;
|
|
525
|
+
let receiver = cloud.shadowOrigin.xyz +
|
|
526
|
+
cloud.shadowRight.xyz * offset.x + cloud.shadowUp.xyz * offset.y;
|
|
527
|
+
let sunDirection = normalize(-view.lightDir);
|
|
528
|
+
let interval = cloud_layer_interval(receiver, sunDirection, projection.x);
|
|
529
|
+
if (interval.y <= interval.x) { return vec4<f32>(1.0); }
|
|
530
|
+
let steps = min(cloud.integration.z, 64u);
|
|
531
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
532
|
+
var opticalDepth = 0.0;
|
|
533
|
+
var firstHeight = 1.0;
|
|
534
|
+
var lastHeight = 0.0;
|
|
535
|
+
var occupied = false;
|
|
536
|
+
let solarJitter = cloud_solar_jitter(receiver);
|
|
537
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
538
|
+
if (index >= steps) { break; }
|
|
539
|
+
let samplePosition = receiver + sunDirection *
|
|
540
|
+
(interval.x + (f32(index) + solarJitter) * stepLength);
|
|
541
|
+
let localDensity = cloud_density(samplePosition);
|
|
542
|
+
opticalDepth = opticalDepth + localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
543
|
+
if (localDensity > 0.0001) {
|
|
544
|
+
let sampleHeight = clamp(
|
|
545
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
|
|
546
|
+
0.0,
|
|
547
|
+
1.0,
|
|
548
|
+
);
|
|
549
|
+
if (!occupied) { firstHeight = sampleHeight; }
|
|
550
|
+
lastHeight = sampleHeight;
|
|
551
|
+
occupied = true;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
let transmittance = exp(-max(opticalDepth, 0.0));
|
|
555
|
+
// R stores full-column transmittance. G/B preserve the first/last
|
|
556
|
+
// occupied normalized layer heights so interior samples can estimate the
|
|
557
|
+
// remaining segment without pretending optical depth is uniform through
|
|
558
|
+
// the whole layer. A stores the integrated optical depth and distinguishes
|
|
559
|
+
// an empty projected column from a valid, nearly-clear one.
|
|
560
|
+
return vec4<f32>(
|
|
561
|
+
transmittance,
|
|
562
|
+
select(1.0, firstHeight, occupied),
|
|
563
|
+
select(0.0, lastHeight, occupied),
|
|
564
|
+
select(0.0, opticalDepth, occupied),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
// The shadow producer branch above is per-fragment (its light-space
|
|
568
|
+
// interval depends on the rasterized receiver), so derivative-based
|
|
569
|
+
// sampling here would violate WGSL uniform-control-flow rules. The cloud
|
|
570
|
+
// scene path already owns an explicit level-0 sampler contract.
|
|
571
|
+
// Keep point semantics for the current full-resolution scene path. The
|
|
572
|
+
// half-resolution transport contract is introduced with its own resolve
|
|
573
|
+
// shader so this path retains the established depth convention.
|
|
574
|
+
let depthPixel = vec2<i32>(input.position.xy);
|
|
575
|
+
let depth = textureLoad(sceneDepth, depthPixel, 0);
|
|
576
|
+
let farWorld = reconstruct_world(input.uv, 1.0);
|
|
577
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
|
|
578
|
+
let ray = normalize(farWorld - view.cameraPos);
|
|
579
|
+
let rayJitter = cloud_ray_jitter(input.position.xy);
|
|
580
|
+
let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
|
|
581
|
+
let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
|
|
582
|
+
|
|
583
|
+
// The depth producer shares the same camera ray, interval and density
|
|
584
|
+
// contract as the display path. It writes a cloud representative world
|
|
585
|
+
// position and a validity bit so history remains comparable after camera
|
|
586
|
+
// translation or rotation and never uses an opaque scene distance as a
|
|
587
|
+
// proxy for a participating medium.
|
|
588
|
+
if (cloud.integration.w == 2u) {
|
|
589
|
+
if (interval.y <= interval.x) { return vec4<f32>(sceneWorld, 0.0); }
|
|
590
|
+
let depthSteps = min(cloud.integration.y, 64u);
|
|
591
|
+
let depthStepLength = (interval.y - interval.x) / max(1.0, f32(depthSteps));
|
|
592
|
+
var depthTransmittance = 1.0;
|
|
593
|
+
var weightedDistance = 0.0;
|
|
594
|
+
var opticalWeight = 0.0;
|
|
595
|
+
for (var depthIndex = 0u; depthIndex < 64u; depthIndex = depthIndex + 1u) {
|
|
596
|
+
if (depthIndex >= depthSteps) { break; }
|
|
597
|
+
let sampleDistance = interval.x + (f32(depthIndex) + rayJitter) * depthStepLength;
|
|
598
|
+
let samplePosition = view.cameraPos + ray * sampleDistance;
|
|
599
|
+
let localDensity = cloud_density(samplePosition);
|
|
600
|
+
let localTransmittance = exp(-max(localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * depthStepLength, 0.0));
|
|
601
|
+
let weight = depthTransmittance * (1.0 - localTransmittance);
|
|
602
|
+
weightedDistance = weightedDistance + sampleDistance * weight;
|
|
603
|
+
opticalWeight = opticalWeight + weight;
|
|
604
|
+
depthTransmittance = depthTransmittance * localTransmittance;
|
|
605
|
+
}
|
|
606
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
607
|
+
let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
|
|
608
|
+
let representativeWorld = view.cameraPos + ray * representativeDistance;
|
|
609
|
+
return vec4<f32>(representativeWorld, select(0.0, 1.0, hasCloud));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
|
|
613
|
+
if (interval.y <= interval.x) { return scene; }
|
|
614
|
+
let steps = min(cloud.integration.y, 64u);
|
|
615
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
616
|
+
var transmittance = 1.0;
|
|
617
|
+
var scattering = vec3<f32>(0.0);
|
|
618
|
+
var weightedDistance = 0.0;
|
|
619
|
+
var opticalWeight = 0.0;
|
|
620
|
+
let sunDirection = normalize(-view.lightDir);
|
|
621
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
622
|
+
if (index >= steps) { break; }
|
|
623
|
+
let distance = interval.x + (f32(index) + rayJitter) * stepLength;
|
|
624
|
+
let samplePosition = view.cameraPos + ray * distance;
|
|
625
|
+
let localDensity = cloud_density(samplePosition);
|
|
626
|
+
let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
627
|
+
let localTransmittance = exp(-max(extinction, 0.0));
|
|
628
|
+
let depthWeight = transmittance * (1.0 - localTransmittance);
|
|
629
|
+
weightedDistance = weightedDistance + distance * depthWeight;
|
|
630
|
+
opticalWeight = opticalWeight + depthWeight;
|
|
631
|
+
// Empty samples are common in the separated-cloud field. Avoid a nested
|
|
632
|
+
// solar march for them and stop once the camera path is effectively opaque.
|
|
633
|
+
if (localDensity > 0.0001 && transmittance > 0.01) {
|
|
634
|
+
let solarTransmittance = cloud_solar_transmittance(samplePosition);
|
|
635
|
+
let cloudHeight = clamp(
|
|
636
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
|
|
637
|
+
);
|
|
638
|
+
scattering = scattering + cloud_incident_light(
|
|
639
|
+
solarTransmittance, dot(ray, sunDirection), cloudHeight,
|
|
640
|
+
) * depthWeight;
|
|
641
|
+
}
|
|
642
|
+
transmittance = transmittance * localTransmittance;
|
|
643
|
+
if (transmittance < 0.01) { break; }
|
|
644
|
+
}
|
|
645
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
646
|
+
let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
|
|
647
|
+
let representativeWorld = view.cameraPos + ray * representativeDistance;
|
|
648
|
+
// Wind is an advective coordinate change. Reproject the cloud point into
|
|
649
|
+
// the previous frame's world before applying the previous camera matrix;
|
|
650
|
+
// authored field values remain immutable.
|
|
651
|
+
let windDelta = cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w);
|
|
652
|
+
// cloud_density evaluates the authored field at position + wind*time.
|
|
653
|
+
// Keeping the same field sample in the previous frame therefore advances
|
|
654
|
+
// the previous world point by the elapsed wind delta as well.
|
|
655
|
+
let previousCloudWorld = representativeWorld + windDelta;
|
|
656
|
+
let previousCloudClip = view.temporalPreviousViewProj * vec4<f32>(previousCloudWorld, 1.0);
|
|
657
|
+
let previousCloudNdc = previousCloudClip.xyz / max(abs(previousCloudClip.w), 0.00001);
|
|
658
|
+
let previousCloudUv = vec2<f32>(
|
|
659
|
+
previousCloudNdc.x * 0.5 + 0.5,
|
|
660
|
+
1.0 - (previousCloudNdc.y * 0.5 + 0.5),
|
|
661
|
+
);
|
|
662
|
+
let cloudInBounds = all(previousCloudUv >= vec2<f32>(0.0)) && all(previousCloudUv <= vec2<f32>(1.0));
|
|
663
|
+
let cloudHistoryUv = clamp(previousCloudUv, vec2<f32>(0.0), vec2<f32>(1.0));
|
|
664
|
+
let cloudHistoryDepthSample = textureSampleLevel(previousDepth, depthSampler, cloudHistoryUv, 0.0);
|
|
665
|
+
let cloudHistoryWorld = cloudHistoryDepthSample.xyz;
|
|
666
|
+
let previous = textureSampleLevel(previousRadiance, sceneSampler, cloudHistoryUv, 0.0);
|
|
667
|
+
let previousTransport = textureSampleLevel(
|
|
668
|
+
previousTransmittance,
|
|
669
|
+
sceneSampler,
|
|
670
|
+
cloudHistoryUv,
|
|
671
|
+
0.0,
|
|
672
|
+
);
|
|
673
|
+
let worldTolerance = max(0.05, length(previousCloudWorld - view.cameraPos) * 0.02);
|
|
674
|
+
let depthAccepted = cloudHistoryDepthSample.a > 0.5 &&
|
|
675
|
+
distance(cloudHistoryWorld, previousCloudWorld) <= worldTolerance;
|
|
676
|
+
let accepted =
|
|
677
|
+
hasCloud && cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && cloudInBounds &&
|
|
678
|
+
depthAccepted && previousTransport.a > 0.0;
|
|
679
|
+
let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.95), accepted);
|
|
680
|
+
let cloudRadiance = mix(scattering, previous.rgb, historyWeight);
|
|
681
|
+
// Preserve scene-linear HDR output for the downstream material lane. The
|
|
682
|
+
// history owner receives cloudRadiance separately through the history pass.
|
|
683
|
+
return vec4<f32>(scene.rgb * transmittance + cloudRadiance, transmittance);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// The light-space shadow producer already paid for a bounded column integral.
|
|
687
|
+
// Reuse that texel for camera samples instead of starting a full solar march
|
|
688
|
+
// for every occupied camera step. Outside the occupied interval the cached
|
|
689
|
+
// full-column transmittance is exact for the projected column. Inside the
|
|
690
|
+
// interval, the cache supplies the stable projection/bounds and a short
|
|
691
|
+
// residual march resolves the non-uniform density that a linear optical-depth
|
|
692
|
+
// interpolation would lose. Invalid/out-of-range projections retain the
|
|
693
|
+
// analytic path as an explicit fallback.
|
|
694
|
+
fn cloud_solar_cached_transmittance(position: vec3<f32>) -> f32 {
|
|
695
|
+
let projection = cloud.shadowProjection;
|
|
696
|
+
if (projection.y < 0.5 || projection.x <= 0.0) {
|
|
697
|
+
return cloud_solar_transmittance(position);
|
|
698
|
+
}
|
|
699
|
+
let relative = position - cloud.shadowOrigin.xyz;
|
|
700
|
+
let uv = vec2<f32>(
|
|
701
|
+
dot(relative, cloud.shadowRight.xyz) / projection.x + 0.5,
|
|
702
|
+
dot(relative, cloud.shadowUp.xyz) / projection.x + 0.5,
|
|
703
|
+
);
|
|
704
|
+
if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
|
|
705
|
+
return cloud_solar_transmittance(position);
|
|
706
|
+
}
|
|
707
|
+
let cachedShadow = textureSampleLevel(previousRadiance, sceneSampler, uv, 0.0);
|
|
708
|
+
let cachedOpticalDepth = max(cachedShadow.a, 0.0);
|
|
709
|
+
let layerHeight = clamp(
|
|
710
|
+
(position.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
|
|
711
|
+
0.0,
|
|
712
|
+
1.0,
|
|
713
|
+
);
|
|
714
|
+
if (cachedOpticalDepth <= 0.0001) { return 1.0; }
|
|
715
|
+
let firstHeight = clamp(cachedShadow.g, 0.0, 1.0);
|
|
716
|
+
let lastHeight = clamp(cachedShadow.b, 0.0, 1.0);
|
|
717
|
+
let occupiedMin = min(firstHeight, lastHeight);
|
|
718
|
+
let occupiedMax = max(firstHeight, lastHeight);
|
|
719
|
+
let sunDirection = normalize(-view.lightDir);
|
|
720
|
+
// G/B are ordered along the light ray, not necessarily bottom-to-top. Only
|
|
721
|
+
// use the cached full/empty fast paths when the light has enough vertical
|
|
722
|
+
// component to make that ordering meaningful; grazing light stays on the
|
|
723
|
+
// residual path so a low-sun column cannot invert the cache bounds.
|
|
724
|
+
if (abs(sunDirection.y) > 0.2) {
|
|
725
|
+
if (sunDirection.y > 0.0) {
|
|
726
|
+
if (layerHeight <= occupiedMin) { return exp(-cachedOpticalDepth); }
|
|
727
|
+
if (layerHeight >= occupiedMax) { return 1.0; }
|
|
728
|
+
} else {
|
|
729
|
+
if (layerHeight >= occupiedMax) { return exp(-cachedOpticalDepth); }
|
|
730
|
+
if (layerHeight <= occupiedMin) { return 1.0; }
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// A short residual march is only paid for samples inside the projected
|
|
735
|
+
// cloud span. Vertical light uses twelve taps, oblique light sixteen, and
|
|
736
|
+
// grazing light up to thirty-two because the same layer projects across a
|
|
737
|
+
// much longer solar segment. This removes the large error caused by
|
|
738
|
+
// assuming optical depth is uniform between the first and last occupied
|
|
739
|
+
// heights while keeping the normal camera path far below a second full
|
|
740
|
+
// solar integration.
|
|
741
|
+
let solarDistance = select(cloud.field.z, projection.x, projection.x > 0.0);
|
|
742
|
+
let interval = cloud_layer_interval(position, sunDirection, solarDistance);
|
|
743
|
+
if (interval.y <= interval.x) { return 1.0; }
|
|
744
|
+
let absLightY = abs(sunDirection.y);
|
|
745
|
+
let directionBudget = select(
|
|
746
|
+
32u,
|
|
747
|
+
select(16u, 12u, absLightY > 0.5),
|
|
748
|
+
absLightY > 0.2,
|
|
749
|
+
);
|
|
750
|
+
let residualSteps = min(directionBudget, max(8u, cloud.integration.z * 3u));
|
|
751
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(residualSteps));
|
|
752
|
+
var opticalDepth = 0.0;
|
|
753
|
+
let solarJitter = cloud_solar_jitter(position);
|
|
754
|
+
for (var index = 0u; index < 32u; index = index + 1u) {
|
|
755
|
+
if (index >= residualSteps) { break; }
|
|
756
|
+
let samplePosition = position + sunDirection *
|
|
757
|
+
(interval.x + (f32(index) + solarJitter) * stepLength);
|
|
758
|
+
opticalDepth = opticalDepth + cloud_density(samplePosition) *
|
|
759
|
+
${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
760
|
+
}
|
|
761
|
+
return exp(-max(opticalDepth, 0.0));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
struct CloudTransportOutput {
|
|
765
|
+
@location(0) radiance: vec4<f32>,
|
|
766
|
+
@location(1) transmittance: vec4<f32>,
|
|
767
|
+
@location(2) depth: vec4<f32>,
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
// One half-resolution transport pass writes all three reusable cloud fields.
|
|
771
|
+
// The full-resolution resolve below reconstructs colour and applies temporal
|
|
772
|
+
// history; no second full raymarch exists solely to obtain cloud depth.
|
|
773
|
+
@fragment
|
|
774
|
+
fn fs_transport(input: CloudViewOut) -> CloudTransportOutput {
|
|
775
|
+
let depth = textureSampleLevel(sceneDepth, depthSampler, input.uv, 0u);
|
|
776
|
+
let farWorld = reconstruct_world(input.uv, 1.0);
|
|
777
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
|
|
778
|
+
let ray = normalize(farWorld - view.cameraPos);
|
|
779
|
+
let rayJitter = cloud_ray_jitter(input.position.xy);
|
|
780
|
+
let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
|
|
781
|
+
let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
|
|
782
|
+
var output: CloudTransportOutput;
|
|
783
|
+
output.radiance = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
784
|
+
output.transmittance = vec4<f32>(1.0, 1.0, 1.0, 1.0);
|
|
785
|
+
output.depth = vec4<f32>(sceneWorld, 0.0);
|
|
786
|
+
if (interval.y <= interval.x) { return output; }
|
|
787
|
+
|
|
788
|
+
let steps = min(cloud.integration.y, 64u);
|
|
789
|
+
let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
|
|
790
|
+
var transmittance = 1.0;
|
|
791
|
+
var scattering = vec3<f32>(0.0);
|
|
792
|
+
var weightedDistance = 0.0;
|
|
793
|
+
var opticalWeight = 0.0;
|
|
794
|
+
let sunDirection = normalize(-view.lightDir);
|
|
795
|
+
for (var index = 0u; index < 64u; index = index + 1u) {
|
|
796
|
+
if (index >= steps) { break; }
|
|
797
|
+
let distance = interval.x + (f32(index) + rayJitter) * stepLength;
|
|
798
|
+
let samplePosition = view.cameraPos + ray * distance;
|
|
799
|
+
let localDensity = cloud_density(samplePosition);
|
|
800
|
+
let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
|
|
801
|
+
let localTransmittance = exp(-max(extinction, 0.0));
|
|
802
|
+
let depthWeight = transmittance * (1.0 - localTransmittance);
|
|
803
|
+
weightedDistance = weightedDistance + distance * depthWeight;
|
|
804
|
+
opticalWeight = opticalWeight + depthWeight;
|
|
805
|
+
if (localDensity > 0.0001 && transmittance > 0.01) {
|
|
806
|
+
let solarTransmittance = cloud_solar_cached_transmittance(samplePosition);
|
|
807
|
+
let cloudHeight = clamp(
|
|
808
|
+
(samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
|
|
809
|
+
);
|
|
810
|
+
scattering = scattering + cloud_incident_light(
|
|
811
|
+
solarTransmittance, dot(ray, sunDirection), cloudHeight,
|
|
812
|
+
) * depthWeight;
|
|
813
|
+
}
|
|
814
|
+
transmittance = transmittance * localTransmittance;
|
|
815
|
+
if (transmittance < 0.01) { break; }
|
|
816
|
+
}
|
|
817
|
+
let hasCloud = opticalWeight > 0.00001;
|
|
818
|
+
let representativeDistance = select(sceneDistance, weightedDistance / max(opticalWeight, 0.00001), hasCloud);
|
|
819
|
+
output.radiance = vec4<f32>(scattering, select(0.0, 1.0, hasCloud));
|
|
820
|
+
output.transmittance = vec4<f32>(transmittance, transmittance, transmittance, 1.0);
|
|
821
|
+
output.depth = vec4<f32>(view.cameraPos + ray * representativeDistance, select(0.0, 1.0, hasCloud));
|
|
822
|
+
return output;
|
|
823
|
+
}
|
|
824
|
+
`;
|
|
825
|
+
|
|
826
|
+
/** Shared cloud transport module; `fs_transport` is selected by the MRT PSO. */
|
|
827
|
+
export const CLOUD_TRANSPORT_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL;
|
|
828
|
+
/** Analytic fallback for adapters without a projected cloud-shadow target. */
|
|
829
|
+
export const CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL.replace(
|
|
830
|
+
'cloud_solar_cached_transmittance(samplePosition)',
|
|
831
|
+
'cloud_solar_transmittance(samplePosition)',
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* History writer. It is a separate MRT pass so the cloud composite remains a
|
|
836
|
+
* normal scene-linear color producer while the renderer owns the ping-pong
|
|
837
|
+
* radiance/transmittance/depth attachments and advances them only on submit.
|
|
838
|
+
*/
|
|
839
|
+
export const CLOUD_HISTORY_FULLSCREEN_WGSL = /* wgsl */ `
|
|
840
|
+
struct CloudCameraView {
|
|
841
|
+
worldViewProj: mat4x4<f32>,
|
|
842
|
+
lightDir: vec3<f32>,
|
|
843
|
+
lightColor: vec3<f32>,
|
|
844
|
+
cameraPos: vec3<f32>,
|
|
845
|
+
lightViewProj_A: mat4x4<f32>,
|
|
846
|
+
inverseViewProj: mat4x4<f32>,
|
|
847
|
+
lightViewProj_B: mat4x4<f32>,
|
|
848
|
+
lightViewProj_C: mat4x4<f32>,
|
|
849
|
+
lightViewProj_D: mat4x4<f32>,
|
|
850
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
851
|
+
cascadeCount: f32,
|
|
852
|
+
cascadeBlend: f32,
|
|
853
|
+
depthBias: f32,
|
|
854
|
+
normalBias: f32,
|
|
855
|
+
directionalShadowFilter: vec4<f32>,
|
|
856
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
857
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
858
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
859
|
+
temporalProjection: vec4<f32>,
|
|
860
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
861
|
+
ssrParams: vec4<f32>,
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
struct CloudHistoryParams {
|
|
865
|
+
temporal: vec4<f32>, // valid, reset, historyWeight, reserved
|
|
866
|
+
windTime: vec4<f32>, // wind.xyz, elapsed seconds since previous submit
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
870
|
+
@group(1) @binding(0) var currentRadiance: texture_2d<f32>;
|
|
871
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
872
|
+
@group(1) @binding(2) var<uniform> params: CloudHistoryParams;
|
|
873
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
874
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
875
|
+
@group(1) @binding(5) var currentCloudDepth: texture_2d<f32>;
|
|
876
|
+
@group(1) @binding(6) var sceneBackground: texture_2d<f32>;
|
|
877
|
+
|
|
878
|
+
struct CloudHistoryInput {
|
|
879
|
+
@builtin(position) position: vec4<f32>,
|
|
880
|
+
@location(0) uv: vec2<f32>,
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
struct CloudHistoryOutput {
|
|
884
|
+
@location(0) radiance: vec4<f32>,
|
|
885
|
+
@location(1) transmittance: vec4<f32>,
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
889
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
890
|
+
let world = view.inverseViewProj * ndc;
|
|
891
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
@vertex
|
|
895
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudHistoryInput {
|
|
896
|
+
var positions = array<vec2<f32>, 3>(
|
|
897
|
+
vec2<f32>(-1.0, -1.0),
|
|
898
|
+
vec2<f32>(3.0, -1.0),
|
|
899
|
+
vec2<f32>(-1.0, 3.0),
|
|
900
|
+
);
|
|
901
|
+
var output: CloudHistoryInput;
|
|
902
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
903
|
+
output.uv = vec2<f32>(
|
|
904
|
+
positions[index].x * 0.5 + 0.5,
|
|
905
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
906
|
+
);
|
|
907
|
+
return output;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
@fragment
|
|
911
|
+
fn fs_main(input: CloudHistoryInput) -> CloudHistoryOutput {
|
|
912
|
+
let current = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
|
|
913
|
+
let background = textureSampleLevel(sceneBackground, sceneSampler, input.uv, 0.0);
|
|
914
|
+
let cloudDepth = textureSampleLevel(currentCloudDepth, depthSampler, input.uv, 0.0);
|
|
915
|
+
let currentValid = cloudDepth.a > 0.5;
|
|
916
|
+
// currentRadiance is the scene-linear cloud composite. Remove the
|
|
917
|
+
// unchanged scene background before writing history so the persistent owner
|
|
918
|
+
// stores raw cloud radiance only. The view pass is the sole temporal blend
|
|
919
|
+
// owner; blending here as well would accumulate history twice per frame.
|
|
920
|
+
let currentCloudRadiance = max(current.rgb - background.rgb * current.a, vec3<f32>(0.0));
|
|
921
|
+
let currentTransmittance = vec4<f32>(current.a, current.a, current.a, 1.0);
|
|
922
|
+
var output: CloudHistoryOutput;
|
|
923
|
+
output.radiance = vec4<f32>(currentCloudRadiance, select(0.0, 1.0, currentValid));
|
|
924
|
+
output.transmittance = currentTransmittance;
|
|
925
|
+
return output;
|
|
926
|
+
}
|
|
927
|
+
`;
|
|
928
|
+
|
|
929
|
+
/** Full-resolution resolve for the half-resolution transport MRT. */
|
|
930
|
+
export const CLOUD_RESOLVE_FULLSCREEN_WGSL = /* wgsl */ `
|
|
931
|
+
struct CloudCameraView {
|
|
932
|
+
worldViewProj: mat4x4<f32>,
|
|
933
|
+
lightDir: vec3<f32>,
|
|
934
|
+
lightColor: vec3<f32>,
|
|
935
|
+
cameraPos: vec3<f32>,
|
|
936
|
+
lightViewProj_A: mat4x4<f32>,
|
|
937
|
+
inverseViewProj: mat4x4<f32>,
|
|
938
|
+
lightViewProj_B: mat4x4<f32>,
|
|
939
|
+
lightViewProj_C: mat4x4<f32>,
|
|
940
|
+
lightViewProj_D: mat4x4<f32>,
|
|
941
|
+
splitPlanes: array<vec4<f32>, 4>,
|
|
942
|
+
cascadeCount: f32,
|
|
943
|
+
cascadeBlend: f32,
|
|
944
|
+
depthBias: f32,
|
|
945
|
+
normalBias: f32,
|
|
946
|
+
directionalShadowFilter: vec4<f32>,
|
|
947
|
+
spotLightViewProj: array<mat4x4<f32>, 4>,
|
|
948
|
+
temporalCurrentViewProj: mat4x4<f32>,
|
|
949
|
+
temporalPreviousViewProj: mat4x4<f32>,
|
|
950
|
+
temporalProjection: vec4<f32>,
|
|
951
|
+
temporalPreviousCameraPos: vec4<f32>,
|
|
952
|
+
ssrParams: vec4<f32>,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
struct CloudResolveParams {
|
|
956
|
+
layer: vec4<f32>,
|
|
957
|
+
field: vec4<f32>,
|
|
958
|
+
wind: vec4<f32>,
|
|
959
|
+
sunDirection: vec4<f32>,
|
|
960
|
+
sunRadiance: vec4<f32>,
|
|
961
|
+
integration: vec4<u32>,
|
|
962
|
+
shadowOrigin: vec4<f32>,
|
|
963
|
+
shadowRight: vec4<f32>,
|
|
964
|
+
shadowUp: vec4<f32>,
|
|
965
|
+
shadowProjection: vec4<f32>,
|
|
966
|
+
temporal: vec4<f32>,
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
@group(0) @binding(0) var<uniform> view: CloudCameraView;
|
|
970
|
+
@group(1) @binding(0) var sceneColor: texture_2d<f32>;
|
|
971
|
+
@group(1) @binding(1) var sceneSampler: sampler;
|
|
972
|
+
@group(1) @binding(2) var<uniform> cloud: CloudResolveParams;
|
|
973
|
+
@group(1) @binding(3) var sceneDepth: texture_depth_2d;
|
|
974
|
+
@group(1) @binding(4) var depthSampler: sampler;
|
|
975
|
+
@group(1) @binding(5) var currentRadiance: texture_2d<f32>;
|
|
976
|
+
@group(1) @binding(6) var currentTransmittance: texture_2d<f32>;
|
|
977
|
+
@group(1) @binding(7) var currentDepth: texture_2d<f32>;
|
|
978
|
+
@group(1) @binding(8) var previousRadiance: texture_2d<f32>;
|
|
979
|
+
@group(1) @binding(9) var previousTransmittance: texture_2d<f32>;
|
|
980
|
+
@group(1) @binding(10) var previousDepth: texture_2d<f32>;
|
|
981
|
+
|
|
982
|
+
struct CloudResolveInput {
|
|
983
|
+
@builtin(position) position: vec4<f32>,
|
|
984
|
+
@location(0) uv: vec2<f32>,
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
|
|
988
|
+
let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
|
|
989
|
+
let world = view.inverseViewProj * ndc;
|
|
990
|
+
return world.xyz / max(abs(world.w), 0.00001);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
@vertex
|
|
994
|
+
fn vs_main(@builtin(vertex_index) index: u32) -> CloudResolveInput {
|
|
995
|
+
var positions = array<vec2<f32>, 3>(
|
|
996
|
+
vec2<f32>(-1.0, -1.0),
|
|
997
|
+
vec2<f32>(3.0, -1.0),
|
|
998
|
+
vec2<f32>(-1.0, 3.0),
|
|
999
|
+
);
|
|
1000
|
+
var output: CloudResolveInput;
|
|
1001
|
+
output.position = vec4<f32>(positions[index], 0.0, 1.0);
|
|
1002
|
+
output.uv = vec2<f32>(
|
|
1003
|
+
positions[index].x * 0.5 + 0.5,
|
|
1004
|
+
1.0 - (positions[index].y * 0.5 + 0.5),
|
|
1005
|
+
);
|
|
1006
|
+
return output;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@fragment
|
|
1010
|
+
fn fs_main(input: CloudResolveInput) -> @location(0) vec4<f32> {
|
|
1011
|
+
let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
|
|
1012
|
+
// The resolve owns the full-resolution foreground test. Reject the current
|
|
1013
|
+
// low-resolution transport before any history or radiance is composited;
|
|
1014
|
+
// gating history alone still lets a newly sampled cloud bleed over geometry.
|
|
1015
|
+
let sceneDepthValue = textureLoad(sceneDepth, vec2<i32>(input.position.xy), 0);
|
|
1016
|
+
let sceneWorld = reconstruct_world(input.uv, clamp(sceneDepthValue, 0.0, 1.0));
|
|
1017
|
+
let sceneDistance = length(sceneWorld - view.cameraPos);
|
|
1018
|
+
let transport = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
|
|
1019
|
+
let transportT = textureSampleLevel(currentTransmittance, sceneSampler, input.uv, 0.0);
|
|
1020
|
+
// The transport depth is an rgba16float color attachment, not the scene's
|
|
1021
|
+
// depth texture. Sample it with the filtering color sampler; binding the
|
|
1022
|
+
// non-filtering scene-depth sampler here makes the shader contract depend on
|
|
1023
|
+
// an incompatible sampler/texture pair on strict WebGPU implementations.
|
|
1024
|
+
let cloudDepth = textureSampleLevel(currentDepth, sceneSampler, input.uv, 0.0);
|
|
1025
|
+
let currentValid = transport.a > 0.5 && cloudDepth.a > 0.5;
|
|
1026
|
+
let cloudDistance = length(cloudDepth.xyz - view.cameraPos);
|
|
1027
|
+
let foregroundAccepted = sceneDepthValue >= 0.999999 ||
|
|
1028
|
+
cloudDistance <= sceneDistance + max(0.05, sceneDistance * 0.01);
|
|
1029
|
+
if (!currentValid || !foregroundAccepted) { return scene; }
|
|
1030
|
+
|
|
1031
|
+
let previousClip = view.temporalPreviousViewProj * vec4<f32>(
|
|
1032
|
+
cloudDepth.xyz + cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w),
|
|
1033
|
+
1.0,
|
|
1034
|
+
);
|
|
1035
|
+
let previousNdc = previousClip.xyz / max(abs(previousClip.w), 0.00001);
|
|
1036
|
+
let previousUv = vec2<f32>(
|
|
1037
|
+
previousNdc.x * 0.5 + 0.5,
|
|
1038
|
+
1.0 - (previousNdc.y * 0.5 + 0.5),
|
|
1039
|
+
);
|
|
1040
|
+
let inBounds = all(previousUv >= vec2<f32>(0.0)) && all(previousUv <= vec2<f32>(1.0));
|
|
1041
|
+
let historyUv = clamp(previousUv, vec2<f32>(0.0), vec2<f32>(1.0));
|
|
1042
|
+
let historyDepth = textureSampleLevel(previousDepth, sceneSampler, historyUv, 0.0);
|
|
1043
|
+
let previous = textureSampleLevel(previousRadiance, sceneSampler, historyUv, 0.0);
|
|
1044
|
+
let previousTransport = textureSampleLevel(previousTransmittance, sceneSampler, historyUv, 0.0);
|
|
1045
|
+
let dims = vec2<f32>(textureDimensions(previousRadiance));
|
|
1046
|
+
let texel = 1.0 / max(dims, vec2<f32>(1.0));
|
|
1047
|
+
// Clamp reprojected history against the current low-resolution transport
|
|
1048
|
+
// neighborhood. Using only previous-frame neighbors (and including the
|
|
1049
|
+
// previous center in both extrema) is a no-op and lets stale bright cloud
|
|
1050
|
+
// pixels leak across a newly exposed edge.
|
|
1051
|
+
let c0 = transport.rgb;
|
|
1052
|
+
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;
|
|
1053
|
+
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;
|
|
1054
|
+
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;
|
|
1055
|
+
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;
|
|
1056
|
+
let minRadiance = min(c0, min(min(c1, c2), min(c3, c4)));
|
|
1057
|
+
let maxRadiance = max(c0, max(max(c1, c2), max(c3, c4)));
|
|
1058
|
+
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;
|
|
1059
|
+
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;
|
|
1060
|
+
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;
|
|
1061
|
+
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;
|
|
1062
|
+
let minTransport = min(transportT.r, min(min(t1, t2), min(t3, t4)));
|
|
1063
|
+
let maxTransport = max(transportT.r, max(max(t1, t2), max(t3, t4)));
|
|
1064
|
+
let worldTolerance = max(0.05, distance(cloudDepth.xyz, view.cameraPos) * 0.02);
|
|
1065
|
+
let depthAccepted = historyDepth.a > 0.5 && distance(historyDepth.xyz, cloudDepth.xyz) <= worldTolerance;
|
|
1066
|
+
let accepted = cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && inBounds && depthAccepted;
|
|
1067
|
+
let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.92), accepted);
|
|
1068
|
+
let stableHistory = clamp(previous.rgb, minRadiance, maxRadiance);
|
|
1069
|
+
let stableTransport = clamp(previousTransport.r, minTransport, maxTransport);
|
|
1070
|
+
let radiance = mix(transport.rgb, stableHistory, historyWeight);
|
|
1071
|
+
let transmittance = mix(transportT.r, stableTransport, historyWeight);
|
|
1072
|
+
return vec4<f32>(scene.rgb * transmittance + radiance, transmittance);
|
|
1073
|
+
}
|
|
1074
|
+
`;
|
|
1075
|
+
|
|
1076
|
+
export interface CloudLayerFeatureFrame {
|
|
1077
|
+
readonly params: ValidatedCloudLayer | undefined;
|
|
1078
|
+
readonly cache: CloudDensityCache | undefined;
|
|
1079
|
+
readonly sourceKey: string | undefined;
|
|
1080
|
+
readonly timeSeconds: number;
|
|
1081
|
+
readonly sunDirection: readonly [number, number, number] | undefined;
|
|
1082
|
+
readonly sunRadiance: readonly [number, number, number] | undefined;
|
|
1083
|
+
readonly shadowProjection: CloudShadowProjection | undefined;
|
|
1084
|
+
readonly temporal: CloudTemporalFrame | undefined;
|
|
1085
|
+
readonly generation: number;
|
|
1086
|
+
readonly inspection: CloudLayerInspection;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export interface CloudLayerFeatureOptions {
|
|
1090
|
+
/** Disable the renderer-owned CloudLayer projection while retaining the component. */
|
|
1091
|
+
readonly enabled?: boolean;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function makeCloudViewParams(
|
|
1095
|
+
frame: CloudLayerFeatureFrame,
|
|
1096
|
+
cache: CloudDensityCache,
|
|
1097
|
+
mode: 0 | 1 | 2 = 0,
|
|
1098
|
+
): Uint8Array {
|
|
1099
|
+
const params = new ArrayBuffer(CLOUD_VIEW_PARAMS_BYTES);
|
|
1100
|
+
const floats = new Float32Array(params);
|
|
1101
|
+
const integers = new Uint32Array(params);
|
|
1102
|
+
const profile = CLOUD_QUALITY_PROFILES[frame.params?.quality ?? 'medium'];
|
|
1103
|
+
const authored = frame.params;
|
|
1104
|
+
floats[0] = authored?.baseHeight ?? 0;
|
|
1105
|
+
floats[1] = authored?.thickness ?? 1;
|
|
1106
|
+
floats[2] = authored?.scale ?? 1;
|
|
1107
|
+
floats[3] = authored?.density ?? 0;
|
|
1108
|
+
floats[4] = authored?.coverage ?? 1;
|
|
1109
|
+
floats[5] = frame.timeSeconds;
|
|
1110
|
+
floats[6] =
|
|
1111
|
+
mode === 1
|
|
1112
|
+
? (authored?.shadowRange ?? 0)
|
|
1113
|
+
: cloudViewDistanceForQuality(authored?.quality ?? 'medium');
|
|
1114
|
+
floats[7] = authored?.seed ?? 0;
|
|
1115
|
+
floats[8] = authored?.wind[0] ?? 0;
|
|
1116
|
+
floats[9] = authored?.wind[1] ?? 0;
|
|
1117
|
+
floats[10] = authored?.wind[2] ?? 0;
|
|
1118
|
+
floats[12] = frame.sunDirection?.[0] ?? 0;
|
|
1119
|
+
floats[13] = frame.sunDirection?.[1] ?? -1;
|
|
1120
|
+
floats[14] = frame.sunDirection?.[2] ?? 0;
|
|
1121
|
+
floats[15] = 0.2;
|
|
1122
|
+
floats[16] = frame.sunRadiance?.[0] ?? 0;
|
|
1123
|
+
floats[17] = frame.sunRadiance?.[1] ?? 0;
|
|
1124
|
+
floats[18] = frame.sunRadiance?.[2] ?? 0;
|
|
1125
|
+
integers[20] = cache.resolution;
|
|
1126
|
+
integers[21] = Math.max(1, Math.min(64, Math.floor(profile.viewSteps)));
|
|
1127
|
+
integers[22] = Math.max(1, Math.min(64, Math.floor(profile.shadowSteps)));
|
|
1128
|
+
integers[23] = mode;
|
|
1129
|
+
const projection = frame.shadowProjection;
|
|
1130
|
+
if (projection !== undefined) {
|
|
1131
|
+
floats[24] = projection.origin[0];
|
|
1132
|
+
floats[25] = projection.origin[1];
|
|
1133
|
+
floats[26] = projection.origin[2];
|
|
1134
|
+
floats[28] = projection.right[0];
|
|
1135
|
+
floats[29] = projection.right[1];
|
|
1136
|
+
floats[30] = projection.right[2];
|
|
1137
|
+
floats[32] = projection.up[0];
|
|
1138
|
+
floats[33] = projection.up[1];
|
|
1139
|
+
floats[34] = projection.up[2];
|
|
1140
|
+
floats[36] = projection.range;
|
|
1141
|
+
floats[37] = projection.lowSun ? 0 : 1;
|
|
1142
|
+
floats[38] = projection.lowSun ? 1 : 0;
|
|
1143
|
+
floats[39] = projection.texelSize;
|
|
1144
|
+
}
|
|
1145
|
+
const temporal = frame.temporal;
|
|
1146
|
+
floats[40] = temporal !== undefined && temporal.reset === false ? 1 : 0;
|
|
1147
|
+
floats[41] = temporal?.reset === true ? 1 : 0;
|
|
1148
|
+
floats[42] = temporal === undefined ? 0 : profile.historyWeight;
|
|
1149
|
+
// The history entry is the last successfully submitted signature. Carry its
|
|
1150
|
+
// timestamp so the shader can advect the representative cloud point by the
|
|
1151
|
+
// exact elapsed time during reprojection.
|
|
1152
|
+
floats[43] = temporal?.history?.temporal.timeSeconds ?? frame.timeSeconds;
|
|
1153
|
+
return new Uint8Array(params);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function planCloudDensity(
|
|
1157
|
+
frame: CloudLayerFeatureFrame,
|
|
1158
|
+
packedCache: Uint32Array,
|
|
1159
|
+
context: RenderFeaturePlanContext,
|
|
1160
|
+
): RenderFeaturePlan {
|
|
1161
|
+
const cacheCount = packedCache.length;
|
|
1162
|
+
const params = new Uint32Array([cacheCount]);
|
|
1163
|
+
const inputTarget = context.targets.find((target) => target.name === 'motion-input');
|
|
1164
|
+
const outputTarget = context.targets.find((target) => target.name === 'motion-output');
|
|
1165
|
+
const historyRadiancePrevious = context.targets.find(
|
|
1166
|
+
(target) => target.name === 'cloud-history-radiance-previous',
|
|
1167
|
+
);
|
|
1168
|
+
const historyTransmittancePrevious = context.targets.find(
|
|
1169
|
+
(target) => target.name === 'cloud-history-transmittance-previous',
|
|
1170
|
+
);
|
|
1171
|
+
const historyDepthPrevious = context.targets.find(
|
|
1172
|
+
(target) => target.name === 'cloud-history-depth-previous',
|
|
1173
|
+
);
|
|
1174
|
+
const historyRadianceCurrent = context.targets.find(
|
|
1175
|
+
(target) => target.name === 'cloud-history-radiance-current',
|
|
1176
|
+
);
|
|
1177
|
+
const historyTransmittanceCurrent = context.targets.find(
|
|
1178
|
+
(target) => target.name === 'cloud-history-transmittance-current',
|
|
1179
|
+
);
|
|
1180
|
+
const historyDepthCurrent = context.targets.find(
|
|
1181
|
+
(target) => target.name === 'cloud-history-depth-current',
|
|
1182
|
+
);
|
|
1183
|
+
const hasHistoryTargets =
|
|
1184
|
+
historyRadiancePrevious !== undefined &&
|
|
1185
|
+
historyTransmittancePrevious !== undefined &&
|
|
1186
|
+
historyDepthPrevious !== undefined &&
|
|
1187
|
+
historyRadianceCurrent !== undefined &&
|
|
1188
|
+
historyTransmittanceCurrent !== undefined &&
|
|
1189
|
+
historyDepthCurrent !== undefined;
|
|
1190
|
+
const shadowTarget = context.targets.find((target) => target.name === 'cloud-shadow');
|
|
1191
|
+
const canShadow =
|
|
1192
|
+
shadowTarget !== undefined &&
|
|
1193
|
+
shadowTarget.sampleCount === 1 &&
|
|
1194
|
+
frame.shadowProjection !== undefined &&
|
|
1195
|
+
hasHistoryTargets;
|
|
1196
|
+
const canCompose =
|
|
1197
|
+
inputTarget !== undefined &&
|
|
1198
|
+
outputTarget !== undefined &&
|
|
1199
|
+
inputTarget.sampleCount === 1 &&
|
|
1200
|
+
outputTarget.sampleCount === 1 &&
|
|
1201
|
+
hasHistoryTargets;
|
|
1202
|
+
const transportReads = ['motion-input', { key: 'depth', sampleType: 'depth' as const }] as const;
|
|
1203
|
+
const transportCacheReads = [
|
|
1204
|
+
'motion-input',
|
|
1205
|
+
'cloud-shadow',
|
|
1206
|
+
{ key: 'depth', sampleType: 'depth' as const },
|
|
1207
|
+
] as const;
|
|
1208
|
+
const historyReads = [
|
|
1209
|
+
'motion-input',
|
|
1210
|
+
'cloud-history-radiance-previous',
|
|
1211
|
+
'cloud-history-transmittance-previous',
|
|
1212
|
+
'cloud-history-depth-previous',
|
|
1213
|
+
{ key: 'depth', sampleType: 'depth' as const },
|
|
1214
|
+
] as const;
|
|
1215
|
+
const historyAdditionalTextures = [
|
|
1216
|
+
'cloud-history-radiance-previous',
|
|
1217
|
+
'cloud-history-transmittance-previous',
|
|
1218
|
+
'cloud-history-depth-previous',
|
|
1219
|
+
] as const;
|
|
1220
|
+
const viewParams = makeCloudViewParams(frame, frame.cache as CloudDensityCache);
|
|
1221
|
+
const shadowParams = canShadow
|
|
1222
|
+
? makeCloudViewParams(frame, frame.cache as CloudDensityCache, 1)
|
|
1223
|
+
: undefined;
|
|
1224
|
+
const transportUsesShadowCache = canCompose && canShadow;
|
|
1225
|
+
return {
|
|
1226
|
+
resources: [
|
|
1227
|
+
{
|
|
1228
|
+
kind: 'compute-program',
|
|
1229
|
+
name: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
1230
|
+
program: {
|
|
1231
|
+
wgsl: CLOUD_DENSITY_COMPUTE_WGSL,
|
|
1232
|
+
entryPoints: ['cloud_density_cache'],
|
|
1233
|
+
bindings: [
|
|
1234
|
+
{
|
|
1235
|
+
label: 'forgeax.cloud-layer.cache',
|
|
1236
|
+
entries: [
|
|
1237
|
+
{ binding: 0, visibility: COMPUTE_STAGE, buffer: { type: 'read-only-storage' } },
|
|
1238
|
+
{ binding: 1, visibility: COMPUTE_STAGE, buffer: { type: 'storage' } },
|
|
1239
|
+
{ binding: 2, visibility: COMPUTE_STAGE, buffer: { type: 'uniform' } },
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1242
|
+
],
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
kind: 'buffer',
|
|
1247
|
+
name: CLOUD_LAYER_DENSITY_CACHE,
|
|
1248
|
+
size: packedCache.byteLength,
|
|
1249
|
+
usage: ['storage', 'copy-src'],
|
|
1250
|
+
data: packedCache,
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
kind: 'buffer',
|
|
1254
|
+
name: CLOUD_LAYER_DENSITY_OUTPUT,
|
|
1255
|
+
size: packedCache.byteLength,
|
|
1256
|
+
usage: ['storage', 'copy-src'],
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
kind: 'buffer',
|
|
1260
|
+
name: 'cloud-layer-density-params',
|
|
1261
|
+
size: params.byteLength,
|
|
1262
|
+
usage: ['uniform'],
|
|
1263
|
+
data: params,
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
kind: 'compute-bindings',
|
|
1267
|
+
name: CLOUD_LAYER_DENSITY_BINDINGS,
|
|
1268
|
+
program: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
1269
|
+
entries: [
|
|
1270
|
+
{ binding: 0, resource: CLOUD_LAYER_DENSITY_CACHE },
|
|
1271
|
+
{ binding: 1, resource: CLOUD_LAYER_DENSITY_OUTPUT },
|
|
1272
|
+
{ binding: 2, resource: 'cloud-layer-density-params' },
|
|
1273
|
+
],
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
kind: 'fullscreen-program',
|
|
1277
|
+
name: 'cloud-layer-view',
|
|
1278
|
+
source: CLOUD_VIEW_FULLSCREEN_WGSL,
|
|
1279
|
+
usesView: true,
|
|
1280
|
+
storageBindings: [8],
|
|
1281
|
+
reads: historyReads,
|
|
1282
|
+
params: {
|
|
1283
|
+
byteSize: viewParams.byteLength,
|
|
1284
|
+
defaultValue: viewParams,
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
...(shadowParams === undefined
|
|
1288
|
+
? []
|
|
1289
|
+
: [
|
|
1290
|
+
{
|
|
1291
|
+
kind: 'fullscreen-program' as const,
|
|
1292
|
+
name: 'cloud-layer-shadow',
|
|
1293
|
+
source: CLOUD_VIEW_FULLSCREEN_WGSL,
|
|
1294
|
+
usesView: true,
|
|
1295
|
+
storageBindings: [8],
|
|
1296
|
+
reads: historyReads,
|
|
1297
|
+
params: {
|
|
1298
|
+
byteSize: shadowParams.byteLength,
|
|
1299
|
+
defaultValue: shadowParams,
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
]),
|
|
1303
|
+
...(canCompose
|
|
1304
|
+
? [
|
|
1305
|
+
{
|
|
1306
|
+
kind: 'fullscreen-program' as const,
|
|
1307
|
+
name: 'cloud-layer-transport',
|
|
1308
|
+
source: transportUsesShadowCache
|
|
1309
|
+
? CLOUD_TRANSPORT_FULLSCREEN_WGSL
|
|
1310
|
+
: CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL,
|
|
1311
|
+
fragmentEntryPoint: 'fs_transport',
|
|
1312
|
+
usesView: true,
|
|
1313
|
+
storageBindings: [8],
|
|
1314
|
+
reads: transportUsesShadowCache ? transportCacheReads : transportReads,
|
|
1315
|
+
params: {
|
|
1316
|
+
byteSize: viewParams.byteLength,
|
|
1317
|
+
defaultValue: viewParams,
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
kind: 'fullscreen-program' as const,
|
|
1322
|
+
name: 'cloud-layer-resolve',
|
|
1323
|
+
source: CLOUD_RESOLVE_FULLSCREEN_WGSL,
|
|
1324
|
+
usesView: true,
|
|
1325
|
+
reads: [
|
|
1326
|
+
'motion-input',
|
|
1327
|
+
'cloud-history-radiance-current',
|
|
1328
|
+
'cloud-history-transmittance-current',
|
|
1329
|
+
'cloud-history-depth-current',
|
|
1330
|
+
'cloud-history-radiance-previous',
|
|
1331
|
+
'cloud-history-transmittance-previous',
|
|
1332
|
+
'cloud-history-depth-previous',
|
|
1333
|
+
{ key: 'depth', sampleType: 'depth' as const },
|
|
1334
|
+
],
|
|
1335
|
+
params: {
|
|
1336
|
+
byteSize: viewParams.byteLength,
|
|
1337
|
+
defaultValue: viewParams,
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
]
|
|
1341
|
+
: []),
|
|
1342
|
+
...(canCompose
|
|
1343
|
+
? [
|
|
1344
|
+
{
|
|
1345
|
+
kind: 'graphics-program' as const,
|
|
1346
|
+
name: 'cloud-layer-transport-pipeline',
|
|
1347
|
+
program: {
|
|
1348
|
+
shader: 'cloud-layer-transport',
|
|
1349
|
+
vertexLayout: 'none',
|
|
1350
|
+
colorFormats: [
|
|
1351
|
+
historyRadianceCurrent.format,
|
|
1352
|
+
historyTransmittanceCurrent.format,
|
|
1353
|
+
historyDepthCurrent.format,
|
|
1354
|
+
],
|
|
1355
|
+
sampleCount: 1 as const,
|
|
1356
|
+
renderState: {
|
|
1357
|
+
depthWriteEnabled: false,
|
|
1358
|
+
depthCompare: 'always' as const,
|
|
1359
|
+
},
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
kind: 'graphics-bindings' as const,
|
|
1364
|
+
name: 'cloud-layer-transport-view-bindings',
|
|
1365
|
+
program: 'cloud-layer-transport-pipeline',
|
|
1366
|
+
values: { group: 0, view: true },
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
kind: 'graphics-bindings' as const,
|
|
1370
|
+
name: 'cloud-layer-transport-bindings',
|
|
1371
|
+
program: 'cloud-layer-transport-pipeline',
|
|
1372
|
+
values: {
|
|
1373
|
+
group: 1,
|
|
1374
|
+
fullscreen: true,
|
|
1375
|
+
shader: 'cloud-layer-transport',
|
|
1376
|
+
input: 'motion-input',
|
|
1377
|
+
depth: 'depth',
|
|
1378
|
+
additionalTextures: transportUsesShadowCache ? ['cloud-shadow'] : [],
|
|
1379
|
+
storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT],
|
|
1380
|
+
},
|
|
1381
|
+
logicalTargets: { input: 'motion-input' },
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
kind: 'graphics-program' as const,
|
|
1385
|
+
name: 'cloud-layer-resolve-pipeline',
|
|
1386
|
+
program: {
|
|
1387
|
+
shader: 'cloud-layer-resolve',
|
|
1388
|
+
vertexLayout: 'none',
|
|
1389
|
+
colorFormats: [outputTarget.format],
|
|
1390
|
+
sampleCount: 1 as const,
|
|
1391
|
+
renderState: {
|
|
1392
|
+
depthWriteEnabled: false,
|
|
1393
|
+
depthCompare: 'always' as const,
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
kind: 'graphics-bindings' as const,
|
|
1399
|
+
name: 'cloud-layer-resolve-view-bindings',
|
|
1400
|
+
program: 'cloud-layer-resolve-pipeline',
|
|
1401
|
+
values: { group: 0, view: true },
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
kind: 'graphics-bindings' as const,
|
|
1405
|
+
name: 'cloud-layer-resolve-bindings',
|
|
1406
|
+
program: 'cloud-layer-resolve-pipeline',
|
|
1407
|
+
values: {
|
|
1408
|
+
group: 1,
|
|
1409
|
+
fullscreen: true,
|
|
1410
|
+
shader: 'cloud-layer-resolve',
|
|
1411
|
+
input: 'motion-input',
|
|
1412
|
+
depth: 'depth',
|
|
1413
|
+
additionalTextures: [
|
|
1414
|
+
'cloud-history-radiance-current',
|
|
1415
|
+
'cloud-history-transmittance-current',
|
|
1416
|
+
'cloud-history-depth-current',
|
|
1417
|
+
'cloud-history-radiance-previous',
|
|
1418
|
+
'cloud-history-transmittance-previous',
|
|
1419
|
+
'cloud-history-depth-previous',
|
|
1420
|
+
],
|
|
1421
|
+
},
|
|
1422
|
+
},
|
|
1423
|
+
]
|
|
1424
|
+
: []),
|
|
1425
|
+
...(canShadow
|
|
1426
|
+
? [
|
|
1427
|
+
{
|
|
1428
|
+
kind: 'graphics-program' as const,
|
|
1429
|
+
name: 'cloud-layer-shadow-pipeline',
|
|
1430
|
+
program: {
|
|
1431
|
+
shader: 'cloud-layer-shadow',
|
|
1432
|
+
vertexLayout: 'none',
|
|
1433
|
+
colorFormats: [shadowTarget.format],
|
|
1434
|
+
sampleCount: 1 as const,
|
|
1435
|
+
renderState: {
|
|
1436
|
+
depthWriteEnabled: false,
|
|
1437
|
+
depthCompare: 'always' as const,
|
|
1438
|
+
blend: {
|
|
1439
|
+
color: {
|
|
1440
|
+
srcFactor: 'one' as const,
|
|
1441
|
+
dstFactor: 'zero' as const,
|
|
1442
|
+
operation: 'add' as const,
|
|
1443
|
+
},
|
|
1444
|
+
alpha: {
|
|
1445
|
+
srcFactor: 'one' as const,
|
|
1446
|
+
dstFactor: 'zero' as const,
|
|
1447
|
+
operation: 'add' as const,
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
},
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
kind: 'graphics-bindings' as const,
|
|
1455
|
+
name: 'cloud-layer-shadow-view-bindings',
|
|
1456
|
+
program: 'cloud-layer-shadow-pipeline',
|
|
1457
|
+
values: { group: 0, view: true },
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
kind: 'graphics-bindings' as const,
|
|
1461
|
+
name: 'cloud-layer-shadow-bindings',
|
|
1462
|
+
program: 'cloud-layer-shadow-pipeline',
|
|
1463
|
+
values: {
|
|
1464
|
+
group: 1,
|
|
1465
|
+
fullscreen: true,
|
|
1466
|
+
shader: 'cloud-layer-shadow',
|
|
1467
|
+
input: false,
|
|
1468
|
+
depth: 'depth',
|
|
1469
|
+
// The shadow branch's WGSL keeps the shared depth binding
|
|
1470
|
+
// shape, but its light-space path never samples scene depth.
|
|
1471
|
+
// Bind the renderer-owned far-depth fallback instead of
|
|
1472
|
+
// introducing a read-before-main graph dependency.
|
|
1473
|
+
depthFallback: true,
|
|
1474
|
+
additionalTextures: historyAdditionalTextures,
|
|
1475
|
+
storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT],
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
]
|
|
1479
|
+
: []),
|
|
1480
|
+
],
|
|
1481
|
+
passes:
|
|
1482
|
+
context.caps.compute && context.caps.storageBuffer
|
|
1483
|
+
? [
|
|
1484
|
+
{
|
|
1485
|
+
kind: 'compute' as const,
|
|
1486
|
+
name: 'cloud-layer-density-cache',
|
|
1487
|
+
program: CLOUD_LAYER_DENSITY_PROGRAM,
|
|
1488
|
+
bindings: CLOUD_LAYER_DENSITY_BINDINGS,
|
|
1489
|
+
dispatches: [
|
|
1490
|
+
{
|
|
1491
|
+
kind: 'direct' as const,
|
|
1492
|
+
entryPoint: 'cloud_density_cache',
|
|
1493
|
+
workgroups: [Math.max(1, Math.ceil(cacheCount / CLOUD_WORKGROUP_SIZE))],
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
},
|
|
1497
|
+
...(canShadow
|
|
1498
|
+
? [
|
|
1499
|
+
{
|
|
1500
|
+
kind: 'raster' as const,
|
|
1501
|
+
name: 'cloud-layer-shadow',
|
|
1502
|
+
colorAttachments: [
|
|
1503
|
+
{
|
|
1504
|
+
target: 'cloud-shadow',
|
|
1505
|
+
loadOp: 'clear' as const,
|
|
1506
|
+
storeOp: 'store' as const,
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
1509
|
+
sampledTargets: [
|
|
1510
|
+
'cloud-history-radiance-previous',
|
|
1511
|
+
'cloud-history-transmittance-previous',
|
|
1512
|
+
'cloud-history-depth-previous',
|
|
1513
|
+
],
|
|
1514
|
+
draws: [
|
|
1515
|
+
{
|
|
1516
|
+
program: 'cloud-layer-shadow-pipeline',
|
|
1517
|
+
bindings: [
|
|
1518
|
+
'cloud-layer-shadow-view-bindings',
|
|
1519
|
+
'cloud-layer-shadow-bindings',
|
|
1520
|
+
],
|
|
1521
|
+
vertexData: [],
|
|
1522
|
+
vertexLayout: 'none' as const,
|
|
1523
|
+
draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
|
|
1524
|
+
},
|
|
1525
|
+
],
|
|
1526
|
+
},
|
|
1527
|
+
]
|
|
1528
|
+
: []),
|
|
1529
|
+
...(canCompose
|
|
1530
|
+
? [
|
|
1531
|
+
{
|
|
1532
|
+
kind: 'raster' as const,
|
|
1533
|
+
name: 'cloud-layer-transport',
|
|
1534
|
+
colorAttachments: [
|
|
1535
|
+
{
|
|
1536
|
+
target: 'cloud-history-radiance-current',
|
|
1537
|
+
loadOp: 'clear' as const,
|
|
1538
|
+
storeOp: 'store' as const,
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
target: 'cloud-history-transmittance-current',
|
|
1542
|
+
loadOp: 'clear' as const,
|
|
1543
|
+
storeOp: 'store' as const,
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
target: 'cloud-history-depth-current',
|
|
1547
|
+
loadOp: 'clear' as const,
|
|
1548
|
+
storeOp: 'store' as const,
|
|
1549
|
+
},
|
|
1550
|
+
],
|
|
1551
|
+
sampledTargets: [
|
|
1552
|
+
'motion-input',
|
|
1553
|
+
'depth',
|
|
1554
|
+
...(transportUsesShadowCache ? ['cloud-shadow'] : []),
|
|
1555
|
+
],
|
|
1556
|
+
draws: [
|
|
1557
|
+
{
|
|
1558
|
+
program: 'cloud-layer-transport-pipeline',
|
|
1559
|
+
bindings: [
|
|
1560
|
+
'cloud-layer-transport-view-bindings',
|
|
1561
|
+
'cloud-layer-transport-bindings',
|
|
1562
|
+
],
|
|
1563
|
+
vertexData: [],
|
|
1564
|
+
vertexLayout: 'none' as const,
|
|
1565
|
+
draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
|
|
1566
|
+
},
|
|
1567
|
+
],
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
kind: 'raster' as const,
|
|
1571
|
+
name: 'cloud-layer-resolve',
|
|
1572
|
+
colorAttachments: [
|
|
1573
|
+
{
|
|
1574
|
+
target: 'motion-output',
|
|
1575
|
+
loadOp: 'clear' as const,
|
|
1576
|
+
storeOp: 'store' as const,
|
|
1577
|
+
},
|
|
1578
|
+
],
|
|
1579
|
+
sampledTargets: [
|
|
1580
|
+
'motion-input',
|
|
1581
|
+
'depth',
|
|
1582
|
+
'cloud-history-radiance-current',
|
|
1583
|
+
'cloud-history-transmittance-current',
|
|
1584
|
+
'cloud-history-depth-current',
|
|
1585
|
+
'cloud-history-radiance-previous',
|
|
1586
|
+
'cloud-history-transmittance-previous',
|
|
1587
|
+
'cloud-history-depth-previous',
|
|
1588
|
+
],
|
|
1589
|
+
draws: [
|
|
1590
|
+
{
|
|
1591
|
+
program: 'cloud-layer-resolve-pipeline',
|
|
1592
|
+
bindings: [
|
|
1593
|
+
'cloud-layer-resolve-view-bindings',
|
|
1594
|
+
'cloud-layer-resolve-bindings',
|
|
1595
|
+
],
|
|
1596
|
+
vertexData: [],
|
|
1597
|
+
vertexLayout: 'none' as const,
|
|
1598
|
+
draw: { kind: 'draw' as const, vertexCount: 3, instanceCount: 1 },
|
|
1599
|
+
},
|
|
1600
|
+
],
|
|
1601
|
+
},
|
|
1602
|
+
]
|
|
1603
|
+
: []),
|
|
1604
|
+
]
|
|
1605
|
+
: [],
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Renderer-installed cloud producer. Authoring is read once from World at
|
|
1611
|
+
* extraction; resource preparation, graph ordering and retirement stay with
|
|
1612
|
+
* RenderFeatureHost.
|
|
1613
|
+
*/
|
|
1614
|
+
export function createCloudLayerFeature(
|
|
1615
|
+
options: CloudLayerFeatureOptions = {},
|
|
1616
|
+
): RenderFeature<CloudLayerFeatureFrame> {
|
|
1617
|
+
let cachedSourceKey: string | undefined;
|
|
1618
|
+
let cached: CloudDensityCache | undefined;
|
|
1619
|
+
let packedCache: Uint32Array | undefined;
|
|
1620
|
+
let generation = 0;
|
|
1621
|
+
const histories = new CloudHistoryStore();
|
|
1622
|
+
let temporalResetCount = 0;
|
|
1623
|
+
let lastTemporalReset: CloudTemporalFrame['reasons'][number] | undefined;
|
|
1624
|
+
return Object.freeze({
|
|
1625
|
+
identity: CLOUD_LAYER_FEATURE_IDENTITY,
|
|
1626
|
+
placement: 'scene' as const,
|
|
1627
|
+
requiredCapabilities: ['compute', 'storageBuffer', 'rgba16floatRenderable'] as const,
|
|
1628
|
+
// Cloud programs are generated by the feature and have no user-authored
|
|
1629
|
+
// manifest entry to await. Use the immediate module path so first-frame
|
|
1630
|
+
// feature admission does not publish a transient pending-shader failure;
|
|
1631
|
+
// pipeline creation remains the validation boundary for this WGSL.
|
|
1632
|
+
shaderModuleMode: 'immediate' as const,
|
|
1633
|
+
extract: (
|
|
1634
|
+
context: RenderFeatureExtractContext,
|
|
1635
|
+
): Result<CloudLayerFeatureFrame, RenderError> => {
|
|
1636
|
+
if (options.enabled === false) {
|
|
1637
|
+
histories.reset();
|
|
1638
|
+
return ok({
|
|
1639
|
+
params: undefined,
|
|
1640
|
+
cache: undefined,
|
|
1641
|
+
sourceKey: undefined,
|
|
1642
|
+
timeSeconds: 0,
|
|
1643
|
+
sunDirection: undefined,
|
|
1644
|
+
sunRadiance: undefined,
|
|
1645
|
+
shadowProjection: undefined,
|
|
1646
|
+
temporal: undefined,
|
|
1647
|
+
generation,
|
|
1648
|
+
inspection: inspectCloudLayer({ authored: false }),
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
const world = context.worlds[context.owner];
|
|
1652
|
+
if (world === undefined) {
|
|
1653
|
+
histories.reset();
|
|
1654
|
+
return ok({
|
|
1655
|
+
params: undefined,
|
|
1656
|
+
cache: undefined,
|
|
1657
|
+
sourceKey: undefined,
|
|
1658
|
+
timeSeconds: 0,
|
|
1659
|
+
sunDirection: undefined,
|
|
1660
|
+
sunRadiance: undefined,
|
|
1661
|
+
shadowProjection: undefined,
|
|
1662
|
+
temporal: undefined,
|
|
1663
|
+
generation,
|
|
1664
|
+
inspection: inspectCloudLayer({ authored: false }),
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
// The normal RenderSystem extract owns World reads and selected-sun
|
|
1668
|
+
// resolution. Direct feature probes must provide the same frame facts;
|
|
1669
|
+
// this keeps the feature from growing a detached authoring callback or
|
|
1670
|
+
// a second per-feature ECS registry.
|
|
1671
|
+
const cloud = context.frame?.cloudLayer;
|
|
1672
|
+
const capability =
|
|
1673
|
+
context.caps === undefined ? undefined : cloudCapabilitiesFromRhi(context.caps);
|
|
1674
|
+
if (cloud === undefined) {
|
|
1675
|
+
histories.reset();
|
|
1676
|
+
return ok({
|
|
1677
|
+
params: undefined,
|
|
1678
|
+
cache: undefined,
|
|
1679
|
+
sourceKey: undefined,
|
|
1680
|
+
timeSeconds: 0,
|
|
1681
|
+
sunDirection: undefined,
|
|
1682
|
+
sunRadiance: undefined,
|
|
1683
|
+
shadowProjection: undefined,
|
|
1684
|
+
temporal: undefined,
|
|
1685
|
+
generation,
|
|
1686
|
+
inspection: inspectCloudLayer({
|
|
1687
|
+
authored: false,
|
|
1688
|
+
...(capability === undefined ? {} : { capability }),
|
|
1689
|
+
}),
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
const formationKey = cloudLayerFormationKey(cloud.params);
|
|
1693
|
+
if (cached === undefined || cachedSourceKey !== formationKey) {
|
|
1694
|
+
cached = buildCloudDensityCache(cloud.params);
|
|
1695
|
+
packedCache = packCloudDensityCache(cached);
|
|
1696
|
+
cachedSourceKey = formationKey;
|
|
1697
|
+
generation += 1;
|
|
1698
|
+
}
|
|
1699
|
+
const view = context.frame?.view;
|
|
1700
|
+
const shadowProjection =
|
|
1701
|
+
view === undefined || cloud.sunDirection === undefined
|
|
1702
|
+
? undefined
|
|
1703
|
+
: createCloudShadowProjection({
|
|
1704
|
+
center: view.shadowAnchor ?? view.cameraPosition,
|
|
1705
|
+
sunDirection: cloud.sunDirection,
|
|
1706
|
+
range: cloud.params.shadowRange,
|
|
1707
|
+
resolution: cloudShadowResolutionForQuality(cloud.params.quality),
|
|
1708
|
+
});
|
|
1709
|
+
const temporalSignature: CloudTemporalSignature | undefined =
|
|
1710
|
+
view === undefined
|
|
1711
|
+
? undefined
|
|
1712
|
+
: {
|
|
1713
|
+
sourceKey: cloud.sourceKey,
|
|
1714
|
+
viewId: view.identity,
|
|
1715
|
+
authoringGeneration: generation,
|
|
1716
|
+
cloudShadowRevision: shadowProjection?.revision ?? 0,
|
|
1717
|
+
cameraRevision: view.cameraRevision,
|
|
1718
|
+
deviceGeneration: view.deviceGeneration,
|
|
1719
|
+
width: view.width,
|
|
1720
|
+
height: view.height,
|
|
1721
|
+
timeSeconds: cloud.worldTimeSeconds,
|
|
1722
|
+
quality: cloud.params.quality,
|
|
1723
|
+
...(view.sceneDepthVersion === undefined
|
|
1724
|
+
? {}
|
|
1725
|
+
: { sceneDepthVersion: view.sceneDepthVersion }),
|
|
1726
|
+
};
|
|
1727
|
+
const temporal =
|
|
1728
|
+
temporalSignature === undefined
|
|
1729
|
+
? undefined
|
|
1730
|
+
: (() => {
|
|
1731
|
+
const decision = histories.begin(temporalSignature, {
|
|
1732
|
+
...(view?.cameraCut === undefined ? {} : { cameraCut: view.cameraCut }),
|
|
1733
|
+
...(view?.recovery === undefined ? {} : { recovery: view.recovery }),
|
|
1734
|
+
});
|
|
1735
|
+
const history =
|
|
1736
|
+
decision.history ?? createCloudHistory(temporalSignature, generation, false);
|
|
1737
|
+
return Object.freeze({
|
|
1738
|
+
signature: temporalSignature,
|
|
1739
|
+
history,
|
|
1740
|
+
reset: decision.reset,
|
|
1741
|
+
reasons: decision.reasons,
|
|
1742
|
+
});
|
|
1743
|
+
})();
|
|
1744
|
+
const resourceFacts = inspectCloudLayerResources({
|
|
1745
|
+
generation,
|
|
1746
|
+
cache: cached,
|
|
1747
|
+
...(shadowProjection === undefined ? {} : { shadow: shadowProjection }),
|
|
1748
|
+
...(temporal?.history === undefined ? {} : { history: temporal.history }),
|
|
1749
|
+
});
|
|
1750
|
+
const temporalReset = temporal?.reasons.at(-1);
|
|
1751
|
+
if (temporal?.reset === true) temporalResetCount += 1;
|
|
1752
|
+
if (temporalReset !== undefined) lastTemporalReset = temporalReset;
|
|
1753
|
+
return ok({
|
|
1754
|
+
params: cloud.params,
|
|
1755
|
+
cache: cached,
|
|
1756
|
+
sourceKey: cloud.sourceKey,
|
|
1757
|
+
timeSeconds: cloud.worldTimeSeconds,
|
|
1758
|
+
sunDirection: cloud.sunDirection,
|
|
1759
|
+
sunRadiance: cloud.sunRadiance,
|
|
1760
|
+
shadowProjection,
|
|
1761
|
+
temporal,
|
|
1762
|
+
generation,
|
|
1763
|
+
inspection: inspectCloudLayer({
|
|
1764
|
+
authored: true,
|
|
1765
|
+
sourceKey: cloud.sourceKey,
|
|
1766
|
+
generation,
|
|
1767
|
+
resourceFacts,
|
|
1768
|
+
...(shadowProjection === undefined ? {} : { shadowRevision: shadowProjection.revision }),
|
|
1769
|
+
...(temporal === undefined
|
|
1770
|
+
? {}
|
|
1771
|
+
: {
|
|
1772
|
+
temporalResets: temporalResetCount,
|
|
1773
|
+
...(lastTemporalReset === undefined ? {} : { lastTemporalReset }),
|
|
1774
|
+
}),
|
|
1775
|
+
...(capability === undefined ? {} : { capability }),
|
|
1776
|
+
budget: {
|
|
1777
|
+
quality: cloud.params.quality,
|
|
1778
|
+
reason:
|
|
1779
|
+
'physical GPU timing and residency receipts are unavailable until a prepared adapter submits them',
|
|
1780
|
+
},
|
|
1781
|
+
}),
|
|
1782
|
+
});
|
|
1783
|
+
},
|
|
1784
|
+
onFrameSubmitted: (frame: CloudLayerFeatureFrame) => {
|
|
1785
|
+
if (frame.temporal?.history !== undefined) {
|
|
1786
|
+
// Advance the metadata timestamp only after submit. The next frame's
|
|
1787
|
+
// GPU reprojection then receives the elapsed time since this exact
|
|
1788
|
+
// submitted signature, while an aborted frame keeps the old entry.
|
|
1789
|
+
histories.commit(
|
|
1790
|
+
frame.temporal.signature,
|
|
1791
|
+
createCloudHistory(frame.temporal.signature, frame.generation, true),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
},
|
|
1795
|
+
onFrameAborted: (frame: CloudLayerFeatureFrame) => {
|
|
1796
|
+
// A reset candidate is committed only after queue submission. The store
|
|
1797
|
+
// still contains the previous entry while this frame is rejected.
|
|
1798
|
+
void frame;
|
|
1799
|
+
},
|
|
1800
|
+
plan: (
|
|
1801
|
+
frame: CloudLayerFeatureFrame,
|
|
1802
|
+
context: RenderFeaturePlanContext,
|
|
1803
|
+
): Result<RenderFeaturePlan, RenderError> => {
|
|
1804
|
+
if (frame.cache === undefined || packedCache === undefined || frame.params === undefined) {
|
|
1805
|
+
return ok({ resources: [], passes: [] });
|
|
1806
|
+
}
|
|
1807
|
+
return ok(planCloudDensity(frame, packedCache, context));
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
}
|