@driftengine/core 3.63.0 → 4.1.0
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/CHANGELOG.json +122 -0
- package/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/core/loop.js +18 -2
- package/dist/core/storage.js +5 -0
- package/dist/geometry/meshBuilder.d.ts +1 -1
- package/dist/geometry/meshBuilder.js +6 -2
- package/dist/index.d.ts +69 -4
- package/dist/index.js +112 -2
- package/dist/input/input.js +6 -1
- package/dist/math/exact.d.ts +13 -3
- package/dist/math/exact.js +68 -3
- package/dist/math/frustum.d.ts +17 -2
- package/dist/math/frustum.js +24 -0
- package/dist/render/backend/api.d.ts +34 -1
- package/dist/render/backend/api.js +22 -1
- package/dist/render/backend/budget.d.ts +7 -6
- package/dist/render/backend/budget.js +7 -6
- package/dist/render/backend/createRenderer.d.ts +57 -0
- package/dist/render/backend/createRenderer.js +51 -2
- package/dist/render/backend/materialChanges.d.ts +41 -0
- package/dist/render/backend/materialChanges.js +38 -0
- package/dist/render/backend/select.js +8 -0
- package/dist/render/backend/webgl2/renderer.d.ts +80 -1
- package/dist/render/backend/webgl2/renderer.js +208 -10
- package/dist/render/backend/webgpu/buffers.d.ts +20 -1
- package/dist/render/backend/webgpu/buffers.js +116 -58
- package/dist/render/backend/webgpu/fieldCompose.d.ts +156 -0
- package/dist/render/backend/webgpu/fieldCompose.js +396 -0
- package/dist/render/backend/webgpu/giFieldPass.d.ts +66 -0
- package/dist/render/backend/webgpu/giFieldPass.js +137 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +728 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +2473 -0
- package/dist/render/backend/webgpu/linePass.d.ts +4 -14
- package/dist/render/backend/webgpu/linePass.js +4 -33
- package/dist/render/backend/webgpu/probeBake.d.ts +148 -0
- package/dist/render/backend/webgpu/probeBake.js +493 -0
- package/dist/render/backend/webgpu/renderer.d.ts +379 -24
- package/dist/render/backend/webgpu/renderer.js +1369 -76
- package/dist/render/backend/webgpu/scatterPass.js +15 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +19 -9
- package/dist/render/backend/webgpu/surfaceTexturePass.js +58 -32
- package/dist/render/causticsRenderer.d.ts +3 -1
- package/dist/render/causticsRenderer.js +4 -1
- package/dist/render/depthConvention.d.ts +12 -0
- package/dist/render/depthConvention.js +14 -0
- package/dist/render/deviceFeatures.d.ts +32 -0
- package/dist/render/deviceFeatures.js +41 -0
- package/dist/render/fog.d.ts +79 -0
- package/dist/render/fog.js +138 -0
- package/dist/render/frame/alias.d.ts +30 -0
- package/dist/render/frame/alias.js +61 -0
- package/dist/render/frame/deps.d.ts +37 -0
- package/dist/render/frame/deps.js +122 -0
- package/dist/render/frame/flushGraph.d.ts +43 -0
- package/dist/render/frame/flushGraph.js +84 -0
- package/dist/render/frame/graphSchedule.d.ts +49 -0
- package/dist/render/frame/graphSchedule.js +120 -0
- package/dist/render/frame/index.d.ts +15 -0
- package/dist/render/frame/index.js +8 -0
- package/dist/render/frame/lifetime.d.ts +24 -0
- package/dist/render/frame/lifetime.js +36 -0
- package/dist/render/frame/passTimings.d.ts +42 -0
- package/dist/render/frame/passTimings.js +38 -0
- package/dist/render/frame/validate.d.ts +15 -0
- package/dist/render/frame/validate.js +22 -0
- package/dist/render/frame/virtual.d.ts +44 -0
- package/dist/render/frame/virtual.js +61 -0
- package/dist/render/gi/chain.d.ts +82 -0
- package/dist/render/gi/chain.js +135 -0
- package/dist/render/gi/denoise.d.ts +114 -0
- package/dist/render/gi/denoise.js +169 -0
- package/dist/render/gi/fieldScene.d.ts +125 -0
- package/dist/render/gi/fieldScene.js +219 -0
- package/dist/render/gi/globalField.d.ts +157 -0
- package/dist/render/gi/globalField.js +373 -0
- package/dist/render/gi/probeGridFit.d.ts +24 -0
- package/dist/render/gi/probeGridFit.js +71 -0
- package/dist/render/gi/probeTrace.d.ts +82 -0
- package/dist/render/gi/probeTrace.js +128 -0
- package/dist/render/gi/probeVolume.d.ts +136 -0
- package/dist/render/gi/probeVolume.js +191 -0
- package/dist/render/gi/reflection.d.ts +73 -0
- package/dist/render/gi/reflection.js +109 -0
- package/dist/render/gi/traceField.d.ts +91 -0
- package/dist/render/gi/traceField.js +129 -0
- package/dist/render/gi/traceScreen.d.ts +68 -0
- package/dist/render/gi/traceScreen.js +82 -0
- package/dist/render/gpuScene.d.ts +38 -0
- package/dist/render/gpuScene.js +41 -0
- package/dist/render/gpudriven/alphaTest.d.ts +28 -0
- package/dist/render/gpudriven/alphaTest.js +32 -0
- package/dist/render/gpudriven/clusterUpload.d.ts +59 -0
- package/dist/render/gpudriven/clusterUpload.js +75 -0
- package/dist/render/gpudriven/clusterWorld.d.ts +57 -0
- package/dist/render/gpudriven/clusterWorld.js +155 -0
- package/dist/render/gpudriven/compact.d.ts +51 -0
- package/dist/render/gpudriven/compact.js +68 -0
- package/dist/render/gpudriven/cullClusters.d.ts +129 -0
- package/dist/render/gpudriven/cullClusters.js +237 -0
- package/dist/render/gpudriven/decodeTables.d.ts +64 -0
- package/dist/render/gpudriven/decodeTables.js +219 -0
- package/dist/render/gpudriven/frustum.d.ts +25 -0
- package/dist/render/gpudriven/frustum.js +59 -0
- package/dist/render/gpudriven/geometryHarness.d.ts +15 -0
- package/dist/render/gpudriven/geometryHarness.js +31 -0
- package/dist/render/gpudriven/hzb.d.ts +60 -0
- package/dist/render/gpudriven/hzb.js +97 -0
- package/dist/render/gpudriven/ibl.d.ts +85 -0
- package/dist/render/gpudriven/ibl.js +129 -0
- package/dist/render/gpudriven/indirect.d.ts +67 -0
- package/dist/render/gpudriven/indirect.js +77 -0
- package/dist/render/gpudriven/instances.d.ts +43 -0
- package/dist/render/gpudriven/instances.js +172 -0
- package/dist/render/gpudriven/lit.d.ts +165 -0
- package/dist/render/gpudriven/lit.js +165 -0
- package/dist/render/gpudriven/lodCut.d.ts +28 -0
- package/dist/render/gpudriven/lodCut.js +38 -0
- package/dist/render/gpudriven/materialBin.d.ts +60 -0
- package/dist/render/gpudriven/materialBin.js +108 -0
- package/dist/render/gpudriven/materialTable.d.ts +88 -0
- package/dist/render/gpudriven/materialTable.js +115 -0
- package/dist/render/gpudriven/pipeline.d.ts +135 -0
- package/dist/render/gpudriven/pipeline.js +237 -0
- package/dist/render/gpudriven/rangeAlloc.d.ts +50 -0
- package/dist/render/gpudriven/rangeAlloc.js +112 -0
- package/dist/render/gpudriven/sceneUpload.d.ts +96 -0
- package/dist/render/gpudriven/sceneUpload.js +115 -0
- package/dist/render/gpudriven/shadeBins.d.ts +92 -0
- package/dist/render/gpudriven/shadeBins.js +182 -0
- package/dist/render/gpudriven/shadow.d.ts +136 -0
- package/dist/render/gpudriven/shadow.js +224 -0
- package/dist/render/gpudriven/shadowCamera.d.ts +87 -0
- package/dist/render/gpudriven/shadowCamera.js +345 -0
- package/dist/render/gpudriven/streamScene.d.ts +194 -0
- package/dist/render/gpudriven/streamScene.js +459 -0
- package/dist/render/gpudriven/surfaceFrame.d.ts +25 -0
- package/dist/render/gpudriven/surfaceFrame.js +51 -0
- package/dist/render/gpudriven/twoPhase.d.ts +122 -0
- package/dist/render/gpudriven/twoPhase.js +160 -0
- package/dist/render/gpudriven/visbuffer.d.ts +25 -0
- package/dist/render/gpudriven/visbuffer.js +37 -0
- package/dist/render/halfFloat.d.ts +10 -0
- package/dist/render/halfFloat.js +23 -4
- package/dist/render/inference/attentionTiled.d.ts +27 -0
- package/dist/render/inference/attentionTiled.js +117 -0
- package/dist/render/inference/denseKernels.d.ts +29 -0
- package/dist/render/inference/denseKernels.js +312 -0
- package/dist/render/inference/device.d.ts +21 -0
- package/dist/render/inference/device.js +32 -0
- package/dist/render/inference/deviceGraph.d.ts +34 -0
- package/dist/render/inference/deviceGraph.js +17 -0
- package/dist/render/inference/fuse.d.ts +15 -0
- package/dist/render/inference/fuse.js +37 -0
- package/dist/render/inference/kernelKit.d.ts +52 -0
- package/dist/render/inference/kernelKit.js +70 -0
- package/dist/render/inference/kernels.d.ts +3 -0
- package/dist/render/inference/kernels.js +11 -0
- package/dist/render/inference/plan.d.ts +13 -0
- package/dist/render/inference/plan.js +18 -0
- package/dist/render/inference/reuse.d.ts +39 -0
- package/dist/render/inference/reuse.js +65 -0
- package/dist/render/inference/runner.d.ts +31 -0
- package/dist/render/inference/runner.js +175 -0
- package/dist/render/inference/schedule.d.ts +39 -0
- package/dist/render/inference/schedule.js +59 -0
- package/dist/render/inference/shapeKernels.d.ts +10 -0
- package/dist/render/inference/shapeKernels.js +80 -0
- package/dist/render/inference/spatialKernels.d.ts +21 -0
- package/dist/render/inference/spatialKernels.js +179 -0
- package/dist/render/inference/splatRaster.wgsl.d.ts +75 -0
- package/dist/render/inference/splatRaster.wgsl.js +337 -0
- package/dist/render/inference/splatTiles.d.ts +79 -0
- package/dist/render/inference/splatTiles.js +161 -0
- package/dist/render/lineBatch.d.ts +2 -5
- package/dist/render/lineBatch.js +4 -24
- package/dist/render/mesh.d.ts +4 -0
- package/dist/render/mesh.js +45 -1
- package/dist/render/pass.d.ts +145 -8
- package/dist/render/prefilterEnvMap.d.ts +22 -0
- package/dist/render/prefilterEnvMap.js +31 -0
- package/dist/render/readback.d.ts +30 -0
- package/dist/render/readback.js +47 -0
- package/dist/render/recon/clamp.d.ts +36 -0
- package/dist/render/recon/clamp.js +92 -0
- package/dist/render/recon/disocclusion.d.ts +60 -0
- package/dist/render/recon/disocclusion.js +109 -0
- package/dist/render/recon/frameGen.d.ts +81 -0
- package/dist/render/recon/frameGen.js +150 -0
- package/dist/render/recon/frameSizes.d.ts +34 -0
- package/dist/render/recon/frameSizes.js +58 -0
- package/dist/render/recon/jitter.d.ts +60 -0
- package/dist/render/recon/jitter.js +118 -0
- package/dist/render/recon/motionVectors.d.ts +55 -0
- package/dist/render/recon/motionVectors.js +107 -0
- package/dist/render/recon/reproject.d.ts +41 -0
- package/dist/render/recon/reproject.js +131 -0
- package/dist/render/recon/resolve.d.ts +90 -0
- package/dist/render/recon/resolve.js +313 -0
- package/dist/render/recon/tier.d.ts +24 -0
- package/dist/render/recon/tier.js +8 -0
- package/dist/render/reflectionProbe.d.ts +26 -0
- package/dist/render/reflectionProbe.js +33 -0
- package/dist/render/renderQuality.d.ts +69 -0
- package/dist/render/renderQuality.js +22 -0
- package/dist/render/screenSpaceReflection.d.ts +23 -0
- package/dist/render/screenSpaceReflection.js +22 -3
- package/dist/render/sdfFont.js +2 -0
- package/dist/render/segmentQuads.d.ts +13 -0
- package/dist/render/segmentQuads.js +32 -0
- package/dist/render/shaders/flat/lobes.d.ts +27 -0
- package/dist/render/shaders/flat/lobes.js +33 -3
- package/dist/render/shaders/flat/preamble.d.ts +7 -0
- package/dist/render/shaders/flat/preamble.js +24 -1
- package/dist/render/shaders/flat/probeGrid.d.ts +1 -1
- package/dist/render/shaders/flat/probeGrid.js +71 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -8
- package/dist/render/shaders/generated/flat.wgsl.js +64 -24
- package/dist/render/shaders/gi/composeField.wgsl.d.ts +56 -0
- package/dist/render/shaders/gi/composeField.wgsl.js +231 -0
- package/dist/render/shaders/gi/marchField.wgsl.d.ts +30 -0
- package/dist/render/shaders/gi/marchField.wgsl.js +141 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +113 -0
- package/dist/render/shaders/gi/probeBake.wgsl.js +635 -0
- package/dist/render/shaders/gi/probeTrace.wgsl.d.ts +72 -0
- package/dist/render/shaders/gi/probeTrace.wgsl.js +261 -0
- package/dist/render/shaders/gi/probeVolume.wgsl.d.ts +49 -0
- package/dist/render/shaders/gi/probeVolume.wgsl.js +242 -0
- package/dist/render/shaders/gi/sampleField.wgsl.d.ts +32 -0
- package/dist/render/shaders/gi/sampleField.wgsl.js +134 -0
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +26 -0
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +339 -0
- package/dist/render/shaders/gpudriven/compact.wgsl.d.ts +29 -0
- package/dist/render/shaders/gpudriven/compact.wgsl.js +58 -0
- package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +90 -0
- package/dist/render/shaders/gpudriven/cull.wgsl.js +423 -0
- package/dist/render/shaders/gpudriven/decode.wgsl.d.ts +72 -0
- package/dist/render/shaders/gpudriven/decode.wgsl.js +349 -0
- package/dist/render/shaders/gpudriven/environment.wgsl.d.ts +44 -0
- package/dist/render/shaders/gpudriven/environment.wgsl.js +137 -0
- package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +28 -0
- package/dist/render/shaders/gpudriven/fog.wgsl.js +107 -0
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +13 -0
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +62 -0
- package/dist/render/shaders/gpudriven/materialBin.wgsl.d.ts +51 -0
- package/dist/render/shaders/gpudriven/materialBin.wgsl.js +112 -0
- package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +25 -0
- package/dist/render/shaders/gpudriven/materialTable.wgsl.js +43 -0
- package/dist/render/shaders/gpudriven/meshTransform.wgsl.d.ts +18 -0
- package/dist/render/shaders/gpudriven/meshTransform.wgsl.js +37 -0
- package/dist/render/shaders/gpudriven/present.wgsl.d.ts +43 -0
- package/dist/render/shaders/gpudriven/present.wgsl.js +127 -0
- package/dist/render/shaders/gpudriven/shade.wgsl.d.ts +127 -0
- package/dist/render/shaders/gpudriven/shade.wgsl.js +874 -0
- package/dist/render/shaders/gpudriven/shadow.wgsl.d.ts +44 -0
- package/dist/render/shaders/gpudriven/shadow.wgsl.js +308 -0
- package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.d.ts +28 -0
- package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.js +245 -0
- package/dist/render/shaders/network.wgsl.d.ts +101 -0
- package/dist/render/shaders/network.wgsl.js +206 -0
- package/dist/render/shaders/recon/motion.wgsl.d.ts +44 -0
- package/dist/render/shaders/recon/motion.wgsl.js +96 -0
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +51 -0
- package/dist/render/shaders/recon/resolve.wgsl.js +733 -0
- package/dist/render/temporalAa.js +8 -33
- package/dist/render/textLayout.js +12 -6
- package/dist/render/textRenderer.d.ts +3 -1
- package/dist/render/textRenderer.js +4 -1
- package/dist/render/translucentQueue.d.ts +6 -0
- package/dist/render/translucentQueue.js +11 -0
- package/dist/render/windStreakRenderer.d.ts +2 -1
- package/dist/render/windStreakRenderer.js +3 -1
- package/dist/world/cell.d.ts +63 -0
- package/dist/world/cell.js +181 -0
- package/dist/world/cellStream.d.ts +103 -0
- package/dist/world/cellStream.js +198 -0
- package/dist/world/freeze.d.ts +81 -0
- package/dist/world/freeze.js +114 -0
- package/dist/world/rebase.d.ts +29 -0
- package/dist/world/rebase.js +41 -0
- package/package.json +8 -3
- package/scripts/frames.mjs +92 -0
- package/scripts/gpuCompute.mjs +323 -0
- package/scripts/png.d.mts +16 -0
- package/scripts/png.mjs +25 -0
- package/src/core/loop.ts +19 -2
- package/src/core/storage.ts +5 -0
- package/src/geometry/meshBuilder.ts +5 -1
- package/src/index.ts +246 -4
- package/src/input/input.ts +6 -1
- package/src/math/exact.ts +69 -3
- package/src/math/frustum.ts +52 -3
- package/src/render/backend/api.ts +42 -1
- package/src/render/backend/budget.ts +7 -6
- package/src/render/backend/createRenderer.ts +92 -3
- package/src/render/backend/materialChanges.ts +54 -0
- package/src/render/backend/select.ts +8 -0
- package/src/render/backend/webgl2/renderer.ts +211 -9
- package/src/render/backend/webgpu/buffers.ts +182 -69
- package/src/render/backend/webgpu/fieldCompose.ts +482 -0
- package/src/render/backend/webgpu/giFieldPass.ts +200 -0
- package/src/render/backend/webgpu/gpuDrivenPass.ts +2993 -0
- package/src/render/backend/webgpu/linePass.ts +4 -39
- package/src/render/backend/webgpu/probeBake.ts +588 -0
- package/src/render/backend/webgpu/renderer.ts +1481 -76
- package/src/render/backend/webgpu/scatterPass.ts +15 -1
- package/src/render/backend/webgpu/surfaceTexturePass.ts +63 -36
- package/src/render/causticsRenderer.ts +5 -2
- package/src/render/depthConvention.ts +15 -0
- package/src/render/deviceFeatures.ts +77 -0
- package/src/render/fog.ts +185 -0
- package/src/render/frame/alias.ts +97 -0
- package/src/render/frame/deps.ts +155 -0
- package/src/render/frame/flushGraph.ts +135 -0
- package/src/render/frame/graphSchedule.ts +170 -0
- package/src/render/frame/index.ts +52 -0
- package/src/render/frame/lifetime.ts +61 -0
- package/src/render/frame/passTimings.ts +69 -0
- package/src/render/frame/validate.ts +40 -0
- package/src/render/frame/virtual.ts +91 -0
- package/src/render/gi/chain.ts +225 -0
- package/src/render/gi/denoise.ts +276 -0
- package/src/render/gi/fieldScene.ts +264 -0
- package/src/render/gi/globalField.ts +543 -0
- package/src/render/gi/probeGridFit.ts +89 -0
- package/src/render/gi/probeTrace.ts +143 -0
- package/src/render/gi/probeVolume.ts +288 -0
- package/src/render/gi/reflection.ts +163 -0
- package/src/render/gi/traceField.ts +223 -0
- package/src/render/gi/traceScreen.ts +106 -0
- package/src/render/gpuScene.ts +80 -0
- package/src/render/gpudriven/alphaTest.ts +33 -0
- package/src/render/gpudriven/clusterUpload.ts +123 -0
- package/src/render/gpudriven/clusterWorld.ts +239 -0
- package/src/render/gpudriven/compact.ts +80 -0
- package/src/render/gpudriven/cullClusters.ts +333 -0
- package/src/render/gpudriven/decodeTables.ts +332 -0
- package/src/render/gpudriven/frustum.ts +70 -0
- package/src/render/gpudriven/geometryHarness.ts +51 -0
- package/src/render/gpudriven/hzb.ts +113 -0
- package/src/render/gpudriven/ibl.ts +147 -0
- package/src/render/gpudriven/indirect.ts +89 -0
- package/src/render/gpudriven/instances.ts +234 -0
- package/src/render/gpudriven/lit.ts +329 -0
- package/src/render/gpudriven/lodCut.ts +62 -0
- package/src/render/gpudriven/materialBin.ts +131 -0
- package/src/render/gpudriven/materialTable.ts +164 -0
- package/src/render/gpudriven/pipeline.ts +336 -0
- package/src/render/gpudriven/rangeAlloc.ts +122 -0
- package/src/render/gpudriven/sceneUpload.ts +173 -0
- package/src/render/gpudriven/shadeBins.ts +222 -0
- package/src/render/gpudriven/shadow.ts +285 -0
- package/src/render/gpudriven/shadowCamera.ts +406 -0
- package/src/render/gpudriven/streamScene.ts +575 -0
- package/src/render/gpudriven/surfaceFrame.ts +68 -0
- package/src/render/gpudriven/twoPhase.ts +214 -0
- package/src/render/gpudriven/visbuffer.ts +41 -0
- package/src/render/halfFloat.ts +22 -4
- package/src/render/inference/attentionTiled.ts +132 -0
- package/src/render/inference/denseKernels.ts +369 -0
- package/src/render/inference/device.ts +44 -0
- package/src/render/inference/deviceGraph.ts +39 -0
- package/src/render/inference/fuse.ts +47 -0
- package/src/render/inference/kernelKit.ts +120 -0
- package/src/render/inference/kernels.ts +18 -0
- package/src/render/inference/plan.ts +26 -0
- package/src/render/inference/reuse.ts +79 -0
- package/src/render/inference/runner.ts +204 -0
- package/src/render/inference/schedule.ts +80 -0
- package/src/render/inference/shapeKernels.ts +112 -0
- package/src/render/inference/spatialKernels.ts +220 -0
- package/src/render/inference/splatRaster.wgsl.ts +348 -0
- package/src/render/inference/splatTiles.ts +186 -0
- package/src/render/lineBatch.ts +4 -24
- package/src/render/mesh.ts +46 -1
- package/src/render/pass.ts +155 -10
- package/src/render/prefilterEnvMap.ts +33 -0
- package/src/render/readback.ts +73 -0
- package/src/render/recon/clamp.ts +113 -0
- package/src/render/recon/disocclusion.ts +143 -0
- package/src/render/recon/frameGen.ts +191 -0
- package/src/render/recon/frameSizes.ts +64 -0
- package/src/render/recon/jitter.ts +126 -0
- package/src/render/recon/motionVectors.ts +138 -0
- package/src/render/recon/reproject.ts +179 -0
- package/src/render/recon/resolve.ts +444 -0
- package/src/render/recon/tier.ts +31 -0
- package/src/render/reflectionProbe.ts +34 -0
- package/src/render/renderQuality.ts +87 -0
- package/src/render/screenSpaceReflection.ts +37 -3
- package/src/render/sdfFont.ts +2 -0
- package/src/render/segmentQuads.ts +39 -0
- package/src/render/shaders/flat/lobes.ts +35 -3
- package/src/render/shaders/flat/preamble.ts +25 -1
- package/src/render/shaders/flat/probeGrid.ts +77 -2
- package/src/render/shaders/generated/flat.wgsl.ts +64 -24
- package/src/render/shaders/gi/composeField.wgsl.ts +239 -0
- package/src/render/shaders/gi/marchField.wgsl.ts +146 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +649 -0
- package/src/render/shaders/gi/probeTrace.wgsl.ts +269 -0
- package/src/render/shaders/gi/probeVolume.wgsl.ts +246 -0
- package/src/render/shaders/gi/sampleField.wgsl.ts +140 -0
- package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +343 -0
- package/src/render/shaders/gpudriven/compact.wgsl.ts +59 -0
- package/src/render/shaders/gpudriven/cull.wgsl.ts +433 -0
- package/src/render/shaders/gpudriven/decode.wgsl.ts +373 -0
- package/src/render/shaders/gpudriven/environment.wgsl.ts +141 -0
- package/src/render/shaders/gpudriven/fog.wgsl.ts +110 -0
- package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +62 -0
- package/src/render/shaders/gpudriven/materialBin.wgsl.ts +116 -0
- package/src/render/shaders/gpudriven/materialTable.wgsl.ts +46 -0
- package/src/render/shaders/gpudriven/meshTransform.wgsl.ts +37 -0
- package/src/render/shaders/gpudriven/present.wgsl.ts +129 -0
- package/src/render/shaders/gpudriven/shade.wgsl.ts +884 -0
- package/src/render/shaders/gpudriven/shadow.wgsl.ts +322 -0
- package/src/render/shaders/gpudriven/visbufferRaster.wgsl.ts +251 -0
- package/src/render/shaders/network.wgsl.ts +238 -0
- package/src/render/shaders/recon/motion.wgsl.ts +101 -0
- package/src/render/shaders/recon/resolve.wgsl.ts +741 -0
- package/src/render/temporalAa.ts +9 -33
- package/src/render/textLayout.ts +12 -6
- package/src/render/textRenderer.ts +5 -2
- package/src/render/translucentQueue.ts +11 -0
- package/src/render/windStreakRenderer.ts +4 -2
- package/src/world/cell.ts +231 -0
- package/src/world/cellStream.ts +278 -0
- package/src/world/freeze.ts +157 -0
- package/src/world/rebase.ts +56 -0
|
@@ -2,7 +2,13 @@ import { mat4 } from 'gl-matrix';
|
|
|
2
2
|
import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
|
|
3
3
|
import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
|
|
4
4
|
import { TranslucentQueue } from '../../translucentQueue.js';
|
|
5
|
-
import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.js';
|
|
5
|
+
import { DEPTH_CLEAR, DEPTH_COMPARE_EQUAL, MAX_DEPTH_LAYER, REVERSED_DEPTH, } from '../../depthConvention.js';
|
|
6
|
+
import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.js';
|
|
7
|
+
import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.js';
|
|
8
|
+
import { RECON_HISTORY_FORMAT, RECON_PARAM_FLOATS, RECON_WORKGROUP, reconResolveWgsl, } from '../../shaders/recon/resolve.wgsl.js';
|
|
9
|
+
import { MOTION_DRAW_FLOATS, MOTION_DRAW_STRIDE, MOTION_FRAME_FLOATS, MOTION_TARGET_FORMAT, RECON_MOTION_WGSL, } from '../../shaders/recon/motion.wgsl.js';
|
|
10
|
+
import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.js';
|
|
11
|
+
import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.js';
|
|
6
12
|
import { clamp } from '../../../math/scalar.js';
|
|
7
13
|
import { Camera } from '../../camera.js';
|
|
8
14
|
import { PickableSet } from '../../pickable.js';
|
|
@@ -13,11 +19,11 @@ import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomProfileWarn
|
|
|
13
19
|
import { createResolvedWater, resolveWater, waterAppearance, } from '../../waterDraw.js';
|
|
14
20
|
import { createResolvedLightVolume, DEPTH_01_TO_CLIP_Y_DOWN, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
|
|
15
21
|
import { WIND_STREAK_DEFAULTS, createResolvedWindStreaks, resolveWindStreaks, } from '../../windStreakDraw.js';
|
|
16
|
-
import { expandBoltSegments } from '../../segmentQuads.js';
|
|
22
|
+
import { expandBoltSegments, expandLineSegments } from '../../segmentQuads.js';
|
|
17
23
|
import { LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from '../../lightBudget.js';
|
|
18
24
|
import { roughnessForLevel } from '../../prefilterEnvMap.js';
|
|
19
25
|
import { equirectToCubeFaces } from '../../equirectToCube.js';
|
|
20
|
-
import { ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid } from '../../probeGrid.js';
|
|
26
|
+
import { MAX_ENV_PROBES, ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid, } from '../../probeGrid.js';
|
|
21
27
|
import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.js';
|
|
22
28
|
import { environmentTexels } from './environmentTexels.js';
|
|
23
29
|
import { packIesAtlas } from '../../iesProfile.js';
|
|
@@ -65,16 +71,33 @@ import { PARTICLE_VERT_FIELDS, PARTICLE_VERT_SIZE, createGpuParticles, createPar
|
|
|
65
71
|
import { WIND_STREAK_FRAG_FIELDS, WIND_STREAK_FRAG_SIZE, WIND_STREAK_VERT_FIELDS, WIND_STREAK_VERT_SIZE, createGpuWindStreaks, createWindStreakBindGroup, createWindStreakBindGroupLayout, windStreakPipeline, } from './windStreakPass.js';
|
|
66
72
|
import { FLOCK_FRAG_FIELDS, FLOCK_FRAG_SIZE, FLOCK_VERT_FIELDS, FLOCK_VERT_SIZE, createFlockBindGroup, createFlockBindGroupLayout, createGpuFlock, flockPipeline, } from './flockPass.js';
|
|
67
73
|
import { BOLT_FRAG_FIELDS, BOLT_FRAG_SIZE, BOLT_VERT_FIELDS, BOLT_VERT_SIZE, boltPipeline, createBoltBindGroup, createBoltBindGroupLayout, createGpuBolts, } from './boltPass.js';
|
|
68
|
-
import { LINE_FRAG_FIELDS, LINE_FRAG_SIZE, LINE_VERT_FIELDS, LINE_VERT_SIZE, createGpuLines, createLineBindGroup, createLineBindGroupLayout,
|
|
74
|
+
import { LINE_FRAG_FIELDS, LINE_FRAG_SIZE, LINE_VERT_FIELDS, LINE_VERT_SIZE, createGpuLines, createLineBindGroup, createLineBindGroupLayout, linePipeline, } from './linePass.js';
|
|
69
75
|
import { LIGHT_VOLUME_VERT_FIELDS, LIGHT_VOLUME_VERT_SIZE, createLightVolumeBindGroup, createLightVolumeBindGroupLayout, lightVolumeFragmentBindings, lightVolumePipeline, } from './lightVolumePass.js';
|
|
70
76
|
import { aimReflection, createGpuReflection } from './reflectionPass.js';
|
|
71
77
|
import { FrameBudget } from '../budget.js';
|
|
78
|
+
import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
|
|
72
79
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
|
|
73
80
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
|
|
81
|
+
import { DistanceFieldScene } from '../../gi/fieldScene.js';
|
|
82
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.js';
|
|
83
|
+
import { ProbeBaker } from './probeBake.js';
|
|
84
|
+
import { distanceFieldBounds } from '../../gi/fieldScene.js';
|
|
85
|
+
import { fitProbeGrid } from '../../gi/probeGridFit.js';
|
|
86
|
+
/**
|
|
87
|
+
* Metres between probes when the renderer fits a grid itself.
|
|
88
|
+
*
|
|
89
|
+
* **Two, which is a room rather than a building.** The spacing is what decides how local the light
|
|
90
|
+
* can be: a bounce off a wall reaches a surface two metres away and no nearer detail than that.
|
|
91
|
+
* `fitProbeGrid` widens it uniformly when the budget bites, so a large scene gets a coarser grid
|
|
92
|
+
* rather than a truncated one.
|
|
93
|
+
*/
|
|
94
|
+
const INDIRECT_PROBE_SPACING = 2;
|
|
95
|
+
/** What the trace is given when a frame never said what was lighting it. */
|
|
96
|
+
const NO_SUN = [0, 0, 0];
|
|
74
97
|
import { CLIP_Y_FLIP, MAX_REFLECTIVE_SURFACES, REFLECTION_EDGE_FADE, ReflectionQueue, } from '../../screenSpaceReflection.js';
|
|
75
98
|
import { decalScissor } from '../../decalProjector.js';
|
|
76
99
|
import { createCommandPool, resetPool, takeCommand } from './drawCommand.js';
|
|
77
|
-
import { RESOURCE_COUNT, createArena, keptNodes, maskOf, nodeCount, nodeState, nodeVerb, recordNode, resetArena, schedule, scratchFor, } from '../../frame/index.js';
|
|
100
|
+
import { RESOURCE_COUNT, createArena, createFlushSchedule, keptNodes, maskOf, nodeCount, nodeState, nodeVerb, recordNode, resetArena, schedule, scheduleFlush, scratchFor, } from '../../frame/index.js';
|
|
78
101
|
import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.js';
|
|
79
102
|
import { OcclusionBuffer } from '../../occlusion.js';
|
|
80
103
|
import { boundsVisible } from '../../visibility.js';
|
|
@@ -352,20 +375,32 @@ function resolvedStoreOp(multisampled, terminal, allowed) {
|
|
|
352
375
|
return multisampled && terminal && allowed ? 'discard' : 'store';
|
|
353
376
|
}
|
|
354
377
|
/**
|
|
355
|
-
* Clip space differs between the two APIs
|
|
378
|
+
* Clip space differs between the two APIs in **one** way, and the other half of `CLIP_CORRECTION`
|
|
379
|
+
* cancels something this repository writes itself.
|
|
356
380
|
*
|
|
357
|
-
* **
|
|
358
|
-
*
|
|
359
|
-
*
|
|
381
|
+
* **Depth lands in a different range, and that half is a real difference between the APIs.**
|
|
382
|
+
* OpenGL clips z to [-1, 1] and WebGPU to [0, 1], so an uncorrected matrix throws away the near
|
|
383
|
+
* half of the depth buffer and compares what remains against the wrong distances.
|
|
360
384
|
*
|
|
361
|
-
* **
|
|
362
|
-
*
|
|
363
|
-
*
|
|
385
|
+
* **The Y negation cancels one the generator writes, and this comment used to say otherwise.**
|
|
386
|
+
* It claimed the flip was the framebuffer origin: WebGPU's is the top-left and OpenGL's is the
|
|
387
|
+
* bottom-left, so a projection built for WebGL2 "draws the world upside down here". That is not
|
|
388
|
+
* what either viewport transform does — both put clip `y = +1` at the top of the image, because
|
|
389
|
+
* OpenGL measures its window upward from the bottom-left and WebGPU measures its framebuffer
|
|
390
|
+
* downward from the top-left. An uncorrected projection lands the same way up on both.
|
|
364
391
|
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
392
|
+
* What does flip it is `naga`. Every module in `shaders/generated/` ends its vertex entry point
|
|
393
|
+
* with `gl_Position.y = -gl_Position.y`, which is how that translator adapts a GLSL shader to
|
|
394
|
+
* WebGPU — and the first frame this backend rendered, a colonnade hanging from the ceiling, was
|
|
395
|
+
* *that* line and not the API. So the pair is: the generator negates, this negates back, and the
|
|
396
|
+
* net effect on a generated shader is the depth remap alone. `shaders/generated.test.ts` pins the
|
|
397
|
+
* generator's half so the two cannot drift apart.
|
|
398
|
+
*
|
|
399
|
+
* **Which makes it the wrong matrix for a shader somebody wrote by hand**, since there is no
|
|
400
|
+
* negation to cancel — see `DEPTH_CORRECTION` below, and `PassDevice` for the seam where a
|
|
401
|
+
* contributed pass chooses. Wiring the GPU-driven pipeline's hand-written raster found it: the
|
|
402
|
+
* picture was an exact vertical mirror of the forward path's, and `frontFace: 'cw'` made it draw
|
|
403
|
+
* the right faces of a mirrored world.
|
|
369
404
|
*
|
|
370
405
|
* **This one is for a target that gets presented. `SHADOW_CLIP_CORRECTION` is for one that
|
|
371
406
|
* gets sampled, and they are not the same transform.**
|
|
@@ -431,6 +466,36 @@ const CLIP_CORRECTION = new Float32Array([
|
|
|
431
466
|
* against, applied by accident. Without the flip it culls what it says it culls, and matches
|
|
432
467
|
* what `renderer.ts` does through the directional pass.
|
|
433
468
|
*/
|
|
469
|
+
/**
|
|
470
|
+
* `CLIP_CORRECTION` without the Y negation: the depth remap alone, reversed.
|
|
471
|
+
*
|
|
472
|
+
* **What a hand-written WGSL shader wants.** The negation in `CLIP_CORRECTION` is there to cancel
|
|
473
|
+
* the one `naga` writes into every generated vertex entry point; a shader that was not generated
|
|
474
|
+
* carries no such line, so taking that matrix mirrors its picture vertically — and mirrors its
|
|
475
|
+
* triangle winding with it, which is how it reads as a culling problem rather than as a flip.
|
|
476
|
+
*
|
|
477
|
+
* The depth half is not optional and is the reason this is a matrix rather than nothing: OpenGL
|
|
478
|
+
* clips z to [-1, 1] and WebGPU to [0, 1], and the row is `0.5 - 0.5z` rather than `0.5z + 0.5`
|
|
479
|
+
* because `depthConvention.ts` puts the near plane at one.
|
|
480
|
+
*/
|
|
481
|
+
const DEPTH_CORRECTION = new Float32Array([
|
|
482
|
+
1,
|
|
483
|
+
0,
|
|
484
|
+
0,
|
|
485
|
+
0,
|
|
486
|
+
0,
|
|
487
|
+
1,
|
|
488
|
+
0,
|
|
489
|
+
0,
|
|
490
|
+
0,
|
|
491
|
+
0,
|
|
492
|
+
REVERSED_DEPTH ? -0.5 : 0.5,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
0,
|
|
496
|
+
0.5,
|
|
497
|
+
1,
|
|
498
|
+
]);
|
|
434
499
|
const SHADOW_CLIP_CORRECTION = new Float32Array([
|
|
435
500
|
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1,
|
|
436
501
|
]);
|
|
@@ -652,6 +717,16 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
652
717
|
* a while telling consumers that working effects did nothing. All three are read by
|
|
653
718
|
* `composite()` now, and each has been compared against WebGL2 on `demo/dev/probe.html`.
|
|
654
719
|
*/
|
|
720
|
+
/**
|
|
721
|
+
* The y negation every generated vertex stage ends with, as a matrix.
|
|
722
|
+
*
|
|
723
|
+
* **A hand-written shader has no such line, so it has to carry it.** `CLIP_CORRECTION` negates y
|
|
724
|
+
* precisely to cancel the one `naga` writes, and the pair's net effect on a generated shader is the
|
|
725
|
+
* depth remap alone — §3 rows 56 and 57. The motion pass has to put its vertices exactly where the
|
|
726
|
+
* scene put them or its depth test admits nothing, so the renderer multiplies this through the
|
|
727
|
+
* matrix the scene drew with and hands over the product.
|
|
728
|
+
*/
|
|
729
|
+
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
655
730
|
export class WebGPURenderer {
|
|
656
731
|
/**
|
|
657
732
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -799,8 +874,8 @@ export class WebGPURenderer {
|
|
|
799
874
|
perFrameStaging;
|
|
800
875
|
perFrameFloats;
|
|
801
876
|
perFrameInts;
|
|
802
|
-
/** The slot the open material occupies,
|
|
803
|
-
|
|
877
|
+
/** The slot the open material occupies, and whether the next draw must take a new one. */
|
|
878
|
+
materials = new MaterialChanges();
|
|
804
879
|
/*
|
|
805
880
|
* The frame graph's recording, and the machinery that replays it.
|
|
806
881
|
*
|
|
@@ -846,6 +921,7 @@ export class WebGPURenderer {
|
|
|
846
921
|
textBudget = this.budget.line('text draws', MAX_OVERLAYS);
|
|
847
922
|
sdfTextBudget = this.budget.line('sdf text draws', MAX_OVERLAYS);
|
|
848
923
|
lineBudget = this.budget.line('line draws', MAX_OVERLAYS);
|
|
924
|
+
panelBudget = this.budget.line('panels', MAX_OVERLAYS);
|
|
849
925
|
/**
|
|
850
926
|
* What the frame just drawn asked for, and what it was refused.
|
|
851
927
|
*
|
|
@@ -866,6 +942,12 @@ export class WebGPURenderer {
|
|
|
866
942
|
* scatter, plumes and text reached it long before the draw ring was full.
|
|
867
943
|
*/
|
|
868
944
|
replayScratch = new Int32Array(MAX_DRAWS_PER_FRAME);
|
|
945
|
+
/**
|
|
946
|
+
* The identifier graph's working set, where `quality.identifierGraph` asks for it and not
|
|
947
|
+
* otherwise: a renderer scheduling by masks carries none of it. Sized with the arena, and grown
|
|
948
|
+
* by `scheduleFlush` the way the arena grows.
|
|
949
|
+
*/
|
|
950
|
+
flushSchedule;
|
|
869
951
|
scheduledPasses = Array.from({ length: 64 }, () => ({
|
|
870
952
|
writes: 0,
|
|
871
953
|
first: 0,
|
|
@@ -1127,6 +1209,26 @@ export class WebGPURenderer {
|
|
|
1127
1209
|
bloomGroups = [];
|
|
1128
1210
|
frameWidth = 1;
|
|
1129
1211
|
frameHeight = 1;
|
|
1212
|
+
/**
|
|
1213
|
+
* What the scene is drawn at, which is the drawing buffer unless a reconstruction is enlarging it.
|
|
1214
|
+
*
|
|
1215
|
+
* **Everything before the resolve is this size and everything after it is the drawing buffer's.**
|
|
1216
|
+
* The scene colour and its multisampled twin, the depth, the refraction snapshot, the resolved
|
|
1217
|
+
* depth, the occlusion pair, the medium, the translucent buffers, the reflection march, the
|
|
1218
|
+
* decals, the temporal history and the frame pass's own viewport are all before it; the composite
|
|
1219
|
+
* and the overlay are after it, and they are what the viewer's pixels are counted in.
|
|
1220
|
+
*
|
|
1221
|
+
* **The composite is what enlarges the picture, and it does so by existing.** It draws a
|
|
1222
|
+
* full-screen triangle into the swap chain sampling the scene by `uv` through a linear sampler, so
|
|
1223
|
+
* a smaller scene target is magnified by that sample and nothing else changes — which is why this
|
|
1224
|
+
* needs `screenEffects` and is refused without it: with no composite the world draws straight into
|
|
1225
|
+
* the swap chain and there is nothing to enlarge from.
|
|
1226
|
+
*
|
|
1227
|
+
* Equal to the drawing buffer whenever `quality.reconstruction` is 0, which is the default, and
|
|
1228
|
+
* `recon/frameSizes.ts` is the one place the two are related.
|
|
1229
|
+
*/
|
|
1230
|
+
renderWidth = 0;
|
|
1231
|
+
renderHeight = 0;
|
|
1130
1232
|
/** The view-projection this frame was drawn with, for the reprojection motion blur needs. */
|
|
1131
1233
|
previousViewProj = new Float32Array(16);
|
|
1132
1234
|
reprojection = new Float32Array(16);
|
|
@@ -1220,6 +1322,94 @@ export class WebGPURenderer {
|
|
|
1220
1322
|
temporalJitterX = 0;
|
|
1221
1323
|
temporalJitterY = 0;
|
|
1222
1324
|
temporalJittering = false;
|
|
1325
|
+
/**
|
|
1326
|
+
* Whether this frame is being reconstructed, settled once in `bindMeshPass`.
|
|
1327
|
+
*
|
|
1328
|
+
* It is not `quality.reconstruction > 0` on its own: a mirror pass is skipped for the same reason
|
|
1329
|
+
* the temporal jitter skips one — a reflection is not the picture the history holds — and without
|
|
1330
|
+
* a composite there is nothing to enlarge the render from.
|
|
1331
|
+
*/
|
|
1332
|
+
reconstructing = false;
|
|
1333
|
+
/** Said once rather than every frame, as the translucent set's own refusal is. */
|
|
1334
|
+
reconMultisampleSaid = false;
|
|
1335
|
+
reconFrameIndex = -1;
|
|
1336
|
+
reconJitter = new Float32Array(2);
|
|
1337
|
+
reconPreviousJitter = new Float32Array(2);
|
|
1338
|
+
/**
|
|
1339
|
+
* `reconJitter` as a fraction of the clip square, which is what a contributed pass is handed.
|
|
1340
|
+
* See `PrepareContext.jitter`; zero on a frame that is not reconstructed.
|
|
1341
|
+
*/
|
|
1342
|
+
passJitter = new Float32Array(2);
|
|
1343
|
+
/**
|
|
1344
|
+
* The reconstruction's own targets, allocated only while a consumer asks for one.
|
|
1345
|
+
*
|
|
1346
|
+
* **Two histories rather than one**, for the reason the temporal resolve keeps two: a dispatch
|
|
1347
|
+
* cannot read the texture it is writing. The shown picture is a third, because the history holds
|
|
1348
|
+
* the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
|
|
1349
|
+
* frame, which is the halo the reference's own clamp exists to prevent, compounded.
|
|
1350
|
+
*
|
|
1351
|
+
* The motion target is render size and carries a drawn object's own motion where something wrote
|
|
1352
|
+
* one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
|
|
1353
|
+
* motion, which the resolve derives from the depth and the reprojection. Static geometry is
|
|
1354
|
+
* exact that way and a moving object ghosts until the motion pass lands.
|
|
1355
|
+
*/
|
|
1356
|
+
reconHistories = [null, null];
|
|
1357
|
+
reconHistoryViews = [null, null];
|
|
1358
|
+
reconWrite = 0;
|
|
1359
|
+
reconHasHistory = false;
|
|
1360
|
+
reconShown = null;
|
|
1361
|
+
reconShownView = null;
|
|
1362
|
+
reconMotion = null;
|
|
1363
|
+
reconMotionView = null;
|
|
1364
|
+
reconPreviousDepth = null;
|
|
1365
|
+
reconPreviousDepthView = null;
|
|
1366
|
+
reconParams = null;
|
|
1367
|
+
reconStaging = new ArrayBuffer(RECON_PARAM_FLOATS * 4);
|
|
1368
|
+
reconFloats = new Float32Array(this.reconStaging);
|
|
1369
|
+
reconInts = new Uint32Array(this.reconStaging);
|
|
1370
|
+
reconLayout = null;
|
|
1371
|
+
reconResolvePipeline = null;
|
|
1372
|
+
reconSharpenPipeline = null;
|
|
1373
|
+
reconGroups = [null, null];
|
|
1374
|
+
reconSharpenGroups = [null, null];
|
|
1375
|
+
reconRushGroup = null;
|
|
1376
|
+
/**
|
|
1377
|
+
* What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
|
|
1378
|
+
*
|
|
1379
|
+
* A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
|
|
1380
|
+
* this is written on the frame's hot path and the house rule about allocating there binds it: the
|
|
1381
|
+
* matrices are copied into a buffer that only ever grows, and the count is what resets.
|
|
1382
|
+
*/
|
|
1383
|
+
motionMeshes = [];
|
|
1384
|
+
motionMatrices = new Float32Array(0);
|
|
1385
|
+
motionCount = 0;
|
|
1386
|
+
motionLayout = null;
|
|
1387
|
+
/**
|
|
1388
|
+
* One pipeline per vertex stride, because a stride is a property of the *mesh*.
|
|
1389
|
+
*
|
|
1390
|
+
* Which optional attributes a mesh supplied decides how far apart its positions are, and this
|
|
1391
|
+
* pass reads nothing but positions — so two meshes drawn by the same shader still need two
|
|
1392
|
+
* vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
|
|
1393
|
+
* and a handful of strides in any scene.
|
|
1394
|
+
*/
|
|
1395
|
+
motionPipelines = new Map();
|
|
1396
|
+
motionModule = null;
|
|
1397
|
+
motionPipelineLayout = null;
|
|
1398
|
+
motionFrameBuffer = null;
|
|
1399
|
+
motionDrawBuffer = null;
|
|
1400
|
+
motionDrawCapacity = 0;
|
|
1401
|
+
motionGroup = null;
|
|
1402
|
+
motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
|
|
1403
|
+
motionDrawStaging = new Uint8Array(0);
|
|
1404
|
+
/** The scene's own clip transform with the generated shaders' y negation folded in. */
|
|
1405
|
+
motionRaster = mat4.create();
|
|
1406
|
+
reconSizeKey = '';
|
|
1407
|
+
/** Last frame's raw view-projection and its inverse, and the two eyes. */
|
|
1408
|
+
reconPreviousRawViewProj = mat4.create();
|
|
1409
|
+
reconInverseViewProj = mat4.create();
|
|
1410
|
+
reconPreviousInverseViewProj = mat4.create();
|
|
1411
|
+
reconEye = new Float32Array(3);
|
|
1412
|
+
reconPreviousEye = new Float32Array(3);
|
|
1223
1413
|
temporalHistoryUsable = false;
|
|
1224
1414
|
aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
|
|
1225
1415
|
aoFloats = new Float32Array(this.aoStaging);
|
|
@@ -1258,6 +1448,22 @@ export class WebGPURenderer {
|
|
|
1258
1448
|
get hasComposite() {
|
|
1259
1449
|
return this.quality.screenEffects;
|
|
1260
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Whether this renderer is set up to reconstruct, asked in one place by everything that cares.
|
|
1453
|
+
*
|
|
1454
|
+
* **One condition and not three**, which §3 row 132 is the lesson of: the frame's flag, the size
|
|
1455
|
+
* the targets are built at and the size the world is drawn at all turn on the same question, and
|
|
1456
|
+
* three copies of it are three lines no test can fail — each covering the others, so breaking any
|
|
1457
|
+
* one of them changes nothing anybody can see.
|
|
1458
|
+
*
|
|
1459
|
+
* A composite, because the composite is what enlarges the render; one sample, because the motion
|
|
1460
|
+
* pass draws against the depth the scene left and every attachment in a pass must agree about its
|
|
1461
|
+
* sample count — `RECONSTRUCTION_MULTISAMPLE_REFUSAL` says the rest. Whether a *frame* is
|
|
1462
|
+
* reconstructed adds one more question, the mirror's, and that one belongs to the frame.
|
|
1463
|
+
*/
|
|
1464
|
+
get reconstructionWanted() {
|
|
1465
|
+
return this.quality.reconstruction > 0 && this.hasComposite && this.samples === 1;
|
|
1466
|
+
}
|
|
1261
1467
|
/** What a frame pass writes into: the scene target where there is one, the swap chain else. */
|
|
1262
1468
|
/**
|
|
1263
1469
|
* This frame's swap-chain view, taken on first use rather than at `beginFrame`.
|
|
@@ -1359,6 +1565,31 @@ export class WebGPURenderer {
|
|
|
1359
1565
|
* the frame has drawn, so it cannot run until the opaque world is finished. See `DecalQueue`.
|
|
1360
1566
|
*/
|
|
1361
1567
|
decalQueue = new DecalQueue();
|
|
1568
|
+
/** The fields this frame declared, composed at `endFrame`. See `addDistanceField`. */
|
|
1569
|
+
distanceFields = new DistanceFieldScene();
|
|
1570
|
+
/**
|
|
1571
|
+
* The composition of them, built at the first frame that has both the flag and a field.
|
|
1572
|
+
*
|
|
1573
|
+
* **Not at construction**, because it is 1.4 MB of device buffers and a compute pipeline, and a
|
|
1574
|
+
* profile with `indirectLight` on is entitled to declare no fields at all — a scene that lights
|
|
1575
|
+
* itself entirely from a baked probe grid, for one. Nothing is allocated until something would
|
|
1576
|
+
* be composed.
|
|
1577
|
+
*/
|
|
1578
|
+
fieldComposer = null;
|
|
1579
|
+
/**
|
|
1580
|
+
* The probes' own bake, built at the first frame that has the flag, a field and an array.
|
|
1581
|
+
*
|
|
1582
|
+
* Not at construction, for `fieldComposer`'s reason: it is two compute pipelines, a render
|
|
1583
|
+
* pipeline and a handful of buffers, and a profile with the flag on may never declare a field.
|
|
1584
|
+
*/
|
|
1585
|
+
probeBaker = null;
|
|
1586
|
+
/** Which array the baker's views were built against, so a replacement is noticed. */
|
|
1587
|
+
bakerArray = null;
|
|
1588
|
+
/** Reused, because deriving a grid from the declared fields runs in the frame path. */
|
|
1589
|
+
fieldBoundsMin = new Float32Array(3);
|
|
1590
|
+
fieldBoundsMax = new Float32Array(3);
|
|
1591
|
+
/** Said once rather than every frame, as the other refusals are. */
|
|
1592
|
+
indirectGridRefused = false;
|
|
1362
1593
|
decalLayout;
|
|
1363
1594
|
decalUniforms;
|
|
1364
1595
|
decalStaging = new ArrayBuffer(DECAL_SLOT * MAX_DRAWN_DECALS);
|
|
@@ -1394,8 +1625,21 @@ export class WebGPURenderer {
|
|
|
1394
1625
|
decalScissorRect = new Int32Array(4);
|
|
1395
1626
|
oitResolveLayout;
|
|
1396
1627
|
oitResolveGroup = null;
|
|
1397
|
-
/**
|
|
1398
|
-
|
|
1628
|
+
/**
|
|
1629
|
+
* Reused, because the frame loop may not allocate. Mirrors the WebGL2 backend's own, which it
|
|
1630
|
+
* had stopped doing: the other one carried a pane's refraction and this one did not.
|
|
1631
|
+
*/
|
|
1632
|
+
oitReplayOptions = {
|
|
1633
|
+
lit: true,
|
|
1634
|
+
fog: true,
|
|
1635
|
+
toneMapped: true,
|
|
1636
|
+
depthWrite: false,
|
|
1637
|
+
depthLayer: 0,
|
|
1638
|
+
tint: null,
|
|
1639
|
+
refraction: 0,
|
|
1640
|
+
refractTint: undefined,
|
|
1641
|
+
thicknessM: 0,
|
|
1642
|
+
};
|
|
1399
1643
|
reflectionPassActive = false;
|
|
1400
1644
|
/** Said once: a mirror asked for outside a frame is a call-order fault, not a device limit. */
|
|
1401
1645
|
saidMirrorOutsideFrame = false;
|
|
@@ -1728,16 +1972,27 @@ export class WebGPURenderer {
|
|
|
1728
1972
|
createSurfaceTexture(source, options = {}) {
|
|
1729
1973
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
1730
1974
|
}
|
|
1975
|
+
/**
|
|
1976
|
+
* GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
|
|
1977
|
+
*
|
|
1978
|
+
* Not at once, because a draw recorded earlier in the frame the update landed in may still read
|
|
1979
|
+
* one, and a destroyed texture in a submit loses the whole command buffer. Every encoder a frame
|
|
1980
|
+
* opens is submitted by the time the next one begins.
|
|
1981
|
+
*/
|
|
1982
|
+
retiredTextures = [];
|
|
1731
1983
|
updateSurfaceTexture(texture, source) {
|
|
1732
1984
|
if (this.surface.lost)
|
|
1733
1985
|
return;
|
|
1734
|
-
texture.update(source);
|
|
1986
|
+
const replaced = texture.update(source);
|
|
1987
|
+
if (replaced === null)
|
|
1988
|
+
return;
|
|
1989
|
+
/* A new size is a new view, so every group holding the old one is dropped — see below. */
|
|
1990
|
+
this.retiredTextures.push(replaced);
|
|
1991
|
+
this.forgetBindingsOf(texture);
|
|
1735
1992
|
}
|
|
1736
1993
|
disposeSurfaceTexture(texture) {
|
|
1737
1994
|
if (this.surface.lost)
|
|
1738
1995
|
return;
|
|
1739
|
-
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
1740
|
-
this.sdfTextBindGroups.delete(texture);
|
|
1741
1996
|
if (this.albedo === texture) {
|
|
1742
1997
|
this.albedo = null;
|
|
1743
1998
|
this.bindGroup = this.blankAlbedoBindGroup;
|
|
@@ -1766,6 +2021,18 @@ export class WebGPURenderer {
|
|
|
1766
2021
|
this.ormMap = null;
|
|
1767
2022
|
if (this.emissiveMap === texture)
|
|
1768
2023
|
this.emissiveMap = null;
|
|
2024
|
+
this.forgetBindingsOf(texture);
|
|
2025
|
+
texture.dispose();
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Drop every bind group that may hold `texture`'s view, and rebuild the ones bound now.
|
|
2029
|
+
*
|
|
2030
|
+
* For a texture going away and for one whose view has been replaced, which are the same problem:
|
|
2031
|
+
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2032
|
+
*/
|
|
2033
|
+
forgetBindingsOf(texture) {
|
|
2034
|
+
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2035
|
+
this.sdfTextBindGroups.delete(texture);
|
|
1769
2036
|
this.flatBindGroups.clear();
|
|
1770
2037
|
/*
|
|
1771
2038
|
* The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
|
|
@@ -1777,7 +2044,6 @@ export class WebGPURenderer {
|
|
|
1777
2044
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
1778
2045
|
this.albedo = held;
|
|
1779
2046
|
this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
|
|
1780
|
-
texture.dispose();
|
|
1781
2047
|
}
|
|
1782
2048
|
/**
|
|
1783
2049
|
* Choose the surface texture the following `drawMesh` calls sample, or null for none.
|
|
@@ -1932,7 +2198,7 @@ export class WebGPURenderer {
|
|
|
1932
2198
|
if (this.surface.lost)
|
|
1933
2199
|
return;
|
|
1934
2200
|
write(this.perFrameFloats, this.perFrameInts);
|
|
1935
|
-
this.
|
|
2201
|
+
this.materials.dirty();
|
|
1936
2202
|
}
|
|
1937
2203
|
materialField(name) {
|
|
1938
2204
|
return (this.fragment.fields[name]?.offset ?? 0) / 4;
|
|
@@ -2014,8 +2280,17 @@ export class WebGPURenderer {
|
|
|
2014
2280
|
return;
|
|
2015
2281
|
const vertexSlot = this.panelVerts.allocate();
|
|
2016
2282
|
const fragmentSlot = this.panelFrags.allocate();
|
|
2017
|
-
if (vertexSlot === null || fragmentSlot === null)
|
|
2283
|
+
if (vertexSlot === null || fragmentSlot === null) {
|
|
2284
|
+
/*
|
|
2285
|
+
* Said, as its text and line siblings say it: this returned without a word until 2026-09-19,
|
|
2286
|
+
* so an interface of panels lost everything past its sixty-fourth here and nothing on WebGL2.
|
|
2287
|
+
*/
|
|
2288
|
+
if (!this.warnedPanelsFull) {
|
|
2289
|
+
this.warnedPanelsFull = true;
|
|
2290
|
+
console.warn(`WebGPU: more than ${MAX_OVERLAYS} panels in a frame; the rest are skipped. renderer.frameBudget names the line and the count.`);
|
|
2291
|
+
}
|
|
2018
2292
|
return;
|
|
2293
|
+
}
|
|
2019
2294
|
const at = (name) => PANEL_VERT_FIELDS[name]?.offset ?? 0;
|
|
2020
2295
|
this.panelVerts.writeFloats(vertexSlot, at('uRect'), [
|
|
2021
2296
|
rect.left,
|
|
@@ -2054,6 +2329,8 @@ export class WebGPURenderer {
|
|
|
2054
2329
|
pass.draw(PANEL_VERTEX_COUNT);
|
|
2055
2330
|
}
|
|
2056
2331
|
}
|
|
2332
|
+
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
2333
|
+
warnedPanelsFull = false;
|
|
2057
2334
|
/** A 3D text object. See `textPass.ts`; the layout it holds is shared with WebGL2. */
|
|
2058
2335
|
createText() {
|
|
2059
2336
|
return new GpuText(this.surface.device);
|
|
@@ -2981,6 +3258,11 @@ export class WebGPURenderer {
|
|
|
2981
3258
|
it ends the current pass to copy the depth out and opens another in its place. */
|
|
2982
3259
|
if (!this.canDraw())
|
|
2983
3260
|
return;
|
|
3261
|
+
/* Geometry that has not all arrived is not drawn, which is the surface's contract and what
|
|
3262
|
+
every other mesh verb here and the other backend's volume already kept. This one drew a
|
|
3263
|
+
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. */
|
|
3264
|
+
if (!mesh.complete)
|
|
3265
|
+
return;
|
|
2984
3266
|
/* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
|
|
2985
3267
|
now covers a clear night too: a beam with no medium to light is not drawn at all. */
|
|
2986
3268
|
const shown = options.medium === undefined
|
|
@@ -4045,6 +4327,41 @@ export class WebGPURenderer {
|
|
|
4045
4327
|
get canvas() {
|
|
4046
4328
|
return this.surface.canvas;
|
|
4047
4329
|
}
|
|
4330
|
+
/**
|
|
4331
|
+
* The size the world is drawn at this frame, which is **not** the canvas when a reconstruction
|
|
4332
|
+
* is enlarging it.
|
|
4333
|
+
*
|
|
4334
|
+
* **A contributed pass that fills a target of its own must size it from here.** The GPU-driven
|
|
4335
|
+
* rig sized its own targets from `canvas.width` and drew a full drawing-buffer picture into a
|
|
4336
|
+
* scene target two thirds as wide: the frame pass clipped it to the top-left corner and the
|
|
4337
|
+
* composite magnified that corner back over the screen, which is a plausible-looking picture of
|
|
4338
|
+
* the wrong part of the world. The device raises nothing — a viewport smaller than the thing
|
|
4339
|
+
* drawn is legal — so there is no failure to see, only a scene that has moved.
|
|
4340
|
+
*
|
|
4341
|
+
* Valid from `beginFrame`; before the first frame it is the canvas, which is what a pass sizing
|
|
4342
|
+
* itself at registration wants.
|
|
4343
|
+
*/
|
|
4344
|
+
get sceneWidth() {
|
|
4345
|
+
return this.sceneSize().width;
|
|
4346
|
+
}
|
|
4347
|
+
get sceneHeight() {
|
|
4348
|
+
return this.sceneSize().height;
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* The render size this frame will use, computed rather than remembered.
|
|
4352
|
+
*
|
|
4353
|
+
* **Computed, because a pass asks before the frame it is asking about.** `GpuDrivenPass.resize`
|
|
4354
|
+
* runs before `beginFrame` — that is when `prepare` records the pipeline — so a stored field
|
|
4355
|
+
* would answer with the previous frame's size, which is right every frame but the one where it
|
|
4356
|
+
* changed. The first frame after a reconstruction is switched on is exactly the frame that
|
|
4357
|
+
* matters.
|
|
4358
|
+
*/
|
|
4359
|
+
sceneSize() {
|
|
4360
|
+
const { canvas } = this.surface;
|
|
4361
|
+
return this.reconstructionWanted
|
|
4362
|
+
? reconRenderSize(canvas.width, canvas.height, this.quality.reconstruction)
|
|
4363
|
+
: { width: Math.max(1, canvas.width), height: Math.max(1, canvas.height) };
|
|
4364
|
+
}
|
|
4048
4365
|
/**
|
|
4049
4366
|
* Whether the device has gone.
|
|
4050
4367
|
*
|
|
@@ -4156,6 +4473,10 @@ export class WebGPURenderer {
|
|
|
4156
4473
|
quality = this.quality;
|
|
4157
4474
|
this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
|
|
4158
4475
|
this.exposure = quality.outputExposure;
|
|
4476
|
+
this.flushSchedule =
|
|
4477
|
+
quality.frameGraph && quality.identifierGraph
|
|
4478
|
+
? createFlushSchedule(MAX_DRAWS_PER_FRAME)
|
|
4479
|
+
: null;
|
|
4159
4480
|
const { device } = surface;
|
|
4160
4481
|
/* Off unless a consumer asked: see `RenderQuality.gpuTiming`. A timer that can
|
|
4161
4482
|
invalidate a command buffer must not be something everybody pays for. */
|
|
@@ -4903,7 +5224,8 @@ export class WebGPURenderer {
|
|
|
4903
5224
|
*/
|
|
4904
5225
|
this.panelLayout = createPanelBindGroupLayout(device);
|
|
4905
5226
|
this.panelCorners = createPanelCorners(device);
|
|
4906
|
-
|
|
5227
|
+
/* The budget on one of the pair only: a panel is one ask, whichever ring runs out. */
|
|
5228
|
+
this.panelVerts = new UniformRing(device, PANEL_VERT_SIZE, MAX_OVERLAYS, USAGE_UNIFORM_DST, 'panel.vertRing', this.panelBudget);
|
|
4907
5229
|
this.panelFrags = new UniformRing(device, PANEL_FRAG_SIZE, MAX_OVERLAYS, USAGE_UNIFORM_DST, 'panel.fragRing');
|
|
4908
5230
|
this.panelBindGroup = createPanelBindGroup(device, this.panelLayout, this.panelVerts.buffer, this.panelFrags.buffer);
|
|
4909
5231
|
this.textLayout = createTextBindGroupLayout(device);
|
|
@@ -5040,6 +5362,10 @@ export class WebGPURenderer {
|
|
|
5040
5362
|
* uncaught exceptions a second in somebody's error reporting.
|
|
5041
5363
|
*/
|
|
5042
5364
|
beginFrame(clearColor) {
|
|
5365
|
+
/* Whatever an update replaced during the last frame; nothing recorded can read it now. */
|
|
5366
|
+
for (const texture of this.retiredTextures)
|
|
5367
|
+
texture.destroy();
|
|
5368
|
+
this.retiredTextures.length = 0;
|
|
5043
5369
|
/*
|
|
5044
5370
|
* **Decided here and held for the frame**, so a draw cannot be recorded under one rule and
|
|
5045
5371
|
* replayed under another. Multisampling is excluded: the two buffers are single-sampled and a
|
|
@@ -5049,6 +5375,8 @@ export class WebGPURenderer {
|
|
|
5049
5375
|
this.translucentQueue.reset();
|
|
5050
5376
|
this.decalQueue.reset();
|
|
5051
5377
|
this.reflectionQueue.reset();
|
|
5378
|
+
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
5379
|
+
this.distanceFields.reset();
|
|
5052
5380
|
this.oitActive =
|
|
5053
5381
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
5054
5382
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -5122,7 +5450,9 @@ export class WebGPURenderer {
|
|
|
5122
5450
|
this.lineFrags.reset();
|
|
5123
5451
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
5124
5452
|
this.perFrame.reset();
|
|
5125
|
-
this.
|
|
5453
|
+
this.materials.dirty();
|
|
5454
|
+
/* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
|
|
5455
|
+
this.motionCount = 0;
|
|
5126
5456
|
/*
|
|
5127
5457
|
* The canvas rather than the swap chain's texture, and the difference is that reading this
|
|
5128
5458
|
* does not *acquire* anything. See `swapView`.
|
|
@@ -5133,8 +5463,17 @@ export class WebGPURenderer {
|
|
|
5133
5463
|
* without the target it draws into, and a scene is free never to call `resize`.
|
|
5134
5464
|
*/
|
|
5135
5465
|
const { canvas } = this.surface;
|
|
5136
|
-
|
|
5137
|
-
|
|
5466
|
+
/*
|
|
5467
|
+
* **The render size first, because every target below is sized from it.** Off — and without a
|
|
5468
|
+
* composite to enlarge from — it is the drawing buffer exactly, which is what makes a frame
|
|
5469
|
+
* with no reconstruction byte-for-byte the frame this renderer has always drawn.
|
|
5470
|
+
*/
|
|
5471
|
+
const render = this.sceneSize();
|
|
5472
|
+
this.renderWidth = render.width;
|
|
5473
|
+
this.renderHeight = render.height;
|
|
5474
|
+
this.ensureDepth(render.width, render.height);
|
|
5475
|
+
this.ensureComposite(render.width, render.height);
|
|
5476
|
+
this.ensureReconstruction(render.width, render.height, Math.max(1, canvas.width), Math.max(1, canvas.height));
|
|
5138
5477
|
/*
|
|
5139
5478
|
* **Not acquired here, and that is the fix for a frame flashing black.**
|
|
5140
5479
|
*
|
|
@@ -5152,6 +5491,7 @@ export class WebGPURenderer {
|
|
|
5152
5491
|
*/
|
|
5153
5492
|
this.frameSwapView = null;
|
|
5154
5493
|
this.encoder = this.surface.device.createCommandEncoder();
|
|
5494
|
+
this.settleReconJitter();
|
|
5155
5495
|
/*
|
|
5156
5496
|
* Passes that own a target fill it here — after the encoder exists and before anything opens
|
|
5157
5497
|
* the frame's render pass, including the eager path twenty lines below. See
|
|
@@ -5418,6 +5758,8 @@ export class WebGPURenderer {
|
|
|
5418
5758
|
*/
|
|
5419
5759
|
if (!this.reflectionPassActive) {
|
|
5420
5760
|
mat4.copy(this.frameRawViewProj, camera.viewProjection);
|
|
5761
|
+
/* The eye in world space, which the resolve's normals are turned to face. */
|
|
5762
|
+
this.reconEye.set(camera.position);
|
|
5421
5763
|
this.frameEye.set(camera.position);
|
|
5422
5764
|
/* Held for the medium's march, which runs long after every draw and needs the sun the
|
|
5423
5765
|
frame was lit by. Referenced rather than copied, exactly as `renderer.ts` holds its
|
|
@@ -5431,16 +5773,53 @@ export class WebGPURenderer {
|
|
|
5431
5773
|
* is skipped by the same guard that skips it for motion blur: it is not the picture the
|
|
5432
5774
|
* history holds, and jittering it would resolve a reflection against the viewer's frame.
|
|
5433
5775
|
*/
|
|
5776
|
+
/*
|
|
5777
|
+
* **Reconstruction jitters instead of the temporal resolve, never as well as it.** Both
|
|
5778
|
+
* accumulate a history out of a jittered sequence, and running them together would resolve the
|
|
5779
|
+
* frame twice — once at the render size and again at the output size, against a history whose
|
|
5780
|
+
* own frames had already been mixed. So this is a choice rather than a pair, reconstruction
|
|
5781
|
+
* wins where a consumer asked for both, and `temporalResolve` is skipped while it runs.
|
|
5782
|
+
*
|
|
5783
|
+
* Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
|
|
5784
|
+
* render pixel covers, where the temporal resolve's eight are enough at one to one.
|
|
5785
|
+
*/
|
|
5786
|
+
this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
|
|
5787
|
+
if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
|
|
5788
|
+
this.reconMultisampleSaid = true;
|
|
5789
|
+
console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
|
|
5790
|
+
}
|
|
5791
|
+
if (this.reconstructing) {
|
|
5792
|
+
/*
|
|
5793
|
+
* The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
|
|
5794
|
+
* advanced here: a frame binding its mesh pass twice is one frame of the sequence.
|
|
5795
|
+
*
|
|
5796
|
+
* **The y offset is negated on the way in, and that is not a preference.** This shifts the
|
|
5797
|
+
* *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
|
|
5798
|
+
* every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
|
|
5799
|
+
* arrives on screen with its sign reversed, while x, which nothing negates, arrives as
|
|
5800
|
+
* given. The temporal resolve cannot see this because its shader never reads the offset; the
|
|
5801
|
+
* reconstruction reads it at every texel it unprojects, and a sign error there moves every
|
|
5802
|
+
* sample two jitters away from where the depth says it is.
|
|
5803
|
+
*/
|
|
5804
|
+
this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, this.reconJitter[0], -this.reconJitter[1], this.renderWidth, this.renderHeight);
|
|
5805
|
+
this.temporalJittering = false;
|
|
5806
|
+
this.temporalHistoryUsable = false;
|
|
5807
|
+
}
|
|
5434
5808
|
this.temporalJittering =
|
|
5435
|
-
this.
|
|
5809
|
+
!this.reconstructing &&
|
|
5810
|
+
this.quality.temporalAa &&
|
|
5811
|
+
this.quality.screenEffects &&
|
|
5812
|
+
!this.reflectionPassActive;
|
|
5436
5813
|
if (this.temporalJittering) {
|
|
5437
|
-
|
|
5814
|
+
/* The scene's size, not the drawing buffer's: the history is a picture of the render. */
|
|
5815
|
+
this.temporalHistoryUsable = this.temporalHistory.openFrame(this.renderWidth, this.renderHeight);
|
|
5438
5816
|
const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
|
|
5439
5817
|
this.temporalJitterX = jx;
|
|
5440
5818
|
this.temporalJitterY = jy;
|
|
5441
|
-
|
|
5819
|
+
/* A jitter is half a *render* texel, which is what the sequence is spread over. */
|
|
5820
|
+
this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, jx, jy, this.renderWidth, this.renderHeight);
|
|
5442
5821
|
}
|
|
5443
|
-
else {
|
|
5822
|
+
else if (!this.reconstructing) {
|
|
5444
5823
|
this.temporalHistoryUsable = false;
|
|
5445
5824
|
this.viewProj = this.correctedViewProj;
|
|
5446
5825
|
}
|
|
@@ -5692,6 +6071,8 @@ export class WebGPURenderer {
|
|
|
5692
6071
|
* face size, and the array's stops one level below the diffuse term.
|
|
5693
6072
|
*/
|
|
5694
6073
|
f[at('uEnvironmentEdge')] = this.probeEdge;
|
|
6074
|
+
/* Off until the indirect pass fills the moment layers. See `quality.indirectLight`. */
|
|
6075
|
+
f[at('uProbeVisibilityEnabled')] = 0;
|
|
5695
6076
|
f[at('uEnvironmentIrradianceLevel')] = irradianceLevelFor(this.probeEdge);
|
|
5696
6077
|
f[at('uProbeGridAmbient')] = usable && this.probeAmbient ? 1 : 0;
|
|
5697
6078
|
}
|
|
@@ -5881,7 +6262,7 @@ export class WebGPURenderer {
|
|
|
5881
6262
|
* block rather than uploading it: the next draw takes the slot. `renderer.ts` resets the
|
|
5882
6263
|
* same terms here, which is why a pass cannot inherit a material from the one before it.
|
|
5883
6264
|
*/
|
|
5884
|
-
this.
|
|
6265
|
+
this.materials.dirty();
|
|
5885
6266
|
/* A pass starts with no material, exactly as `renderer.ts` resets these terms. */
|
|
5886
6267
|
this.albedo = null;
|
|
5887
6268
|
this.bindGroup = this.blankAlbedoBindGroup;
|
|
@@ -5896,8 +6277,8 @@ export class WebGPURenderer {
|
|
|
5896
6277
|
* each, which is what it asked for.
|
|
5897
6278
|
*/
|
|
5898
6279
|
materialSlotForDraw() {
|
|
5899
|
-
if (this.
|
|
5900
|
-
return this.
|
|
6280
|
+
if (this.materials.open)
|
|
6281
|
+
return this.materials.slot;
|
|
5901
6282
|
const slot = this.perFrame.allocate();
|
|
5902
6283
|
if (slot === null) {
|
|
5903
6284
|
if (!this.warnedMaterialsFull) {
|
|
@@ -5907,7 +6288,7 @@ export class WebGPURenderer {
|
|
|
5907
6288
|
return null;
|
|
5908
6289
|
}
|
|
5909
6290
|
this.perFrame.writeBlock(slot, this.perFrameInts);
|
|
5910
|
-
this.
|
|
6291
|
+
this.materials.slot = slot;
|
|
5911
6292
|
return slot;
|
|
5912
6293
|
}
|
|
5913
6294
|
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
@@ -5926,7 +6307,9 @@ export class WebGPURenderer {
|
|
|
5926
6307
|
* surfaces should win got the answer on WebGL2 and a per-pixel coin toss here — with nothing
|
|
5927
6308
|
* failing and nothing in the parity ledger saying so.
|
|
5928
6309
|
*/
|
|
5929
|
-
drawMesh(mesh, model, depthLayer = 0, tint = null
|
|
6310
|
+
drawMesh(mesh, model, depthLayer = 0, tint = null,
|
|
6311
|
+
/** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
|
|
6312
|
+
previousModel = null) {
|
|
5930
6313
|
/*
|
|
5931
6314
|
* Skipped where the flag asks and the bounds say so.
|
|
5932
6315
|
*
|
|
@@ -5947,6 +6330,13 @@ export class WebGPURenderer {
|
|
|
5947
6330
|
*/
|
|
5948
6331
|
if (this.quality.cullDraws && this.occluded(mesh.bounds, model))
|
|
5949
6332
|
return;
|
|
6333
|
+
/*
|
|
6334
|
+
* Recorded after the two culls rather than before: a mesh the frame does not draw writes no
|
|
6335
|
+
* depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
|
|
6336
|
+
* where it happened to pass, write motion for a surface no pixel of the frame belongs to.
|
|
6337
|
+
*/
|
|
6338
|
+
if (previousModel !== null)
|
|
6339
|
+
this.recordMotion(mesh, model, previousModel);
|
|
5950
6340
|
this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
|
|
5951
6341
|
}
|
|
5952
6342
|
/**
|
|
@@ -6115,7 +6505,6 @@ export class WebGPURenderer {
|
|
|
6115
6505
|
/* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
|
|
6116
6506
|
below — the revealage pass draws with it at zero and its colour multiplied away. */
|
|
6117
6507
|
this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
|
|
6118
|
-
this.materialSlot = -1;
|
|
6119
6508
|
}
|
|
6120
6509
|
/* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
|
|
6121
6510
|
`bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
|
|
@@ -6128,13 +6517,11 @@ export class WebGPURenderer {
|
|
|
6128
6517
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
6129
6518
|
if (!fog)
|
|
6130
6519
|
this.perFrameInts[this.materialField('uFogEnabled')] = 0;
|
|
6131
|
-
this.materialSlot = -1;
|
|
6132
6520
|
}
|
|
6133
6521
|
/* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
|
|
6134
6522
|
a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
|
|
6135
6523
|
if (!toneMapped) {
|
|
6136
6524
|
this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
|
|
6137
|
-
this.materialSlot = -1;
|
|
6138
6525
|
}
|
|
6139
6526
|
/*
|
|
6140
6527
|
* **Refraction, and the snapshot it reads is taken here or the draw does not refract.**
|
|
@@ -6155,8 +6542,12 @@ export class WebGPURenderer {
|
|
|
6155
6542
|
this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
|
|
6156
6543
|
this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
|
|
6157
6544
|
this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
|
|
6158
|
-
this.materialSlot = -1;
|
|
6159
6545
|
}
|
|
6546
|
+
/* Taken for itself when any of those differs from the pass, and put back below: the rule both
|
|
6547
|
+
backends count material changes by. See `materialChanges.ts`. */
|
|
6548
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
|
|
6549
|
+
if (own)
|
|
6550
|
+
this.materials.dirty();
|
|
6160
6551
|
const base = mesh.key ?? 'flat:s0:u0';
|
|
6161
6552
|
/*
|
|
6162
6553
|
* A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
|
|
@@ -6274,25 +6665,23 @@ export class WebGPURenderer {
|
|
|
6274
6665
|
if (dimmed) {
|
|
6275
6666
|
this.perFrameFloats[this.materialField('uOpacity')] = 1;
|
|
6276
6667
|
this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
|
|
6277
|
-
this.materialSlot = -1;
|
|
6278
6668
|
}
|
|
6279
6669
|
if (unlitOrUnfogged) {
|
|
6280
6670
|
if (!lit)
|
|
6281
6671
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
6282
6672
|
if (!fog)
|
|
6283
6673
|
this.perFrameInts[this.materialField('uFogEnabled')] = 1;
|
|
6284
|
-
this.materialSlot = -1;
|
|
6285
6674
|
}
|
|
6286
6675
|
if (refracting) {
|
|
6287
6676
|
/* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
|
|
6288
6677
|
shared, so a strength left set is worn by everything drawn after it. */
|
|
6289
6678
|
this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
|
|
6290
|
-
this.materialSlot = -1;
|
|
6291
6679
|
}
|
|
6292
6680
|
if (!toneMapped) {
|
|
6293
6681
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
6294
|
-
this.materialSlot = -1;
|
|
6295
6682
|
}
|
|
6683
|
+
if (own)
|
|
6684
|
+
this.materials.dirty();
|
|
6296
6685
|
}
|
|
6297
6686
|
/** Release geometry. */
|
|
6298
6687
|
disposeMesh(mesh) {
|
|
@@ -6398,7 +6787,6 @@ export class WebGPURenderer {
|
|
|
6398
6787
|
const dimmed = opacity < 1;
|
|
6399
6788
|
if (dimmed) {
|
|
6400
6789
|
this.perFrameFloats[this.materialField('uOpacity')] = opacity;
|
|
6401
|
-
this.materialSlot = -1;
|
|
6402
6790
|
}
|
|
6403
6791
|
const lit = options.lit ?? true;
|
|
6404
6792
|
const fog = options.fog ?? true;
|
|
@@ -6409,14 +6797,16 @@ export class WebGPURenderer {
|
|
|
6409
6797
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
6410
6798
|
if (!fog)
|
|
6411
6799
|
this.perFrameInts[this.materialField('uFogEnabled')] = 0;
|
|
6412
|
-
this.materialSlot = -1;
|
|
6413
6800
|
}
|
|
6414
6801
|
if (!toneMapped) {
|
|
6415
6802
|
this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
|
|
6416
|
-
this.materialSlot = -1;
|
|
6417
6803
|
}
|
|
6418
6804
|
const depthWrite = options.depthWrite ?? true;
|
|
6419
6805
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
6806
|
+
/* An instanced draw does not refract. See `materialChanges.ts`. */
|
|
6807
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
|
|
6808
|
+
if (own)
|
|
6809
|
+
this.materials.dirty();
|
|
6420
6810
|
const base = mesh.key ?? 'flat:s0:u0';
|
|
6421
6811
|
/* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
|
|
6422
6812
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
@@ -6472,17 +6862,16 @@ export class WebGPURenderer {
|
|
|
6472
6862
|
past the restore leaves the rest of the frame unlit, unfogged or undimmed. */
|
|
6473
6863
|
if (dimmed) {
|
|
6474
6864
|
this.perFrameFloats[this.materialField('uOpacity')] = 1;
|
|
6475
|
-
this.materialSlot = -1;
|
|
6476
6865
|
}
|
|
6477
6866
|
if (unlitOrUnfogged) {
|
|
6478
6867
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
6479
6868
|
this.perFrameInts[this.materialField('uFogEnabled')] = 1;
|
|
6480
|
-
this.materialSlot = -1;
|
|
6481
6869
|
}
|
|
6482
6870
|
if (!toneMapped) {
|
|
6483
6871
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
6484
|
-
this.materialSlot = -1;
|
|
6485
6872
|
}
|
|
6873
|
+
if (own)
|
|
6874
|
+
this.materials.dirty();
|
|
6486
6875
|
}
|
|
6487
6876
|
/* -- Scatter ------------------------------------------------------------------------ */
|
|
6488
6877
|
/** Upload a base mesh and size its per-instance buffers. See `createGpuScatter`. */
|
|
@@ -7166,7 +7555,7 @@ export class WebGPURenderer {
|
|
|
7166
7555
|
this.emissiveMap = emissiveMap;
|
|
7167
7556
|
this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap);
|
|
7168
7557
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
7169
|
-
this.
|
|
7558
|
+
this.materials.dirty();
|
|
7170
7559
|
}
|
|
7171
7560
|
/**
|
|
7172
7561
|
* Rebuild the group for the open material, and drop every cached one.
|
|
@@ -7293,7 +7682,7 @@ export class WebGPURenderer {
|
|
|
7293
7682
|
* consumer running the temporal resolve without motion blur would have reprojected through a
|
|
7294
7683
|
* `previousViewProj` nothing had ever written.
|
|
7295
7684
|
*/
|
|
7296
|
-
const wantsReprojection = blur > 0 || this.temporalJittering;
|
|
7685
|
+
const wantsReprojection = blur > 0 || this.temporalJittering || this.reconstructing;
|
|
7297
7686
|
if (wantsReprojection && this.viewProj !== null) {
|
|
7298
7687
|
if (this.hasPreviousView) {
|
|
7299
7688
|
mat4.invert(this.reprojection, this.correctedViewProj);
|
|
@@ -7348,6 +7737,15 @@ export class WebGPURenderer {
|
|
|
7348
7737
|
this.runMedium(encoder);
|
|
7349
7738
|
/* After the depth it reprojects through, and before anything reads the scene. */
|
|
7350
7739
|
this.temporalResolve(encoder);
|
|
7740
|
+
/*
|
|
7741
|
+
* **Reconstruction stands where the temporal resolve stands**, for the same reasons: after
|
|
7742
|
+
* everything that draws into the scene and after the depth it reprojects through, and before
|
|
7743
|
+
* bloom and the composite read the picture. It cannot copy its result back over the scene the
|
|
7744
|
+
* way the temporal resolve does — the two are different sizes — so the composite is given a
|
|
7745
|
+
* bind group of its own instead, which is the second group `temporalResolve` declined.
|
|
7746
|
+
*/
|
|
7747
|
+
this.runMotion(encoder);
|
|
7748
|
+
this.runReconstruction(encoder);
|
|
7351
7749
|
const aoStrength = this.quality.ambientOcclusion;
|
|
7352
7750
|
if (aoStrength > 0 && this.frameProjection !== null)
|
|
7353
7751
|
this.runOcclusion(encoder);
|
|
@@ -7401,9 +7799,10 @@ export class WebGPURenderer {
|
|
|
7401
7799
|
],
|
|
7402
7800
|
});
|
|
7403
7801
|
pass.setPipeline(postPipeline(this.pipelines, device, this.rushLayout, 'post.rush', RUSH_FRAG_WGSL, this.surface.format));
|
|
7404
|
-
|
|
7802
|
+
const rush = this.reconstructing ? this.reconRushGroup : this.rushBindGroup;
|
|
7803
|
+
if (rush === null)
|
|
7405
7804
|
return;
|
|
7406
|
-
pass.setBindGroup(0,
|
|
7805
|
+
pass.setBindGroup(0, rush, [0]);
|
|
7407
7806
|
pass.draw(3);
|
|
7408
7807
|
pass.end();
|
|
7409
7808
|
}
|
|
@@ -7425,6 +7824,15 @@ export class WebGPURenderer {
|
|
|
7425
7824
|
if (this.aoTargetView === null)
|
|
7426
7825
|
return;
|
|
7427
7826
|
this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
|
|
7827
|
+
/*
|
|
7828
|
+
* **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
|
|
7829
|
+
* the lookup table and a resize replaces the depth and the occlusion target, and both groups
|
|
7830
|
+
* hold all three — so a second place that rebuilt only one of them is how the composite would
|
|
7831
|
+
* come to read a stale view on exactly the frames a consumer changed something.
|
|
7832
|
+
*/
|
|
7833
|
+
if (this.reconShownView !== null) {
|
|
7834
|
+
this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
|
|
7835
|
+
}
|
|
7428
7836
|
}
|
|
7429
7837
|
/** The composite's group over one scene source. See `rebuildRushBindGroup`. */
|
|
7430
7838
|
buildRushBindGroup(label, sceneView) {
|
|
@@ -7473,6 +7881,479 @@ export class WebGPURenderer {
|
|
|
7473
7881
|
return field.offset / 4;
|
|
7474
7882
|
}
|
|
7475
7883
|
/** Sample zero of the depth, into something the generated shaders can sample. */
|
|
7884
|
+
/**
|
|
7885
|
+
* Allocate what a reconstruction needs, and release it when nobody is asking.
|
|
7886
|
+
*
|
|
7887
|
+
* Sized by a key rather than by comparing four numbers, because three of the six targets are the
|
|
7888
|
+
* output's size and three are the render's, and a comparison that checked one pair would rebuild
|
|
7889
|
+
* half of them on a resize and keep the other half at the old size — which the device accepts,
|
|
7890
|
+
* a bind group holding views of two different sizes being perfectly legal, and which draws a
|
|
7891
|
+
* picture with a piece of it at the wrong scale.
|
|
7892
|
+
*/
|
|
7893
|
+
ensureReconstruction(renderWidth, renderHeight, outputWidth, outputHeight) {
|
|
7894
|
+
const wanted = this.reconstructionWanted;
|
|
7895
|
+
const key = wanted ? `${renderWidth}x${renderHeight}:${outputWidth}x${outputHeight}` : '';
|
|
7896
|
+
if (key === this.reconSizeKey)
|
|
7897
|
+
return;
|
|
7898
|
+
this.reconSizeKey = key;
|
|
7899
|
+
for (const dead of [
|
|
7900
|
+
...this.reconHistories,
|
|
7901
|
+
this.reconShown,
|
|
7902
|
+
this.reconMotion,
|
|
7903
|
+
this.reconPreviousDepth,
|
|
7904
|
+
]) {
|
|
7905
|
+
dead?.destroy();
|
|
7906
|
+
}
|
|
7907
|
+
this.reconHistories = [null, null];
|
|
7908
|
+
this.reconHistoryViews = [null, null];
|
|
7909
|
+
this.reconShown = null;
|
|
7910
|
+
this.reconShownView = null;
|
|
7911
|
+
this.reconMotion = null;
|
|
7912
|
+
this.reconMotionView = null;
|
|
7913
|
+
this.reconPreviousDepth = null;
|
|
7914
|
+
this.reconPreviousDepthView = null;
|
|
7915
|
+
this.reconGroups = [null, null];
|
|
7916
|
+
this.reconSharpenGroups = [null, null];
|
|
7917
|
+
this.reconRushGroup = null;
|
|
7918
|
+
/* A history of a size that no longer exists is not a history of this frame. */
|
|
7919
|
+
this.reconHasHistory = false;
|
|
7920
|
+
if (!wanted)
|
|
7921
|
+
return;
|
|
7922
|
+
const { device } = this.surface;
|
|
7923
|
+
const HISTORY_USAGE = 0x4 | 0x8; // TEXTURE_BINDING | STORAGE_BINDING
|
|
7924
|
+
for (let i = 0; i < 2; i += 1) {
|
|
7925
|
+
const texture = device.createTexture({
|
|
7926
|
+
label: `recon.history${String(i)}`,
|
|
7927
|
+
size: [outputWidth, outputHeight],
|
|
7928
|
+
format: RECON_HISTORY_FORMAT,
|
|
7929
|
+
usage: HISTORY_USAGE,
|
|
7930
|
+
});
|
|
7931
|
+
this.reconHistories[i] = texture;
|
|
7932
|
+
this.reconHistoryViews[i] = texture.createView();
|
|
7933
|
+
}
|
|
7934
|
+
this.reconShown = device.createTexture({
|
|
7935
|
+
label: 'recon.shown',
|
|
7936
|
+
size: [outputWidth, outputHeight],
|
|
7937
|
+
format: RECON_HISTORY_FORMAT,
|
|
7938
|
+
usage: HISTORY_USAGE,
|
|
7939
|
+
});
|
|
7940
|
+
this.reconShownView = this.reconShown.createView();
|
|
7941
|
+
this.reconMotion = device.createTexture({
|
|
7942
|
+
label: 'recon.motion',
|
|
7943
|
+
size: [renderWidth, renderHeight],
|
|
7944
|
+
format: RECON_HISTORY_FORMAT,
|
|
7945
|
+
/* An attachment as well, because the pass that will write it is a draw. Nothing writes it
|
|
7946
|
+
today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
|
|
7947
|
+
a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
|
|
7948
|
+
usage: 0x4 | 0x10,
|
|
7949
|
+
});
|
|
7950
|
+
this.reconMotionView = this.reconMotion.createView();
|
|
7951
|
+
this.reconPreviousDepth = device.createTexture({
|
|
7952
|
+
label: 'recon.previousDepth',
|
|
7953
|
+
size: [renderWidth, renderHeight],
|
|
7954
|
+
format: RESOLVED_DEPTH_FORMAT,
|
|
7955
|
+
usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
|
|
7956
|
+
});
|
|
7957
|
+
this.reconPreviousDepthView = this.reconPreviousDepth.createView();
|
|
7958
|
+
this.reconParams ??= device.createBuffer({
|
|
7959
|
+
label: 'recon.params',
|
|
7960
|
+
size: this.reconStaging.byteLength,
|
|
7961
|
+
usage: 0x40 | 0x8, // UNIFORM | COPY_DST
|
|
7962
|
+
});
|
|
7963
|
+
this.buildReconPipelines();
|
|
7964
|
+
this.rebuildReconGroups();
|
|
7965
|
+
}
|
|
7966
|
+
/**
|
|
7967
|
+
* The two dispatches, over one explicit layout.
|
|
7968
|
+
*
|
|
7969
|
+
* **Explicit and not `layout: 'auto'`**, which §3 row 51 is about: an inferred layout names only
|
|
7970
|
+
* the bindings its own entry point reads, and these two read different subsets of one module —
|
|
7971
|
+
* the sharpen touches nothing but the history and the target. Two inferred layouts cannot share
|
|
7972
|
+
* a bind group, and building two groups over the same resources is two things to keep in step.
|
|
7973
|
+
*/
|
|
7974
|
+
buildReconPipelines() {
|
|
7975
|
+
if (this.reconLayout !== null)
|
|
7976
|
+
return;
|
|
7977
|
+
const { device } = this.surface;
|
|
7978
|
+
const COMPUTE = 0x4; // GPUShaderStage.COMPUTE
|
|
7979
|
+
const sampled = (binding, sampleType) => ({
|
|
7980
|
+
binding,
|
|
7981
|
+
visibility: COMPUTE,
|
|
7982
|
+
texture: { sampleType, viewDimension: '2d' },
|
|
7983
|
+
});
|
|
7984
|
+
this.reconLayout = device.createBindGroupLayout({
|
|
7985
|
+
label: 'recon.layout',
|
|
7986
|
+
entries: [
|
|
7987
|
+
sampled(0, 'float'),
|
|
7988
|
+
/* r32float is not filterable, and the resolve only ever loads a depth texel. */
|
|
7989
|
+
sampled(1, 'unfilterable-float'),
|
|
7990
|
+
sampled(2, 'float'),
|
|
7991
|
+
sampled(3, 'unfilterable-float'),
|
|
7992
|
+
sampled(4, 'float'),
|
|
7993
|
+
{ binding: 5, visibility: COMPUTE, sampler: { type: 'filtering' } },
|
|
7994
|
+
{ binding: 6, visibility: COMPUTE, buffer: { type: 'uniform' } },
|
|
7995
|
+
{
|
|
7996
|
+
binding: 7,
|
|
7997
|
+
visibility: COMPUTE,
|
|
7998
|
+
storageTexture: {
|
|
7999
|
+
access: 'write-only',
|
|
8000
|
+
format: RECON_HISTORY_FORMAT,
|
|
8001
|
+
viewDimension: '2d',
|
|
8002
|
+
},
|
|
8003
|
+
},
|
|
8004
|
+
],
|
|
8005
|
+
});
|
|
8006
|
+
const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
|
|
8007
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts: [this.reconLayout] });
|
|
8008
|
+
this.reconResolvePipeline = device.createComputePipeline({
|
|
8009
|
+
label: 'recon.resolve',
|
|
8010
|
+
layout,
|
|
8011
|
+
compute: { module, entryPoint: 'resolveMain' },
|
|
8012
|
+
});
|
|
8013
|
+
this.reconSharpenPipeline = device.createComputePipeline({
|
|
8014
|
+
label: 'recon.sharpen',
|
|
8015
|
+
layout,
|
|
8016
|
+
compute: { module, entryPoint: 'sharpenMain' },
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
/**
|
|
8020
|
+
* The four groups, one a direction of the ping-pong and one a dispatch.
|
|
8021
|
+
*
|
|
8022
|
+
* The resolve reads history `1 - write` and writes history `write`; the sharpen reads history
|
|
8023
|
+
* `write` and writes the shown picture. A group holds the views it was built with, so this is
|
|
8024
|
+
* called whenever any of them is replaced.
|
|
8025
|
+
*/
|
|
8026
|
+
rebuildReconGroups() {
|
|
8027
|
+
const layout = this.reconLayout;
|
|
8028
|
+
const params = this.reconParams;
|
|
8029
|
+
if (layout === null || params === null)
|
|
8030
|
+
return;
|
|
8031
|
+
if (this.sceneColorView === null || this.resolvedDepthView === null)
|
|
8032
|
+
return;
|
|
8033
|
+
if (this.reconMotionView === null || this.reconPreviousDepthView === null)
|
|
8034
|
+
return;
|
|
8035
|
+
if (this.reconShownView === null)
|
|
8036
|
+
return;
|
|
8037
|
+
const { device } = this.surface;
|
|
8038
|
+
const build = (label, history, target) => device.createBindGroup({
|
|
8039
|
+
label,
|
|
8040
|
+
layout,
|
|
8041
|
+
entries: [
|
|
8042
|
+
{ binding: 0, resource: this.sceneColorView },
|
|
8043
|
+
{ binding: 1, resource: this.resolvedDepthView },
|
|
8044
|
+
{ binding: 2, resource: this.reconMotionView },
|
|
8045
|
+
{ binding: 3, resource: this.reconPreviousDepthView },
|
|
8046
|
+
{ binding: 4, resource: history },
|
|
8047
|
+
{ binding: 5, resource: this.postSampler },
|
|
8048
|
+
{ binding: 6, resource: { buffer: params } },
|
|
8049
|
+
{ binding: 7, resource: target },
|
|
8050
|
+
],
|
|
8051
|
+
});
|
|
8052
|
+
for (let write = 0; write < 2; write += 1) {
|
|
8053
|
+
const read = this.reconHistoryViews[1 - write];
|
|
8054
|
+
const target = this.reconHistoryViews[write];
|
|
8055
|
+
if (read === null || target === null)
|
|
8056
|
+
continue;
|
|
8057
|
+
this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
|
|
8058
|
+
this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView);
|
|
8059
|
+
}
|
|
8060
|
+
/* The composite's own group, over the resolved picture instead of the scene target. */
|
|
8061
|
+
this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
|
|
8062
|
+
/* And the scene's, because `ensureComposite` built it before this one existed. */
|
|
8063
|
+
this.rebuildRushBindGroup();
|
|
8064
|
+
}
|
|
8065
|
+
/**
|
|
8066
|
+
* Remember that this mesh moved, so the motion pass can draw where it was.
|
|
8067
|
+
*
|
|
8068
|
+
* Silent unless a reconstruction is running: a scene that always states its previous transforms
|
|
8069
|
+
* — which is the right thing for a scene to do, the alternative being to know what the renderer
|
|
8070
|
+
* is configured as — must cost nothing at all when nobody is going to read them.
|
|
8071
|
+
*/
|
|
8072
|
+
recordMotion(mesh, model, previousModel) {
|
|
8073
|
+
if (!this.reconstructing)
|
|
8074
|
+
return;
|
|
8075
|
+
const at = this.motionCount;
|
|
8076
|
+
const wanted = (at + 1) * MOTION_DRAW_FLOATS;
|
|
8077
|
+
if (this.motionMatrices.length < wanted) {
|
|
8078
|
+
/* Doubling, so a scene settles on one allocation rather than one a draw. */
|
|
8079
|
+
const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
|
|
8080
|
+
grown.set(this.motionMatrices);
|
|
8081
|
+
this.motionMatrices = grown;
|
|
8082
|
+
}
|
|
8083
|
+
this.motionMatrices.set(model, at * MOTION_DRAW_FLOATS);
|
|
8084
|
+
this.motionMatrices.set(previousModel, at * MOTION_DRAW_FLOATS + 16);
|
|
8085
|
+
this.motionMeshes[at] = mesh;
|
|
8086
|
+
this.motionCount = at + 1;
|
|
8087
|
+
}
|
|
8088
|
+
/**
|
|
8089
|
+
* The pipeline the motion pass draws with, built once.
|
|
8090
|
+
*
|
|
8091
|
+
* **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
|
|
8092
|
+
* an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
|
|
8093
|
+
*/
|
|
8094
|
+
buildMotionPipeline() {
|
|
8095
|
+
if (this.motionLayout !== null)
|
|
8096
|
+
return;
|
|
8097
|
+
const { device } = this.surface;
|
|
8098
|
+
this.motionLayout = device.createBindGroupLayout({
|
|
8099
|
+
label: 'recon.motion.layout',
|
|
8100
|
+
entries: [
|
|
8101
|
+
{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } },
|
|
8102
|
+
{
|
|
8103
|
+
binding: 1,
|
|
8104
|
+
visibility: 0x1,
|
|
8105
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
8106
|
+
},
|
|
8107
|
+
],
|
|
8108
|
+
});
|
|
8109
|
+
this.motionModule = device.createShaderModule({
|
|
8110
|
+
label: 'recon.motion',
|
|
8111
|
+
code: RECON_MOTION_WGSL,
|
|
8112
|
+
});
|
|
8113
|
+
this.motionPipelineLayout = device.createPipelineLayout({
|
|
8114
|
+
bindGroupLayouts: [this.motionLayout],
|
|
8115
|
+
});
|
|
8116
|
+
}
|
|
8117
|
+
/** The pipeline for one vertex stride, built on first sight of it. */
|
|
8118
|
+
motionPipelineFor(stride) {
|
|
8119
|
+
const held = this.motionPipelines.get(stride);
|
|
8120
|
+
if (held !== undefined)
|
|
8121
|
+
return held;
|
|
8122
|
+
const module = this.motionModule;
|
|
8123
|
+
const layout = this.motionPipelineLayout;
|
|
8124
|
+
if (module === null || layout === null)
|
|
8125
|
+
return null;
|
|
8126
|
+
const { device } = this.surface;
|
|
8127
|
+
const pipeline = device.createRenderPipeline({
|
|
8128
|
+
label: `recon.motion.${String(stride)}`,
|
|
8129
|
+
layout,
|
|
8130
|
+
vertex: {
|
|
8131
|
+
module,
|
|
8132
|
+
entryPoint: 'motionVert',
|
|
8133
|
+
/* Positions are the layout's first attribute and are never optional, so they are at offset
|
|
8134
|
+
zero of every stride this can be handed. */
|
|
8135
|
+
buffers: [
|
|
8136
|
+
{
|
|
8137
|
+
arrayStride: stride,
|
|
8138
|
+
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
|
|
8139
|
+
},
|
|
8140
|
+
],
|
|
8141
|
+
},
|
|
8142
|
+
fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
|
|
8143
|
+
/*
|
|
8144
|
+
* **The scene's own winding**, because this draws the scene's geometry through the scene's
|
|
8145
|
+
* clip transform — the product the renderer hands over already carries the negation that
|
|
8146
|
+
* `naga` writes into every generated vertex stage, so the faces come out the way they did
|
|
8147
|
+
* and a `cw` front face here would draw the inside of every mesh. §3 row 57.
|
|
8148
|
+
*/
|
|
8149
|
+
primitive: { topology: 'triangle-list', cullMode: 'back' },
|
|
8150
|
+
depthStencil: {
|
|
8151
|
+
format: DEPTH_FORMAT,
|
|
8152
|
+
/*
|
|
8153
|
+
* **Tested and not written.** The frame's depth is finished; this pass only wants the
|
|
8154
|
+
* pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
|
|
8155
|
+
* exactly them — the same transform on the same positions gives the same depth, which is
|
|
8156
|
+
* why the rasterisation matrix is handed over whole rather than rebuilt here.
|
|
8157
|
+
*
|
|
8158
|
+
* **And the equality is not fragile here**, which was worth measuring rather than
|
|
8159
|
+
* assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
|
|
8160
|
+
* contract a matrix product differently, so an equality across them could have admitted
|
|
8161
|
+
* only some of a surface. Run with `greater-equal` instead, the ghost measured the same
|
|
8162
|
+
* to the pixel — 5,942 either way — so nothing is being dropped.
|
|
8163
|
+
*/
|
|
8164
|
+
depthWriteEnabled: false,
|
|
8165
|
+
depthCompare: DEPTH_COMPARE_EQUAL,
|
|
8166
|
+
},
|
|
8167
|
+
});
|
|
8168
|
+
this.motionPipelines.set(stride, pipeline);
|
|
8169
|
+
return pipeline;
|
|
8170
|
+
}
|
|
8171
|
+
/**
|
|
8172
|
+
* Where every mover was last frame, into the motion target.
|
|
8173
|
+
*
|
|
8174
|
+
* **After everything that draws and before the resolve reads it.** The frame's depth has to be
|
|
8175
|
+
* finished for the equality test to mean anything, and the resolve is what consumes the result.
|
|
8176
|
+
*/
|
|
8177
|
+
runMotion(encoder) {
|
|
8178
|
+
const target = this.reconMotionView;
|
|
8179
|
+
const depth = this.depthView;
|
|
8180
|
+
if (!this.reconstructing || target === null || depth === null)
|
|
8181
|
+
return;
|
|
8182
|
+
const { device } = this.surface;
|
|
8183
|
+
this.buildMotionPipeline();
|
|
8184
|
+
const layout = this.motionLayout;
|
|
8185
|
+
if (layout === null)
|
|
8186
|
+
return;
|
|
8187
|
+
/*
|
|
8188
|
+
* **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
|
|
8189
|
+
* saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
|
|
8190
|
+
* is no longer there. The clear is the pass's own load operation, so an empty queue still runs
|
|
8191
|
+
* it — which is why this is not returned from before the pass is opened.
|
|
8192
|
+
*/
|
|
8193
|
+
const pass = encoder.beginRenderPass({
|
|
8194
|
+
label: 'recon.motion',
|
|
8195
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
8196
|
+
colorAttachments: [
|
|
8197
|
+
{ view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
|
|
8198
|
+
],
|
|
8199
|
+
depthStencilAttachment: {
|
|
8200
|
+
view: depth,
|
|
8201
|
+
depthLoadOp: 'load',
|
|
8202
|
+
depthStoreOp: 'store',
|
|
8203
|
+
depthReadOnly: false,
|
|
8204
|
+
},
|
|
8205
|
+
});
|
|
8206
|
+
const count = this.motionCount;
|
|
8207
|
+
if (count > 0) {
|
|
8208
|
+
const bytes = count * MOTION_DRAW_STRIDE;
|
|
8209
|
+
if (this.motionDrawCapacity < count) {
|
|
8210
|
+
this.motionDrawBuffer?.destroy();
|
|
8211
|
+
this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
|
|
8212
|
+
this.motionDrawBuffer = device.createBuffer({
|
|
8213
|
+
label: 'recon.motion.draws',
|
|
8214
|
+
size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
|
|
8215
|
+
usage: 0x40 | 0x8, // UNIFORM | COPY_DST
|
|
8216
|
+
});
|
|
8217
|
+
this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
|
|
8218
|
+
this.motionGroup = null;
|
|
8219
|
+
}
|
|
8220
|
+
this.motionFrameBuffer ??= device.createBuffer({
|
|
8221
|
+
label: 'recon.motion.frame',
|
|
8222
|
+
size: MOTION_FRAME_FLOATS * 4,
|
|
8223
|
+
usage: 0x40 | 0x8,
|
|
8224
|
+
});
|
|
8225
|
+
this.motionGroup ??= device.createBindGroup({
|
|
8226
|
+
label: 'recon.motion.group',
|
|
8227
|
+
layout,
|
|
8228
|
+
entries: [
|
|
8229
|
+
{ binding: 0, resource: { buffer: this.motionFrameBuffer } },
|
|
8230
|
+
{
|
|
8231
|
+
binding: 1,
|
|
8232
|
+
resource: { buffer: this.motionDrawBuffer, size: MOTION_DRAW_STRIDE },
|
|
8233
|
+
},
|
|
8234
|
+
],
|
|
8235
|
+
});
|
|
8236
|
+
/* The scene's clip transform, with the generated stages' y negation folded in. */
|
|
8237
|
+
mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
|
|
8238
|
+
this.motionFrameStaging.set(this.motionRaster, 0);
|
|
8239
|
+
this.motionFrameStaging.set(this.frameRawViewProj, 16);
|
|
8240
|
+
this.motionFrameStaging.set(this.reconPreviousRawViewProj, 32);
|
|
8241
|
+
device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
|
|
8242
|
+
const draws = new Float32Array(this.motionDrawStaging.buffer);
|
|
8243
|
+
for (let i = 0; i < count; i += 1) {
|
|
8244
|
+
draws.set(this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS), (i * MOTION_DRAW_STRIDE) / 4);
|
|
8245
|
+
}
|
|
8246
|
+
device.queue.writeBuffer(this.motionDrawBuffer, 0, this.motionDrawStaging, 0, bytes);
|
|
8247
|
+
let bound = null;
|
|
8248
|
+
for (let i = 0; i < count; i += 1) {
|
|
8249
|
+
const mesh = this.motionMeshes[i];
|
|
8250
|
+
if (mesh === undefined)
|
|
8251
|
+
continue;
|
|
8252
|
+
const pipeline = this.motionPipelineFor(mesh.vertexStride);
|
|
8253
|
+
if (pipeline === null)
|
|
8254
|
+
continue;
|
|
8255
|
+
if (pipeline !== bound) {
|
|
8256
|
+
pass.setPipeline(pipeline);
|
|
8257
|
+
bound = pipeline;
|
|
8258
|
+
}
|
|
8259
|
+
pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
|
|
8260
|
+
pass.setVertexBuffer(0, mesh.vertexBuffers[0]);
|
|
8261
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
8262
|
+
pass.drawIndexed(mesh.indexCount);
|
|
8263
|
+
}
|
|
8264
|
+
}
|
|
8265
|
+
pass.end();
|
|
8266
|
+
}
|
|
8267
|
+
/**
|
|
8268
|
+
* DriftTR tier 0: the jittered render at the render size into the picture at the output size.
|
|
8269
|
+
*
|
|
8270
|
+
* `recon/resolve.ts` is the reference this is held to and `scripts/recon-parity.mjs` compares the
|
|
8271
|
+
* two; nothing here decides anything the reference does not.
|
|
8272
|
+
*/
|
|
8273
|
+
runReconstruction(encoder) {
|
|
8274
|
+
if (!this.reconstructing)
|
|
8275
|
+
return;
|
|
8276
|
+
const resolve = this.reconResolvePipeline;
|
|
8277
|
+
const sharpen = this.reconSharpenPipeline;
|
|
8278
|
+
const params = this.reconParams;
|
|
8279
|
+
const write = this.reconWrite;
|
|
8280
|
+
const group = this.reconGroups[write];
|
|
8281
|
+
const sharpenGroup = this.reconSharpenGroups[write];
|
|
8282
|
+
if (resolve === null || sharpen === null || params === null)
|
|
8283
|
+
return;
|
|
8284
|
+
if (group === null || sharpenGroup === null)
|
|
8285
|
+
return;
|
|
8286
|
+
const { device } = this.surface;
|
|
8287
|
+
const outputWidth = Math.max(1, this.surface.canvas.width);
|
|
8288
|
+
const outputHeight = Math.max(1, this.surface.canvas.height);
|
|
8289
|
+
/*
|
|
8290
|
+
* **The unjittered matrices**, which is what the reference's own header insists on: the
|
|
8291
|
+
* history stands for the scene as it would have been drawn with no jitter, and the motion the
|
|
8292
|
+
* resolve derives carries none. `frameRawViewProj` is the camera's own view-projection, in
|
|
8293
|
+
* OpenGL's depth convention — which is the convention the shader's accessor converts the
|
|
8294
|
+
* renderer's reversed buffer into.
|
|
8295
|
+
*/
|
|
8296
|
+
mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
|
|
8297
|
+
const f = this.reconFloats;
|
|
8298
|
+
const u = this.reconInts;
|
|
8299
|
+
u[0] = this.renderWidth;
|
|
8300
|
+
u[1] = this.renderHeight;
|
|
8301
|
+
u[2] = outputWidth;
|
|
8302
|
+
u[3] = outputHeight;
|
|
8303
|
+
f[4] = this.reconJitter[0];
|
|
8304
|
+
f[5] = this.reconJitter[1];
|
|
8305
|
+
f[6] = this.reconPreviousJitter[0];
|
|
8306
|
+
f[7] = this.reconPreviousJitter[1];
|
|
8307
|
+
f.set(this.reconInverseViewProj, 8);
|
|
8308
|
+
f.set(this.reconPreviousRawViewProj, 24);
|
|
8309
|
+
f.set(this.reconPreviousInverseViewProj, 40);
|
|
8310
|
+
f.set(this.reconEye, 56);
|
|
8311
|
+
f.set(this.reconPreviousEye, 60);
|
|
8312
|
+
f[64] = DEFAULT_RECON_QUALITY.alpha;
|
|
8313
|
+
f[65] = DEFAULT_RECON_QUALITY.sharpen;
|
|
8314
|
+
f[66] = DEFAULT_RECON_QUALITY.clampGamma;
|
|
8315
|
+
u[67] = this.reconHasHistory ? 1 : 0;
|
|
8316
|
+
f[68] = DEFAULT_DISOCCLUSION.depthTolerance;
|
|
8317
|
+
f[69] = DEFAULT_DISOCCLUSION.motionScale;
|
|
8318
|
+
f[70] = DEFAULT_DISOCCLUSION.normalFloor;
|
|
8319
|
+
f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
|
|
8320
|
+
device.queue.writeBuffer(params, 0, this.reconStaging);
|
|
8321
|
+
const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
|
|
8322
|
+
const groupsY = Math.ceil(outputHeight / RECON_WORKGROUP);
|
|
8323
|
+
const pass = encoder.beginComputePass({
|
|
8324
|
+
label: 'recon.resolve',
|
|
8325
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
8326
|
+
});
|
|
8327
|
+
pass.setPipeline(resolve);
|
|
8328
|
+
pass.setBindGroup(0, group);
|
|
8329
|
+
pass.dispatchWorkgroups(groupsX, groupsY, 1);
|
|
8330
|
+
/*
|
|
8331
|
+
* **The sharpen in the same pass, and that is legal because it reads a different texture.**
|
|
8332
|
+
* The resolve wrote history `write`; the sharpen reads it and writes the shown picture. Two
|
|
8333
|
+
* dispatches in one compute pass see each other's writes in recording order, and the rule
|
|
8334
|
+
* §3 row 52 is about — one buffer writable and readable in a single synchronisation scope —
|
|
8335
|
+
* does not bite, because no resource here is both.
|
|
8336
|
+
*/
|
|
8337
|
+
pass.setPipeline(sharpen);
|
|
8338
|
+
pass.setBindGroup(0, sharpenGroup);
|
|
8339
|
+
pass.dispatchWorkgroups(groupsX, groupsY, 1);
|
|
8340
|
+
pass.end();
|
|
8341
|
+
/* Last frame's depth, for the disocclusion test and the normals derived from it. */
|
|
8342
|
+
const depth = this.resolvedDepth;
|
|
8343
|
+
const previous = this.reconPreviousDepth;
|
|
8344
|
+
if (depth !== null && previous !== null) {
|
|
8345
|
+
encoder.copyTextureToTexture({ texture: depth }, { texture: previous }, [
|
|
8346
|
+
previous.width,
|
|
8347
|
+
previous.height,
|
|
8348
|
+
1,
|
|
8349
|
+
]);
|
|
8350
|
+
}
|
|
8351
|
+
this.reconWrite = 1 - write;
|
|
8352
|
+
this.reconHasHistory = true;
|
|
8353
|
+
mat4.copy(this.reconPreviousRawViewProj, this.frameRawViewProj);
|
|
8354
|
+
mat4.copy(this.reconPreviousInverseViewProj, this.reconInverseViewProj);
|
|
8355
|
+
this.reconPreviousEye.set(this.reconEye);
|
|
8356
|
+
}
|
|
7476
8357
|
resolveDepth(encoder) {
|
|
7477
8358
|
const target = this.resolvedDepthView;
|
|
7478
8359
|
const source = this.depthView;
|
|
@@ -7510,6 +8391,12 @@ export class WebGPURenderer {
|
|
|
7510
8391
|
* must still write, or the history it leaves for the next frame is whatever was in the texture.
|
|
7511
8392
|
*/
|
|
7512
8393
|
temporalResolve(encoder) {
|
|
8394
|
+
/*
|
|
8395
|
+
* **One guard, not two.** A second test on `reconstructing` here read as belt and braces and
|
|
8396
|
+
* was worse than nothing: it and the `!reconstructing` in `temporalJittering`'s own condition
|
|
8397
|
+
* each covered the other, so breaking either one changed no behaviour and no test could see
|
|
8398
|
+
* it. `temporalJittering` is where the choice is made, and it is made once.
|
|
8399
|
+
*/
|
|
7513
8400
|
if (!this.temporalJittering)
|
|
7514
8401
|
return;
|
|
7515
8402
|
const write = this.taaWrite;
|
|
@@ -7521,8 +8408,8 @@ export class WebGPURenderer {
|
|
|
7521
8408
|
const f = this.taaFloats;
|
|
7522
8409
|
const at = (name) => this.postField(TAA_FRAG_FIELDS, name);
|
|
7523
8410
|
f.set(this.reprojection, at('uReprojection'));
|
|
7524
|
-
f[at('uTexel')] = 1 /
|
|
7525
|
-
f[at('uTexel') + 1] = 1 /
|
|
8411
|
+
f[at('uTexel')] = 1 / this.renderWidth;
|
|
8412
|
+
f[at('uTexel') + 1] = 1 / this.renderHeight;
|
|
7526
8413
|
f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
|
|
7527
8414
|
device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
|
|
7528
8415
|
const pass = encoder.beginRenderPass({
|
|
@@ -7592,8 +8479,9 @@ export class WebGPURenderer {
|
|
|
7592
8479
|
if (!this.frameCameraSeen)
|
|
7593
8480
|
return;
|
|
7594
8481
|
const { device } = this.surface;
|
|
7595
|
-
|
|
7596
|
-
const
|
|
8482
|
+
/* The scene's size: this marches through the picture's own texels, not the viewer's. */
|
|
8483
|
+
const width = this.renderWidth;
|
|
8484
|
+
const height = this.renderHeight;
|
|
7597
8485
|
const view = this.frameRawViewProj;
|
|
7598
8486
|
mat4.invert(this.ssrDepthToWorld, view);
|
|
7599
8487
|
mat4.multiply(this.ssrDepthToWorld, this.ssrDepthToWorld, DEPTH_01_TO_CLIP_Y_DOWN);
|
|
@@ -7674,8 +8562,9 @@ export class WebGPURenderer {
|
|
|
7674
8562
|
if (!this.frameCameraSeen)
|
|
7675
8563
|
return;
|
|
7676
8564
|
const { device } = this.surface;
|
|
7677
|
-
|
|
7678
|
-
const
|
|
8565
|
+
/* The scene's size: a mark is projected onto the depth this frame drew, at that depth's size. */
|
|
8566
|
+
const width = this.renderWidth;
|
|
8567
|
+
const height = this.renderHeight;
|
|
7679
8568
|
const view = this.frameRawViewProj;
|
|
7680
8569
|
mat4.invert(this.decalDepthToWorld, view);
|
|
7681
8570
|
mat4.multiply(this.decalDepthToWorld, this.decalDepthToWorld, DEPTH_01_TO_CLIP_Y_DOWN);
|
|
@@ -7744,6 +8633,23 @@ export class WebGPURenderer {
|
|
|
7744
8633
|
}
|
|
7745
8634
|
const { device } = this.surface;
|
|
7746
8635
|
const options = this.oitReplayOptions;
|
|
8636
|
+
/*
|
|
8637
|
+
* **A refracting pane reads the frame behind it, and here that frame is finished.** So the
|
|
8638
|
+
* copy it reads is taken now, from the resolved scene, once for the whole set — the immediate
|
|
8639
|
+
* path takes it by ending the scene's pass, and that pass has already ended. Only when
|
|
8640
|
+
* something in the set refracts, because a copy of the frame is not free.
|
|
8641
|
+
*/
|
|
8642
|
+
if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
|
|
8643
|
+
const snapshot = this.refractSnapshot;
|
|
8644
|
+
const source = this.sceneColor;
|
|
8645
|
+
if (snapshot !== null && source !== null) {
|
|
8646
|
+
encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
|
|
8647
|
+
snapshot.width,
|
|
8648
|
+
snapshot.height,
|
|
8649
|
+
]);
|
|
8650
|
+
this.refractSnapshotTaken = true;
|
|
8651
|
+
}
|
|
8652
|
+
}
|
|
7747
8653
|
const buffers = [
|
|
7748
8654
|
{ mode: 'accum', view: accum, clear: [0, 0, 0, 0] },
|
|
7749
8655
|
/* Cleared to one: nothing has covered this pixel yet. Cleared to zero the resolve would show
|
|
@@ -7773,6 +8679,12 @@ export class WebGPURenderer {
|
|
|
7773
8679
|
options.depthWrite = false;
|
|
7774
8680
|
options.depthLayer = draw.depthLayer;
|
|
7775
8681
|
options.tint = draw.tint;
|
|
8682
|
+
/* Replayed too, as the other backend replays it: a pane that refracts under sorted
|
|
8683
|
+
blending and stands clear under this effect reads as a bug in the effect. It did stand
|
|
8684
|
+
clear here until 2026-09-19. */
|
|
8685
|
+
options.refraction = draw.refraction;
|
|
8686
|
+
options.refractTint = draw.refractTint;
|
|
8687
|
+
options.thicknessM = draw.thicknessM;
|
|
7776
8688
|
this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
|
|
7777
8689
|
});
|
|
7778
8690
|
this.oitReplaying = false;
|
|
@@ -8137,6 +9049,10 @@ export class WebGPURenderer {
|
|
|
8137
9049
|
return false;
|
|
8138
9050
|
if (this.refractSnapshotTaken)
|
|
8139
9051
|
return true;
|
|
9052
|
+
/* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
|
|
9053
|
+
took the copy for it before it began; there is no pass here to end and reopen. */
|
|
9054
|
+
if (this.oitReplaying)
|
|
9055
|
+
return false;
|
|
8140
9056
|
const snapshot = this.refractSnapshot;
|
|
8141
9057
|
const source = this.sceneColor;
|
|
8142
9058
|
const encoder = this.encoder;
|
|
@@ -8297,7 +9213,15 @@ export class WebGPURenderer {
|
|
|
8297
9213
|
* recorded so far writes the scene target, so this must come out as exactly one pass;
|
|
8298
9214
|
* anything else means a node declared a write it does not perform.
|
|
8299
9215
|
*/
|
|
8300
|
-
|
|
9216
|
+
/*
|
|
9217
|
+
* **Either scheduler, into the same records**, so nothing below knows which ran. The identifier
|
|
9218
|
+
* graph is the one that can also express the second pipeline's frame; running it here is what
|
|
9219
|
+
* shows it is a generalisation of this one rather than a different renderer — see
|
|
9220
|
+
* `quality.identifierGraph` for the captures that say so.
|
|
9221
|
+
*/
|
|
9222
|
+
const passes = this.flushSchedule === null
|
|
9223
|
+
? schedule(this.arena, this.pendingClearMask, liveOut, this.scheduledPasses)
|
|
9224
|
+
: scheduleFlush(this.arena, this.pendingClearMask, liveOut, this.flushSchedule, this.scheduledPasses);
|
|
8301
9225
|
this.passesLastFlush = passes;
|
|
8302
9226
|
let discards = 0;
|
|
8303
9227
|
let clears = 0;
|
|
@@ -8415,6 +9339,14 @@ export class WebGPURenderer {
|
|
|
8415
9339
|
once and ran against a discarded attachment that read zero everywhere — which is why
|
|
8416
9340
|
there is one of these and why this line is in it. */
|
|
8417
9341
|
(this.quality.temporalAa && this.quality.screenEffects) ||
|
|
9342
|
+
/*
|
|
9343
|
+
* **And so does a reconstruction, for the same reason, and it was missing for the same
|
|
9344
|
+
* reason.** Without it the resolve read a discarded attachment as zero — the far plane
|
|
9345
|
+
* reversed — so every surface stood at infinity: a turning camera reprojected correctly,
|
|
9346
|
+
* since a turn moves every depth alike, and a sliding one moved nothing, and the history
|
|
9347
|
+
* trailed every slide by the parallax it never saw.
|
|
9348
|
+
*/
|
|
9349
|
+
this.reconstructing ||
|
|
8418
9350
|
/*
|
|
8419
9351
|
* **The order-independent passes attach the depth and test against it**, so a frame running
|
|
8420
9352
|
* them needs it to survive the main pass. Without this it is discarded, those passes test
|
|
@@ -8775,6 +9707,15 @@ export class WebGPURenderer {
|
|
|
8775
9707
|
this.flushRings();
|
|
8776
9708
|
framePass.end();
|
|
8777
9709
|
this.pass = null;
|
|
9710
|
+
/*
|
|
9711
|
+
* **The one window in the frame where every declaration is in and no render pass is open.**
|
|
9712
|
+
* A consumer declares its fields wherever in its own frame the objects live, so the earliest
|
|
9713
|
+
* the set is complete is after the last verb; and a compute pass cannot be recorded inside a
|
|
9714
|
+
* render pass, so the latest it can be recorded is before the composite opens its own. The
|
|
9715
|
+
* probes traced from it are read by the next frame's shading, which is what a probe volume
|
|
9716
|
+
* does anyway — it accumulates.
|
|
9717
|
+
*/
|
|
9718
|
+
this.runIndirectLight(this.encoder);
|
|
8778
9719
|
if (this.hasComposite)
|
|
8779
9720
|
this.composite(this.encoder);
|
|
8780
9721
|
/* And where there is no composite there is no depth to read, so a frame that submitted marks
|
|
@@ -8830,7 +9771,17 @@ export class WebGPURenderer {
|
|
|
8830
9771
|
const width = Math.max(1, this.budgeted.width);
|
|
8831
9772
|
const height = Math.max(1, this.budgeted.height);
|
|
8832
9773
|
this.surface.configure(width, height);
|
|
8833
|
-
|
|
9774
|
+
/*
|
|
9775
|
+
* **The same split `beginFrame` makes, and it has to be made twice.** This path resized the
|
|
9776
|
+
* composite to the drawing buffer while the frame resized it to the render size, so the two
|
|
9777
|
+
* fought every time a canvas changed — and the reconstruction's bind groups, built here
|
|
9778
|
+
* against the previous scene depth, held a view of a texture this call had just destroyed.
|
|
9779
|
+
* The device's words for that are "destroyed texture used in a submit", on the next frame,
|
|
9780
|
+
* after a resize, which is as far from the cause as a message gets.
|
|
9781
|
+
*/
|
|
9782
|
+
const render = this.sceneSize();
|
|
9783
|
+
this.ensureComposite(render.width, render.height);
|
|
9784
|
+
this.ensureReconstruction(render.width, render.height, width, height);
|
|
8834
9785
|
this.ensureReflection(width, height);
|
|
8835
9786
|
}
|
|
8836
9787
|
/**
|
|
@@ -8897,6 +9848,195 @@ export class WebGPURenderer {
|
|
|
8897
9848
|
addOccluder(min, max, model) {
|
|
8898
9849
|
this.occlusion?.addOccluder(min, max, model);
|
|
8899
9850
|
}
|
|
9851
|
+
/**
|
|
9852
|
+
* Declare a baked distance field that indirect light may be traced against.
|
|
9853
|
+
*
|
|
9854
|
+
* **The seam is `addOccluder`'s, and for `addOccluder`'s reason.** A distance field is the shape
|
|
9855
|
+
* of a thing it is safe to be *traced against*, and only a consumer knows which of its objects
|
|
9856
|
+
* that is true of — the walls and the terrain, not the leaves, not the water, not the thing it
|
|
9857
|
+
* is about to delete. A renderer that derived the field from whatever was drawn would trace
|
|
9858
|
+
* against the foliage and light the room with a hedge.
|
|
9859
|
+
*
|
|
9860
|
+
* **Declared anywhere in the frame and composed at its end**, which is the one window where
|
|
9861
|
+
* every declaration is in and the encoder has no render pass open. Cleared at `beginFrame` with
|
|
9862
|
+
* the decal and reflection queues, so a consumer redeclares every frame and a field it stops
|
|
9863
|
+
* declaring stops lighting.
|
|
9864
|
+
*
|
|
9865
|
+
* The field is in the model matrix's own space and the matrix must carry uniform scale only —
|
|
9866
|
+
* `composeGlobalField` gives the reason: a distance is not preserved by a non-uniform scale, and
|
|
9867
|
+
* the factor to correct by depends on the direction to the nearest surface, which is exactly
|
|
9868
|
+
* what a distance field does not record.
|
|
9869
|
+
*
|
|
9870
|
+
* **`albedo` is what colour this placement's surface is, and it decides what the bounce carries.**
|
|
9871
|
+
* What leaves a surface is the light arriving times its albedo, so a ray that lands on a wall
|
|
9872
|
+
* and does not know its colour returns the same answer for a red wall and a white one — measured
|
|
9873
|
+
* on `demo/dev/bounce.html` before this argument existed. It is a placement's rather than a
|
|
9874
|
+
* field's, because the same baked shape is placed many times and two pillars cut from one mould
|
|
9875
|
+
* may be painted differently. White where it is left out, so every existing caller is unchanged.
|
|
9876
|
+
*/
|
|
9877
|
+
addDistanceField(field, model, albedo) {
|
|
9878
|
+
this.distanceFields.record(field, model, albedo);
|
|
9879
|
+
}
|
|
9880
|
+
/** Forget the fields declared this frame, for a consumer that rebuilds its list mid-frame. */
|
|
9881
|
+
clearDistanceFields() {
|
|
9882
|
+
this.distanceFields.reset();
|
|
9883
|
+
}
|
|
9884
|
+
/**
|
|
9885
|
+
* Compose what the frame declared, or nothing at all.
|
|
9886
|
+
*
|
|
9887
|
+
* **Nothing at all is the ordinary case and must cost nothing.** `quality.indirectLight` is off
|
|
9888
|
+
* by default and a frame that declared no fields has nothing to compose, so both are answered
|
|
9889
|
+
* before the composer exists — which is what keeps the eighteen scenes byte-identical with the
|
|
9890
|
+
* flag off rather than merely close.
|
|
9891
|
+
*/
|
|
9892
|
+
runIndirectLight(encoder) {
|
|
9893
|
+
/*
|
|
9894
|
+
* **The flag is read here and in no other place.** It used to be read again by each of the two
|
|
9895
|
+
* steps, and a perturbation proved neither of those could fail: the bake needs a composed
|
|
9896
|
+
* field, and only the composition makes one. A guard another guard covers is a line no test
|
|
9897
|
+
* can hold — this session has had to remove three of them.
|
|
9898
|
+
*/
|
|
9899
|
+
if (!this.quality.indirectLight)
|
|
9900
|
+
return;
|
|
9901
|
+
this.composeDistanceField(encoder);
|
|
9902
|
+
this.bakeIndirectProbes(encoder);
|
|
9903
|
+
}
|
|
9904
|
+
composeDistanceField(encoder) {
|
|
9905
|
+
/*
|
|
9906
|
+
* **An empty frame still reaches the composer, and a test had to find that.** The early return
|
|
9907
|
+
* used to cover the whole method, so a frame that declared nothing left `composed` true from
|
|
9908
|
+
* the frame before — and the bake traced against a field built out of geometry the consumer
|
|
9909
|
+
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
9910
|
+
* profile that never declares anything, so that is all it now guards.
|
|
9911
|
+
*/
|
|
9912
|
+
if (this.fieldComposer === null) {
|
|
9913
|
+
if (this.distanceFields.length === 0)
|
|
9914
|
+
return;
|
|
9915
|
+
this.fieldComposer = new FieldComposer(this.surface.device, DEFAULT_FIELD_COMPOSE);
|
|
9916
|
+
}
|
|
9917
|
+
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
9918
|
+
}
|
|
9919
|
+
/** The world field this frame composed, or null where none was. For a pass that marches it. */
|
|
9920
|
+
get distanceField() {
|
|
9921
|
+
return this.fieldComposer?.field ?? null;
|
|
9922
|
+
}
|
|
9923
|
+
/**
|
|
9924
|
+
* Refresh this frame's share of the probe grid from the world field, or do nothing at all.
|
|
9925
|
+
*
|
|
9926
|
+
* **Doing nothing is the ordinary case and must cost nothing**, which is why every condition is
|
|
9927
|
+
* answered before the baker exists: a frame may have composed no field — including a frame that
|
|
9928
|
+
* declared none after a frame that did, where the composer exists and holds nothing — and a
|
|
9929
|
+
* profile may have asked for no probe array. With either false this returns having allocated
|
|
9930
|
+
* nothing and recorded nothing, which is what keeps the scenes byte-identical.
|
|
9931
|
+
*
|
|
9932
|
+
* **The grid is the declared one, or one fitted to the fields themselves.** A consumer that
|
|
9933
|
+
* placed its probes always wins. One that did not gets a grid around whatever it said its light
|
|
9934
|
+
* may be traced against — which is the right extent rather than a convenient one, because a grid
|
|
9935
|
+
* sized to everything drawn would spend its probes on the inside of the sky.
|
|
9936
|
+
*/
|
|
9937
|
+
bakeIndirectProbes(encoder) {
|
|
9938
|
+
const field = this.fieldComposer?.field;
|
|
9939
|
+
if (field === undefined || field === null)
|
|
9940
|
+
return;
|
|
9941
|
+
/*
|
|
9942
|
+
* **The grid is settled before the array is read, and the first version read it first.**
|
|
9943
|
+
* Fitting a grid calls `setProbeGrid`, which reallocates the array for the new layer count —
|
|
9944
|
+
* so a baker built from the array captured a line earlier held the one-layer array the
|
|
9945
|
+
* renderer starts with, and asked it for a view of layer 1. The device refused in those words.
|
|
9946
|
+
* It is the trap `setProbeGrid` already documents for the flat bind groups: a group holds
|
|
9947
|
+
* whatever texture existed when it was built, for ever, with no error anywhere until the
|
|
9948
|
+
* sizes disagree.
|
|
9949
|
+
*/
|
|
9950
|
+
const grid = this.indirectGrid();
|
|
9951
|
+
if (grid === null)
|
|
9952
|
+
return;
|
|
9953
|
+
const array = this.probeArray;
|
|
9954
|
+
const view = this.probeView;
|
|
9955
|
+
if (array === null || view === null)
|
|
9956
|
+
return;
|
|
9957
|
+
if (this.probeBaker !== null && this.bakerArray !== array) {
|
|
9958
|
+
this.probeBaker.dispose();
|
|
9959
|
+
this.probeBaker = null;
|
|
9960
|
+
}
|
|
9961
|
+
this.probeBaker ??= new ProbeBaker(this.surface.device, {
|
|
9962
|
+
array,
|
|
9963
|
+
view,
|
|
9964
|
+
sampler: this.probeSampler,
|
|
9965
|
+
edge: this.probeEdge,
|
|
9966
|
+
format: this.pipelines.format,
|
|
9967
|
+
});
|
|
9968
|
+
this.bakerArray = array;
|
|
9969
|
+
/*
|
|
9970
|
+
* **Whether the scene rasterised this grid first**, which decides two things: that the
|
|
9971
|
+
* roughness chain is already valid and must not be overwritten by a diffuse map, and that the
|
|
9972
|
+
* probes hold direct light for the trace to bounce. A grid that was never rasterised is traced
|
|
9973
|
+
* anyway and converges on whatever it started with — see `ProbeBaker`'s header.
|
|
9974
|
+
*/
|
|
9975
|
+
/*
|
|
9976
|
+
* The frame's own sun, taken from the environment `bindMeshPass` was given rather than from a
|
|
9977
|
+
* capture — which is what makes a bounce follow a light that moved. A frame that drew no mesh
|
|
9978
|
+
* has no environment and traces with the sun off, which is the honest answer for a frame that
|
|
9979
|
+
* never said what was lighting it.
|
|
9980
|
+
*/
|
|
9981
|
+
const env = this.frameEnv;
|
|
9982
|
+
this.probeBaker.bake(encoder, field, grid, this.probeBaked, {
|
|
9983
|
+
direction: env?.directionalDir ?? NO_SUN,
|
|
9984
|
+
colour: env?.directionalColor ?? NO_SUN,
|
|
9985
|
+
/* What a ray that left the world finds. The frame's own ambient, not a capture. */
|
|
9986
|
+
sky: env?.ambient ?? NO_SUN,
|
|
9987
|
+
});
|
|
9988
|
+
/*
|
|
9989
|
+
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
9990
|
+
* gates the whole array, and a grid nothing rasterised would otherwise never be sampled — the
|
|
9991
|
+
* lit pass would read the hemispheric gradient and the traced light would go nowhere.
|
|
9992
|
+
*/
|
|
9993
|
+
if (this.probeBaker.complete) {
|
|
9994
|
+
for (let layer = 0; layer < grid.layers; layer += 1)
|
|
9995
|
+
this.markProbeFilled(layer);
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
/** The declared grid, or one fitted to this frame's fields, or null where neither is possible. */
|
|
9999
|
+
indirectGrid() {
|
|
10000
|
+
if (this.probes !== null)
|
|
10001
|
+
return this.probes;
|
|
10002
|
+
if (!distanceFieldBounds(this.distanceFields, this.fieldBoundsMin, this.fieldBoundsMax)) {
|
|
10003
|
+
return null;
|
|
10004
|
+
}
|
|
10005
|
+
const fitted = fitProbeGrid({ min: this.fieldBoundsMin, max: this.fieldBoundsMax }, INDIRECT_PROBE_SPACING, MAX_ENV_PROBES);
|
|
10006
|
+
if (!this.setProbeGrid(fitted)) {
|
|
10007
|
+
if (!this.indirectGridRefused) {
|
|
10008
|
+
this.indirectGridRefused = true;
|
|
10009
|
+
console.warn('[driftengine] indirect light needs a probe grid and this profile has no probe array, ' +
|
|
10010
|
+
'so nothing is traced. Ask for a quality profile with the environment probe enabled.');
|
|
10011
|
+
}
|
|
10012
|
+
return null;
|
|
10013
|
+
}
|
|
10014
|
+
return this.probes;
|
|
10015
|
+
}
|
|
10016
|
+
/**
|
|
10017
|
+
* The device milliseconds the last composed field took, or null where nothing measured it.
|
|
10018
|
+
*
|
|
10019
|
+
* One frame behind, because a timestamp is read back and a readback that blocked would be the
|
|
10020
|
+
* measurement changing what it measures. Null also where no field has ever been composed, and
|
|
10021
|
+
* where the device was not asked for `timestamp-query`.
|
|
10022
|
+
*/
|
|
10023
|
+
get distanceFieldMs() {
|
|
10024
|
+
return this.fieldComposer?.composeMs ?? null;
|
|
10025
|
+
}
|
|
10026
|
+
/**
|
|
10027
|
+
* The device milliseconds the last traced probe refresh took, or null where nothing measured it.
|
|
10028
|
+
*
|
|
10029
|
+
* **A refresh rather than a grid**, which is `PROBES_PER_FRAME` probes and is what a frame
|
|
10030
|
+
* actually pays. One frame behind, for the reason `distanceFieldMs` gives.
|
|
10031
|
+
*/
|
|
10032
|
+
get indirectBakeMs() {
|
|
10033
|
+
return this.probeBaker?.bakeMs ?? null;
|
|
10034
|
+
}
|
|
10035
|
+
/** Ask for the last frame's figures, once the frame that recorded them has been submitted. */
|
|
10036
|
+
readDistanceFieldTimings() {
|
|
10037
|
+
this.fieldComposer?.readTimings();
|
|
10038
|
+
this.probeBaker?.readTimings();
|
|
10039
|
+
}
|
|
8900
10040
|
/**
|
|
8901
10041
|
* Whether a mesh is entirely behind the occluders declared this frame.
|
|
8902
10042
|
*
|
|
@@ -8937,6 +10077,7 @@ export class WebGPURenderer {
|
|
|
8937
10077
|
pass: null,
|
|
8938
10078
|
outputTransform: 0,
|
|
8939
10079
|
outputExposure: 1,
|
|
10080
|
+
jitter: this.passJitter,
|
|
8940
10081
|
};
|
|
8941
10082
|
/**
|
|
8942
10083
|
* Let something outside this file draw into the frame. See `Renderer.registerPass`.
|
|
@@ -8966,7 +10107,53 @@ export class WebGPURenderer {
|
|
|
8966
10107
|
prepareContext = {
|
|
8967
10108
|
backend: 'webgpu',
|
|
8968
10109
|
encoder: null,
|
|
10110
|
+
environment: null,
|
|
10111
|
+
distanceField: null,
|
|
10112
|
+
jitter: this.passJitter,
|
|
8969
10113
|
};
|
|
10114
|
+
/**
|
|
10115
|
+
* The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
|
|
10116
|
+
*
|
|
10117
|
+
* **Mutated in place for the reason the context above is**: this runs once a frame for every
|
|
10118
|
+
* registered pass, and the house rule about per-frame allocation binds the seam as much as the
|
|
10119
|
+
* verbs. The view is the only field that can change identity, and it changes when the grid's
|
|
10120
|
+
* layer count does.
|
|
10121
|
+
*/
|
|
10122
|
+
passEnvironment = {
|
|
10123
|
+
view: null,
|
|
10124
|
+
sampler: null,
|
|
10125
|
+
edge: 0,
|
|
10126
|
+
maxLod: 0,
|
|
10127
|
+
irradianceLevel: 0,
|
|
10128
|
+
irradiance: false,
|
|
10129
|
+
layers: 0,
|
|
10130
|
+
};
|
|
10131
|
+
/**
|
|
10132
|
+
* The frame's reconstruction jitter: one step of the sequence a frame, taken before any pass
|
|
10133
|
+
* prepares.
|
|
10134
|
+
*
|
|
10135
|
+
* **Here and not in `bindMeshPass`, for two reasons that were both defects.** A registered pass
|
|
10136
|
+
* prepares at `beginFrame`, before any mesh pass is bound, so a jitter chosen there could not
|
|
10137
|
+
* reach it — and the GPU-driven pipeline draws its whole world in `prepare`, unjittered, into a
|
|
10138
|
+
* resolve that un-jitters everything. And a frame that bound its mesh pass twice took two steps
|
|
10139
|
+
* of the sequence and resolved against the second.
|
|
10140
|
+
*
|
|
10141
|
+
* The sizes are the frame's by now: `ensureReconstruction` has run.
|
|
10142
|
+
*/
|
|
10143
|
+
settleReconJitter() {
|
|
10144
|
+
if (!this.reconstructionWanted) {
|
|
10145
|
+
this.passJitter[0] = 0;
|
|
10146
|
+
this.passJitter[1] = 0;
|
|
10147
|
+
return;
|
|
10148
|
+
}
|
|
10149
|
+
this.reconFrameIndex += 1;
|
|
10150
|
+
this.reconPreviousJitter.set(this.reconJitter);
|
|
10151
|
+
jitterOffsetFor(this.reconFrameIndex, reconJitterPhases(this.renderWidth, this.surface.canvas.width), this.reconJitter);
|
|
10152
|
+
/* A render pixel is two over the render's size of the clip square; y upward, as the resolve's
|
|
10153
|
+
rows and the camera's own matrix both run. */
|
|
10154
|
+
this.passJitter[0] = (2 * this.reconJitter[0]) / this.renderWidth;
|
|
10155
|
+
this.passJitter[1] = (2 * this.reconJitter[1]) / this.renderHeight;
|
|
10156
|
+
}
|
|
8970
10157
|
/**
|
|
8971
10158
|
* Give every pass that owns a target its chance to fill it, before the frame's own opens.
|
|
8972
10159
|
*
|
|
@@ -8981,6 +10168,33 @@ export class WebGPURenderer {
|
|
|
8981
10168
|
if (this.preparingPasses === 0)
|
|
8982
10169
|
return;
|
|
8983
10170
|
this.prepareContext.encoder = encoder;
|
|
10171
|
+
/*
|
|
10172
|
+
* **The same test the lit pass applies to its own binding**: a view exists from the moment the
|
|
10173
|
+
* array is created, and what makes it *readable* is a completed bake that is not the one
|
|
10174
|
+
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
10175
|
+
* the failure `probeBaked` was introduced for on the forward path.
|
|
10176
|
+
*/
|
|
10177
|
+
const usable = this.probeView !== null && this.probeBaked && !this.probePassActive;
|
|
10178
|
+
if (usable) {
|
|
10179
|
+
const environment = this.passEnvironment;
|
|
10180
|
+
environment.view = this.probeView;
|
|
10181
|
+
environment.sampler = this.probeSampler;
|
|
10182
|
+
environment.edge = this.probeEdge;
|
|
10183
|
+
environment.maxLod = this.probeMaxLod;
|
|
10184
|
+
environment.irradianceLevel = irradianceLevelFor(this.probeEdge);
|
|
10185
|
+
environment.irradiance = this.probeAmbient;
|
|
10186
|
+
environment.layers = this.probes?.layers ?? 1;
|
|
10187
|
+
this.prepareContext.environment = environment;
|
|
10188
|
+
}
|
|
10189
|
+
else {
|
|
10190
|
+
this.prepareContext.environment = null;
|
|
10191
|
+
}
|
|
10192
|
+
/*
|
|
10193
|
+
* The field the *previous* frame composed, because this runs at `beginFrame` and the
|
|
10194
|
+
* composition runs at `endFrame`. `PrepareContext.distanceField` says why that is right rather
|
|
10195
|
+
* than merely what happens.
|
|
10196
|
+
*/
|
|
10197
|
+
this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
|
|
8984
10198
|
for (const definition of this.passes.definitions) {
|
|
8985
10199
|
definition?.prepare?.(this.prepareContext);
|
|
8986
10200
|
}
|
|
@@ -9172,7 +10386,12 @@ export class WebGPURenderer {
|
|
|
9172
10386
|
depthFormat: DEPTH_FORMAT,
|
|
9173
10387
|
/* The same four numbers this backend's own verbs are projected through. See `PassDevice`. */
|
|
9174
10388
|
clipCorrection: CLIP_CORRECTION,
|
|
10389
|
+
/* And the one a pass that wrote its own WGSL wants, which is the same without the Y
|
|
10390
|
+
negation — that negation cancels `naga`'s, and a hand-written shader has none. */
|
|
10391
|
+
depthCorrection: DEPTH_CORRECTION,
|
|
9175
10392
|
samples: this.samples,
|
|
10393
|
+
/* Whether a pass drawing the world owes the frame its jitter and its depth. See `PassDevice`. */
|
|
10394
|
+
reconstruction: this.reconstructionWanted,
|
|
9176
10395
|
};
|
|
9177
10396
|
}
|
|
9178
10397
|
/**
|
|
@@ -9223,8 +10442,13 @@ export class WebGPURenderer {
|
|
|
9223
10442
|
const cssWidth = Math.max(canvas.clientWidth, 1);
|
|
9224
10443
|
const cssHeight = Math.max(canvas.clientHeight, 1);
|
|
9225
10444
|
const origin = canvas.getBoundingClientRect();
|
|
9226
|
-
|
|
9227
|
-
|
|
10445
|
+
/*
|
|
10446
|
+
* **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
|
|
10447
|
+
* own pass, which draws into the render-size targets, so a box measured in the drawing buffer
|
|
10448
|
+
* would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
|
|
10449
|
+
*/
|
|
10450
|
+
const scaleX = this.renderWidth / cssWidth;
|
|
10451
|
+
const scaleY = this.renderHeight / cssHeight;
|
|
9228
10452
|
const left = rect.left - origin.left;
|
|
9229
10453
|
const top = rect.top - origin.top;
|
|
9230
10454
|
const w = Math.max(1, Math.round(rect.width * scaleX));
|
|
@@ -9251,8 +10475,9 @@ export class WebGPURenderer {
|
|
|
9251
10475
|
* edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
|
|
9252
10476
|
* recorded in the parity ledger, and the alternative was an exception.
|
|
9253
10477
|
*/
|
|
9254
|
-
|
|
9255
|
-
const
|
|
10478
|
+
/* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
|
|
10479
|
+
const targetWidth = this.renderWidth;
|
|
10480
|
+
const targetHeight = this.renderHeight;
|
|
9256
10481
|
const clampedX = Math.max(0, Math.min(x, targetWidth));
|
|
9257
10482
|
const clampedY = Math.max(0, Math.min(y, targetHeight));
|
|
9258
10483
|
const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
|
|
@@ -9324,9 +10549,9 @@ export class WebGPURenderer {
|
|
|
9324
10549
|
const pass = this.pass;
|
|
9325
10550
|
if (this.surface.lost || pass === null)
|
|
9326
10551
|
return;
|
|
9327
|
-
|
|
9328
|
-
pass.setViewport(0, 0,
|
|
9329
|
-
pass.setScissorRect(0, 0,
|
|
10552
|
+
/* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
|
|
10553
|
+
pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
|
|
10554
|
+
pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
|
|
9330
10555
|
}
|
|
9331
10556
|
/**
|
|
9332
10557
|
* Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
|
|
@@ -9777,7 +11002,22 @@ export class WebGPURenderer {
|
|
|
9777
11002
|
get probeBaked() {
|
|
9778
11003
|
return this.probes !== null && this.probeFilledCount === this.probes.layers;
|
|
9779
11004
|
}
|
|
9780
|
-
/**
|
|
11005
|
+
/**
|
|
11006
|
+
* Record that one layer now holds a convolution, without counting a rebake twice.
|
|
11007
|
+
*
|
|
11008
|
+
* **And rebuild the flat groups on the last one**, which is the third time this backend has paid
|
|
11009
|
+
* for a bind group holding whatever texture existed when it was built. `flatTextures` resolves
|
|
11010
|
+
* `uEnvironment` to a one-texel **white** stand-in while `probeBaked` is false, so a grid filled
|
|
11011
|
+
* by the trace rather than by `bakeProbeGrid` — which runs before any frame and therefore before
|
|
11012
|
+
* any group — leaves every flat group holding that white for the life of the renderer. The
|
|
11013
|
+
* uniform beside it flips to "use the grid" and the grid the shader reads is pure white.
|
|
11014
|
+
*
|
|
11015
|
+
* Measured on `demo/dev/bounce.html?seed=0`: every surface came back at exactly its own albedo
|
|
11016
|
+
* times 255, with the sun switched off and the trace pinned to a constant. It had been recorded
|
|
11017
|
+
* as undefined memory in the probe array; it is a deliberate white texture, read on purpose.
|
|
11018
|
+
* The refraction snapshot's own note two hundred lines below says the same thing about the same
|
|
11019
|
+
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
11020
|
+
*/
|
|
9781
11021
|
markProbeFilled(layer) {
|
|
9782
11022
|
if (layer < 0 || layer >= this.probeFilled.length)
|
|
9783
11023
|
return;
|
|
@@ -9785,6 +11025,8 @@ export class WebGPURenderer {
|
|
|
9785
11025
|
return;
|
|
9786
11026
|
this.probeFilled[layer] = 1;
|
|
9787
11027
|
this.probeFilledCount++;
|
|
11028
|
+
if (this.probeBaked)
|
|
11029
|
+
this.rebuildFlatGroupsForNewRings();
|
|
9788
11030
|
}
|
|
9789
11031
|
/**
|
|
9790
11032
|
* The array every probe is a layer of, sized for a grid.
|
|
@@ -9805,6 +11047,46 @@ export class WebGPURenderer {
|
|
|
9805
11047
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
9806
11048
|
this.probeFilled = new Uint8Array(layers);
|
|
9807
11049
|
this.probeFilledCount = 0;
|
|
11050
|
+
/*
|
|
11051
|
+
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
11052
|
+
* sentence the directional shadow maps are cleared under, arrived at from the other end.
|
|
11053
|
+
*
|
|
11054
|
+
* A traced probe reads the array's own irradiance level back as the light that has already
|
|
11055
|
+
* bounced, so an unwritten array is not "nothing has bounced yet": it is whatever the
|
|
11056
|
+
* allocator left, amplified by every refresh. Measured on `demo/dev/bounce.html` with
|
|
11057
|
+
* `?seed=0`, a grid nothing had rasterised settled on a uniform **209 of 255** in every
|
|
11058
|
+
* channel, which reads as a working ambient term and is memory.
|
|
11059
|
+
*
|
|
11060
|
+
* **Every level, not only the one the bake writes.** The roughness chain is sampled by
|
|
11061
|
+
* reflective surfaces long before any bake has filled it, and a first fill writes the whole
|
|
11062
|
+
* chain only for a layer the scheduler has reached.
|
|
11063
|
+
*/
|
|
11064
|
+
const clear = device.createCommandEncoder({ label: 'probe.clear' });
|
|
11065
|
+
const levels = irradianceLevelFor(this.probeEdge) + 1;
|
|
11066
|
+
for (let layer = 0; layer < layers; layer += 1) {
|
|
11067
|
+
for (let level = 0; level < levels; level += 1) {
|
|
11068
|
+
clear
|
|
11069
|
+
.beginRenderPass({
|
|
11070
|
+
label: 'probe.clear',
|
|
11071
|
+
colorAttachments: [
|
|
11072
|
+
{
|
|
11073
|
+
view: this.probeArray.createView({
|
|
11074
|
+
dimension: '2d',
|
|
11075
|
+
baseArrayLayer: layer,
|
|
11076
|
+
arrayLayerCount: 1,
|
|
11077
|
+
baseMipLevel: level,
|
|
11078
|
+
mipLevelCount: 1,
|
|
11079
|
+
}),
|
|
11080
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
11081
|
+
loadOp: 'clear',
|
|
11082
|
+
storeOp: 'store',
|
|
11083
|
+
},
|
|
11084
|
+
],
|
|
11085
|
+
})
|
|
11086
|
+
.end();
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
device.queue.submit([clear.finish()]);
|
|
9808
11090
|
}
|
|
9809
11091
|
/**
|
|
9810
11092
|
* Declare where a grid's probes stand, and allocate the layers for them.
|
|
@@ -9955,7 +11237,7 @@ export class WebGPURenderer {
|
|
|
9955
11237
|
const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
|
|
9956
11238
|
this.perFrameInts[this.materialField('uOutputTransform')] = 0;
|
|
9957
11239
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
9958
|
-
this.
|
|
11240
|
+
this.materials.dirty();
|
|
9959
11241
|
for (let face = 0; face < PROBE_FACES.length; face++) {
|
|
9960
11242
|
const aim = PROBE_FACES[face];
|
|
9961
11243
|
if (aim === undefined)
|
|
@@ -10029,7 +11311,7 @@ export class WebGPURenderer {
|
|
|
10029
11311
|
this.markProbeFilled(layer);
|
|
10030
11312
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
10031
11313
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
10032
|
-
this.
|
|
11314
|
+
this.materials.dirty();
|
|
10033
11315
|
this.probePassActive = false;
|
|
10034
11316
|
this.rebuildFlatBindGroup();
|
|
10035
11317
|
return true;
|
|
@@ -10479,7 +11761,13 @@ export class WebGPURenderer {
|
|
|
10479
11761
|
label: 'post.resolvedDepth',
|
|
10480
11762
|
size: [width, height],
|
|
10481
11763
|
format: RESOLVED_DEPTH_FORMAT,
|
|
10482
|
-
|
|
11764
|
+
/*
|
|
11765
|
+
* **`COPY_SRC` only where a reconstruction is going to keep it**, because a usage is a
|
|
11766
|
+
* promise about the whole life of the texture and every frame that does not reconstruct
|
|
11767
|
+
* would be paying for one it never makes. A reconstruction copies this into its own target
|
|
11768
|
+
* at the end of the frame, which is what its disocclusion tests against next frame.
|
|
11769
|
+
*/
|
|
11770
|
+
usage: this.quality.reconstruction > 0 ? USAGE | 0x1 : USAGE,
|
|
10483
11771
|
});
|
|
10484
11772
|
this.resolvedDepthView = this.resolvedDepth.createView();
|
|
10485
11773
|
/*
|
|
@@ -10891,6 +12179,9 @@ export class WebGPURenderer {
|
|
|
10891
12179
|
* the 2026-08-13 rule asks for — one decision, bound twice.
|
|
10892
12180
|
*/
|
|
10893
12181
|
dispose() {
|
|
12182
|
+
for (const texture of this.retiredTextures)
|
|
12183
|
+
texture.destroy();
|
|
12184
|
+
this.retiredTextures.length = 0;
|
|
10894
12185
|
const passDevice = this.passDevice();
|
|
10895
12186
|
drainRegistry(this.passes, (definition) => definition.dispose?.(passDevice));
|
|
10896
12187
|
this.passReads.clear();
|
|
@@ -10900,6 +12191,8 @@ export class WebGPURenderer {
|
|
|
10900
12191
|
this.preparingPasses = 0;
|
|
10901
12192
|
const computeDevice = this.computeDevice();
|
|
10902
12193
|
drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
|
|
12194
|
+
this.fieldComposer?.dispose();
|
|
12195
|
+
this.fieldComposer = null;
|
|
10903
12196
|
this.pass = null;
|
|
10904
12197
|
this.encoder = null;
|
|
10905
12198
|
this.skinPalettes.dispose();
|