@downdraft/engine 0.1.1 → 0.2.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/app/README.md +11 -0
- package/app/src/renderer/bootstrap.ts +295 -19
- package/app/src/renderer/feature-log.ts +1 -1
- package/app/src/renderer/game-module.ts +60 -12
- package/app/src/renderer/hooks.ts +9 -1
- package/app/src/renderer/hot-reload.ts +9 -9
- package/app/src/renderer/mcp-harness.ts +2 -2
- package/app/src/renderer/standard-automation-tools.ts +104 -3
- package/app/src/shared/feature-log.ts +2 -1
- package/app/src/shared/mcp-discovery.ts +8 -3
- package/app/src/shared/mcp-proxy.ts +2 -2
- package/asset-bake/README.md +22 -0
- package/core/README.md +25 -0
- package/core/src/animation/skeleton-animator.ts +23 -5
- package/core/src/assets/manager.ts +4 -4
- package/core/src/ecs/events.ts +1 -1
- package/core/src/ecs/schedule.ts +1 -1
- package/core/src/ecs/world.ts +1 -1
- package/core/src/index.ts +3 -4
- package/core/src/input/local-player-manager.ts +2 -2
- package/core/src/library/host.ts +27 -13
- package/core/src/library/library.ts +13 -5
- package/core/src/module/cross-thread.ts +6 -0
- package/core/src/module/diagnostics.ts +10 -6
- package/core/src/module/host.ts +65 -18
- package/core/src/module/index.ts +1 -0
- package/core/src/module/module.ts +11 -5
- package/core/src/module/renderer-module.ts +15 -0
- package/core/src/physics/safety.ts +1 -1
- package/core/src/platform/bun-preload.ts +7 -2
- package/core/src/platform/glob-polyfill.ts +17 -8
- package/core/src/platform/pkg-root.ts +27 -0
- package/core/src/platform/runtime.ts +29 -0
- package/core/src/plugin/discovery.ts +1 -1
- package/core/src/plugin/extension-loaders.ts +6 -4
- package/core/src/plugin/host.ts +13 -13
- package/core/src/plugin/loader-asset.ts +5 -5
- package/core/src/plugin/loader-quickjs.ts +1 -1
- package/core/src/plugin/loader-wasm.ts +3 -3
- package/core/src/plugin/loader-worker.ts +1 -1
- package/core/src/plugin/manifest.ts +8 -3
- package/core/src/plugin/workshop.ts +3 -3
- package/core/src/profiling/index.ts +1 -1
- package/core/src/profiling/profiling-sab.ts +19 -0
- package/core/src/profiling/warnings.ts +1 -1
- package/core/src/profiling/worker-prelude.ts +1 -1
- package/core/src/render/bindless/frame-bindings.ts +1 -1
- package/core/src/render/bindless/material-manager.ts +2 -2
- package/core/src/render/bindless/texture-registry.ts +2 -2
- package/core/src/render/device.ts +5 -5
- package/core/src/render/frame-graph.ts +2 -2
- package/core/src/render/game-renderer.ts +42 -18
- package/core/src/render/hot-reload.ts +3 -3
- package/core/src/render/renderer-module-host.ts +32 -12
- package/core/src/render/shader-validator.ts +7 -7
- package/core/src/render/surface.ts +1 -1
- package/core/src/sab/errors.ts +1 -1
- package/core/src/sab/sab-polyfill.ts +1 -1
- package/core/src/save/migration-registry.ts +4 -4
- package/core/src/save/schema.ts +1 -1
- package/core/src/scene/prefab.ts +1 -1
- package/core/src/scene/scene-manager.ts +2 -2
- package/core/src/scene/scene.ts +4 -4
- package/core/src/scene/streaming.ts +2 -2
- package/core/src/scripting/hot-reload.ts +2 -2
- package/core/src/sim/hot-reload-pipeline.ts +30 -6
- package/core/src/sim/sim-state-helper.ts +1 -1
- package/core/src/sim/transient-state-registry.ts +1 -1
- package/core/src/sim/worker-loop.ts +2 -2
- package/core/src/telemetry/gpu-profiler.ts +3 -3
- package/core/src/util/index.ts +1 -1
- package/core/src/util/logger.ts +38 -7
- package/core/src/version.ts +13 -0
- package/core/src/worker/base-worker-host.ts +87 -25
- package/core/src/worker/buffer-sync.ts +1 -1
- package/core/src/worker/crash-recovery.ts +2 -2
- package/core/src/worker/instrumented-worker-host.ts +3 -3
- package/core/src/worker/rpc.ts +62 -11
- package/core/src/worker/sim-worker-base.ts +4 -9
- package/core/src/worker/task-pool.ts +2 -2
- package/dist/.dd-manifest.json +4119 -0
- package/dist/app/src/index.js +9 -0
- package/dist/app/src/renderer/bootstrap.js +446 -0
- package/dist/app/src/renderer/downdraft-base.css +93 -0
- package/dist/app/src/renderer/feature-log.js +180 -0
- package/dist/app/src/renderer/game-module.js +562 -0
- package/dist/app/src/renderer/hooks.js +82 -0
- package/dist/app/src/renderer/hot-reload.js +158 -0
- package/dist/app/src/renderer/import-cache.js +77 -0
- package/dist/app/src/renderer/index.js +149 -0
- package/dist/app/src/renderer/mcp-harness.js +112 -0
- package/dist/app/src/renderer/save-store-factory.js +180 -0
- package/dist/app/src/renderer/sim-bridge.js +138 -0
- package/dist/app/src/renderer/standard-automation-tools.js +720 -0
- package/dist/app/src/shared/feature-log.js +54 -0
- package/dist/app/src/shared/gpu-info.js +74 -0
- package/dist/app/src/shared/import-cache-store.js +123 -0
- package/dist/app/src/shared/index.js +4 -0
- package/dist/app/src/shared/mcp-discovery.js +55 -0
- package/dist/app/src/shared/mcp-proxy.js +71 -0
- package/dist/app/src/shared/types.js +13 -0
- package/dist/asset-bake/src/bake-audio.js +103 -0
- package/dist/asset-bake/src/bake-gltf.js +141 -0
- package/dist/asset-bake/src/bake.js +63 -0
- package/dist/asset-bake/src/cache.js +147 -0
- package/dist/asset-bake/src/config.js +93 -0
- package/dist/asset-bake/src/index.js +25 -0
- package/dist/core/src/animation/animation-event.js +24 -0
- package/dist/core/src/animation/bone-mask.js +103 -0
- package/dist/core/src/animation/clip.js +210 -0
- package/dist/core/src/animation/index.js +13 -0
- package/dist/core/src/animation/mixamo.js +78 -0
- package/dist/core/src/animation/morph-target.js +37 -0
- package/dist/core/src/animation/player.js +621 -0
- package/dist/core/src/animation/retarget.js +101 -0
- package/dist/core/src/animation/skeleton-animator.js +1361 -0
- package/dist/core/src/animation/skeleton.js +166 -0
- package/dist/core/src/animation/state-machine.js +252 -0
- package/dist/core/src/assets/blob-store.js +14 -0
- package/dist/core/src/assets/cache.js +168 -0
- package/dist/core/src/assets/cubemap-converter.js +129 -0
- package/dist/core/src/assets/environment-manager.js +84 -0
- package/dist/core/src/assets/import-cache.js +36 -0
- package/dist/core/src/assets/import-settings.js +34 -0
- package/dist/core/src/assets/importer.js +78 -0
- package/dist/core/src/assets/index.js +29 -0
- package/dist/core/src/assets/irradiance-generator.js +179 -0
- package/dist/core/src/assets/loader-audio.js +70 -0
- package/dist/core/src/assets/loader-cubemap.js +141 -0
- package/dist/core/src/assets/loader-dds.js +185 -0
- package/dist/core/src/assets/loader-hdr.js +430 -0
- package/dist/core/src/assets/loader-mesh.js +47 -0
- package/dist/core/src/assets/loader-shader.js +61 -0
- package/dist/core/src/assets/loader-texture.js +264 -0
- package/dist/core/src/assets/lod.js +150 -0
- package/dist/core/src/assets/manager.js +401 -0
- package/dist/core/src/assets/manifest.js +57 -0
- package/dist/core/src/assets/material-bridge.js +160 -0
- package/dist/core/src/assets/model-importer.js +32 -0
- package/dist/core/src/assets/model-normalizer.js +213 -0
- package/dist/core/src/assets/model-to-gpu.js +44 -0
- package/dist/core/src/assets/model-to-mesh.js +171 -0
- package/dist/core/src/assets/prefilter-generator.js +220 -0
- package/dist/core/src/audio/engine.js +153 -0
- package/dist/core/src/audio/interface.js +8 -0
- package/dist/core/src/audio/listener.js +19 -0
- package/dist/core/src/audio/mixer.js +124 -0
- package/dist/core/src/audio/registry.js +43 -0
- package/dist/core/src/audio/sab.js +74 -0
- package/dist/core/src/audio/source.js +60 -0
- package/dist/core/src/change-detection/atomics.js +1 -0
- package/dist/core/src/change-detection/tracker.js +26 -0
- package/dist/core/src/debug-draw/lines.js +27 -0
- package/dist/core/src/debug-draw/points.js +9 -0
- package/dist/core/src/debug-draw/queue.js +77 -0
- package/dist/core/src/debug-draw/text.js +12 -0
- package/dist/core/src/ecs/archetype.js +271 -0
- package/dist/core/src/ecs/component.js +109 -0
- package/dist/core/src/ecs/entity.js +11 -0
- package/dist/core/src/ecs/events.js +73 -0
- package/dist/core/src/ecs/hierarchy.js +104 -0
- package/dist/core/src/ecs/hmr-swap-registry.js +15 -0
- package/dist/core/src/ecs/index.js +14 -0
- package/dist/core/src/ecs/query.js +110 -0
- package/dist/core/src/ecs/resource.js +3 -0
- package/dist/core/src/ecs/schedule.js +157 -0
- package/dist/core/src/ecs/system-params.js +45 -0
- package/dist/core/src/ecs/system.js +18 -0
- package/dist/core/src/ecs/world.js +292 -0
- package/dist/core/src/index.js +191 -0
- package/dist/core/src/input/context.js +69 -0
- package/dist/core/src/input/dom-handler.js +288 -0
- package/dist/core/src/input/local-player-manager.js +145 -0
- package/dist/core/src/input/mapping.js +36 -0
- package/dist/core/src/input/multi-sab-bridge.js +224 -0
- package/dist/core/src/input/multi-state.js +48 -0
- package/dist/core/src/input/sab-bridge.js +66 -0
- package/dist/core/src/input/state.js +108 -0
- package/dist/core/src/input/ui-router.js +76 -0
- package/dist/core/src/library/host.js +291 -0
- package/dist/core/src/library/index.js +1 -0
- package/dist/core/src/library/library.js +20 -0
- package/dist/core/src/material/compiler.js +73 -0
- package/dist/core/src/material/graph-bridge.js +119 -0
- package/dist/core/src/material/index.js +6 -0
- package/dist/core/src/material/library.js +414 -0
- package/dist/core/src/material/material.js +120 -0
- package/dist/core/src/material/variants.js +37 -0
- package/dist/core/src/math/mat4.js +198 -0
- package/dist/core/src/math/perlin-noise-2d.js +66 -0
- package/dist/core/src/math/perlin-noise-3d.js +76 -0
- package/dist/core/src/math/quat.js +29 -0
- package/dist/core/src/math/rng.js +25 -0
- package/dist/core/src/math/trig.js +42 -0
- package/dist/core/src/math/value-noise.js +49 -0
- package/dist/core/src/mesh/builder.js +137 -0
- package/dist/core/src/mesh/csg.js +250 -0
- package/dist/core/src/mesh/greased-line.js +102 -0
- package/dist/core/src/mesh/mesh.js +6 -0
- package/dist/core/src/mesh/parametric.js +392 -0
- package/dist/core/src/mesh/skinning.js +30 -0
- package/dist/core/src/mesh/vertex-layout.js +65 -0
- package/dist/core/src/module/cross-thread.js +126 -0
- package/dist/core/src/module/diagnostics.js +99 -0
- package/dist/core/src/module/host.js +449 -0
- package/dist/core/src/module/index.js +6 -0
- package/dist/core/src/module/module.js +1 -0
- package/dist/core/src/module/registry.js +83 -0
- package/dist/core/src/module/renderer-module.js +13 -0
- package/dist/core/src/module/ts-loader.js +20 -0
- package/dist/core/src/particles/compute-pass.js +329 -0
- package/dist/core/src/particles/emitter.js +113 -0
- package/dist/core/src/particles/index.js +7 -0
- package/dist/core/src/particles/particle-data.js +33 -0
- package/dist/core/src/particles/render-pass.js +338 -0
- package/dist/core/src/particles/simulator.js +224 -0
- package/dist/core/src/particles/system.js +157 -0
- package/dist/core/src/physics/body.js +25 -0
- package/dist/core/src/physics/ccd-heuristic.js +46 -0
- package/dist/core/src/physics/character.js +184 -0
- package/dist/core/src/physics/collider.js +36 -0
- package/dist/core/src/physics/collision-system.js +62 -0
- package/dist/core/src/physics/events.js +74 -0
- package/dist/core/src/physics/index.js +24 -0
- package/dist/core/src/physics/interface.js +14 -0
- package/dist/core/src/physics/interpolation-buffer.js +182 -0
- package/dist/core/src/physics/lifecycle.js +66 -0
- package/dist/core/src/physics/load-shedder.js +155 -0
- package/dist/core/src/physics/physics-accumulator.js +94 -0
- package/dist/core/src/physics/physics-system.js +142 -0
- package/dist/core/src/physics/ragdoll-presets.js +248 -0
- package/dist/core/src/physics/ragdoll-system.js +193 -0
- package/dist/core/src/physics/ragdoll.js +117 -0
- package/dist/core/src/physics/raycast.js +97 -0
- package/dist/core/src/physics/realm-manager.js +431 -0
- package/dist/core/src/physics/realm-worker-pool.js +181 -0
- package/dist/core/src/physics/realm-worker.js +91 -0
- package/dist/core/src/physics/realm.js +137 -0
- package/dist/core/src/physics/registry.js +43 -0
- package/dist/core/src/physics/safety.js +211 -0
- package/dist/core/src/physics/snapshot-manager.js +118 -0
- package/dist/core/src/physics/worker-protocol.js +1 -0
- package/dist/core/src/platform/bun-preload.js +175 -0
- package/dist/core/src/platform/css-stub.js +4 -0
- package/dist/core/src/platform/glob-polyfill.js +137 -0
- package/dist/core/src/platform/hidpi.js +52 -0
- package/dist/core/src/platform/pkg-root.js +27 -0
- package/dist/core/src/platform/render-surface.js +15 -0
- package/dist/core/src/platform/rpc.js +82 -0
- package/dist/core/src/platform/runtime.js +93 -0
- package/dist/core/src/plugin/context.js +144 -0
- package/dist/core/src/plugin/diagnostics.js +20 -0
- package/dist/core/src/plugin/discovery.js +124 -0
- package/dist/core/src/plugin/extension-loaders.js +111 -0
- package/dist/core/src/plugin/host.js +542 -0
- package/dist/core/src/plugin/index.js +19 -0
- package/dist/core/src/plugin/loader-asset.js +105 -0
- package/dist/core/src/plugin/loader-quickjs.js +67 -0
- package/dist/core/src/plugin/loader-wasm.js +385 -0
- package/dist/core/src/plugin/loader-worker.js +228 -0
- package/dist/core/src/plugin/manifest.js +424 -0
- package/dist/core/src/plugin/material-registry.js +44 -0
- package/dist/core/src/plugin/mcp-tools.js +147 -0
- package/dist/core/src/plugin/permissions.js +152 -0
- package/dist/core/src/plugin/quickjs-bridge.js +219 -0
- package/dist/core/src/plugin/registry.js +92 -0
- package/dist/core/src/plugin/sandbox-shim.js +79 -0
- package/dist/core/src/plugin/sandbox-worker.js +194 -0
- package/dist/core/src/plugin/wasm-abi.js +62 -0
- package/dist/core/src/plugin/wasm-worker.js +234 -0
- package/dist/core/src/plugin/workshop.js +131 -0
- package/dist/core/src/profiling/event-loop.js +164 -0
- package/dist/core/src/profiling/index.js +23 -0
- package/dist/core/src/profiling/iops/idb-patch.js +211 -0
- package/dist/core/src/profiling/iops/opfs-patch.js +206 -0
- package/dist/core/src/profiling/profiling-sab.js +632 -0
- package/dist/core/src/profiling/task-latency.js +118 -0
- package/dist/core/src/profiling/thread-metrics.js +103 -0
- package/dist/core/src/profiling/trace-event-writer.js +165 -0
- package/dist/core/src/profiling/warnings.js +304 -0
- package/dist/core/src/profiling/worker-prelude.js +281 -0
- package/dist/core/src/render/bind-group.js +33 -0
- package/dist/core/src/render/bindless/bindless-struct.js +39 -0
- package/dist/core/src/render/bindless/bindless.wgsl.js +163 -0
- package/dist/core/src/render/bindless/frame-bindings.js +142 -0
- package/dist/core/src/render/bindless/index.js +6 -0
- package/dist/core/src/render/bindless/material-manager.js +182 -0
- package/dist/core/src/render/bindless/texture-registry.js +498 -0
- package/dist/core/src/render/buffer.js +74 -0
- package/dist/core/src/render/camera-2d.js +151 -0
- package/dist/core/src/render/camera-controller.js +370 -0
- package/dist/core/src/render/camera.js +332 -0
- package/dist/core/src/render/canvas-resize-watcher.js +36 -0
- package/dist/core/src/render/compute-kernel.js +130 -0
- package/dist/core/src/render/constants.js +2 -0
- package/dist/core/src/render/device.js +91 -0
- package/dist/core/src/render/frame-graph.js +594 -0
- package/dist/core/src/render/frustum.js +152 -0
- package/dist/core/src/render/fullscreen-vs.js +24 -0
- package/dist/core/src/render/g-buffer.js +156 -0
- package/dist/core/src/render/game-renderer.js +1165 -0
- package/dist/core/src/render/gpu-driven/cull-pass.js +352 -0
- package/dist/core/src/render/gpu-driven/hzb.js +183 -0
- package/dist/core/src/render/gpu-driven/index.js +6 -0
- package/dist/core/src/render/gpu-driven/indirect-draw-pass.js +300 -0
- package/dist/core/src/render/gpu-driven/instance-buffer.js +150 -0
- package/dist/core/src/render/gpu-driven/mesh-table.js +130 -0
- package/dist/core/src/render/gpu-driven/passes/hzb-build-pass.js +39 -0
- package/dist/core/src/render/gpu-utils.js +72 -0
- package/dist/core/src/render/hdr.js +69 -0
- package/dist/core/src/render/hot-reload.js +236 -0
- package/dist/core/src/render/ibl-bind-group.js +232 -0
- package/dist/core/src/render/ibl.js +148 -0
- package/dist/core/src/render/index.js +86 -0
- package/dist/core/src/render/input-manager.js +224 -0
- package/dist/core/src/render/lighting/area-light.js +142 -0
- package/dist/core/src/render/lighting/cluster-grid.js +255 -0
- package/dist/core/src/render/lighting/cluster-types.js +87 -0
- package/dist/core/src/render/lighting/ies-parser.js +111 -0
- package/dist/core/src/render/lighting.js +300 -0
- package/dist/core/src/render/material-types/depth.wgsl +44 -0
- package/dist/core/src/render/material-types/line.wgsl +50 -0
- package/dist/core/src/render/material-types/matcap.wgsl +51 -0
- package/dist/core/src/render/material-types/normal.wgsl +41 -0
- package/dist/core/src/render/material-types/physical.wgsl +152 -0
- package/dist/core/src/render/material-types/sprite.wgsl +46 -0
- package/dist/core/src/render/material-types/sss.wgsl +79 -0
- package/dist/core/src/render/material-types/toon.wgsl +79 -0
- package/dist/core/src/render/passes/README.md +121 -0
- package/dist/core/src/render/passes/atmosphere.js +107 -0
- package/dist/core/src/render/passes/cluster-lighting-pass.js +441 -0
- package/dist/core/src/render/passes/csm.js +338 -0
- package/dist/core/src/render/passes/cubemap-capture.js +93 -0
- package/dist/core/src/render/passes/debug-viz.js +248 -0
- package/dist/core/src/render/passes/debug.js +500 -0
- package/dist/core/src/render/passes/decal-mesh.js +86 -0
- package/dist/core/src/render/passes/decal-pass.js +344 -0
- package/dist/core/src/render/passes/deferred-lighting.js +406 -0
- package/dist/core/src/render/passes/depth-prepass.js +148 -0
- package/dist/core/src/render/passes/dome-360.js +68 -0
- package/dist/core/src/render/passes/fluid-render.js +116 -0
- package/dist/core/src/render/passes/gi-types.js +78 -0
- package/dist/core/src/render/passes/graph-compute.js +251 -0
- package/dist/core/src/render/passes/greased-line-pass.js +218 -0
- package/dist/core/src/render/passes/morph-target.js +35 -0
- package/dist/core/src/render/passes/opaque.js +966 -0
- package/dist/core/src/render/passes/point-light-shadow.js +221 -0
- package/dist/core/src/render/passes/procedural-texture.js +142 -0
- package/dist/core/src/render/passes/reflection-probe.js +100 -0
- package/dist/core/src/render/passes/rsm-pass.js +228 -0
- package/dist/core/src/render/passes/sdf-text.js +114 -0
- package/dist/core/src/render/passes/shadow-map.js +121 -0
- package/dist/core/src/render/passes/shadow.js +191 -0
- package/dist/core/src/render/passes/skinning-vs.js +102 -0
- package/dist/core/src/render/passes/skinning.js +282 -0
- package/dist/core/src/render/passes/sky-dome.js +380 -0
- package/dist/core/src/render/passes/skybox.js +146 -0
- package/dist/core/src/render/passes/spot-light-shadow.js +223 -0
- package/dist/core/src/render/passes/terrain.js +303 -0
- package/dist/core/src/render/passes/transparent.js +323 -0
- package/dist/core/src/render/passes/underwater-fog.js +173 -0
- package/dist/core/src/render/passes/video-texture.js +58 -0
- package/dist/core/src/render/passes/volumetric-pass.js +278 -0
- package/dist/core/src/render/passes/volumetric-types.js +96 -0
- package/dist/core/src/render/passes/water.js +697 -0
- package/dist/core/src/render/pbr.js +257 -0
- package/dist/core/src/render/pipeline.js +49 -0
- package/dist/core/src/render/render-pass.js +13 -0
- package/dist/core/src/render/renderer-input-bus.js +176 -0
- package/dist/core/src/render/renderer-module-host.js +539 -0
- package/dist/core/src/render/resource-tracker.js +59 -0
- package/dist/core/src/render/sab-canvas-overlay.js +83 -0
- package/dist/core/src/render/shader-validator.js +172 -0
- package/dist/core/src/render/shaders/debug/debug-line.wgsl +27 -0
- package/dist/core/src/render/shaders/debug/debug-point.wgsl +30 -0
- package/dist/core/src/render/shaders/debug/debug-text.wgsl +42 -0
- package/dist/core/src/render/shaders/debug-viz/aabb.wgsl +20 -0
- package/dist/core/src/render/shaders/debug-viz/depth.wgsl +29 -0
- package/dist/core/src/render/shaders/debug-viz/lod.wgsl +38 -0
- package/dist/core/src/render/shaders/debug-viz/normals.wgsl +30 -0
- package/dist/core/src/render/shaders/debug-viz/overdraw.wgsl +21 -0
- package/dist/core/src/render/shaders/debug-viz/tangents.wgsl +29 -0
- package/dist/core/src/render/shaders/debug-viz/wireframe.wgsl +33 -0
- package/dist/core/src/render/shaders/shadow-chunks.js +133 -0
- package/dist/core/src/render/skybox.js +123 -0
- package/dist/core/src/render/splitscreen.js +60 -0
- package/dist/core/src/render/surface.js +63 -0
- package/dist/core/src/render/tonemap.js +171 -0
- package/dist/core/src/render/tracked-render-pass.js +115 -0
- package/dist/core/src/render/viewport.js +81 -0
- package/dist/core/src/render/wgsl-hmr.js +66 -0
- package/dist/core/src/sab/core-input-channel.js +22 -0
- package/dist/core/src/sab/define.js +127 -0
- package/dist/core/src/sab/errors.js +60 -0
- package/dist/core/src/sab/game-input.js +299 -0
- package/dist/core/src/sab/gamepad-devices.js +205 -0
- package/dist/core/src/sab/grid-buffer.js +91 -0
- package/dist/core/src/sab/input.js +38 -0
- package/dist/core/src/sab/layout.js +160 -0
- package/dist/core/src/sab/multi-input-channel.js +39 -0
- package/dist/core/src/sab/record-buffer.js +130 -0
- package/dist/core/src/sab/sab-polyfill.js +47 -0
- package/dist/core/src/sab/sim-channel.js +397 -0
- package/dist/core/src/sab/slot-buffer.js +140 -0
- package/dist/core/src/sab/types.js +1 -0
- package/dist/core/src/safety/bounds.js +71 -0
- package/dist/core/src/safety/json.js +70 -0
- package/dist/core/src/safety/path.js +151 -0
- package/dist/core/src/save/binary-format.js +95 -0
- package/dist/core/src/save/game-save-system.js +101 -0
- package/dist/core/src/save/grid-save-system.js +102 -0
- package/dist/core/src/save/migrate.js +35 -0
- package/dist/core/src/save/migration-registry.js +66 -0
- package/dist/core/src/save/persist-types.js +4 -0
- package/dist/core/src/save/schema.js +36 -0
- package/dist/core/src/save/serializer.js +139 -0
- package/dist/core/src/scene/builder.js +31 -0
- package/dist/core/src/scene/camera.js +118 -0
- package/dist/core/src/scene/checkpoint.js +84 -0
- package/dist/core/src/scene/game-loop.js +155 -0
- package/dist/core/src/scene/index.js +14 -0
- package/dist/core/src/scene/layer.js +7 -0
- package/dist/core/src/scene/prefab.js +97 -0
- package/dist/core/src/scene/scene-manager.js +141 -0
- package/dist/core/src/scene/scene.js +147 -0
- package/dist/core/src/scene/sky.js +179 -0
- package/dist/core/src/scene/spatial-grid.js +140 -0
- package/dist/core/src/scene/streaming.js +247 -0
- package/dist/core/src/scene/vegetation.js +107 -0
- package/dist/core/src/scene/world.js +66 -0
- package/dist/core/src/scripting/binding.js +85 -0
- package/dist/core/src/scripting/hot-reload.js +123 -0
- package/dist/core/src/scripting/script.js +68 -0
- package/dist/core/src/sim/hot-reload-pipeline.js +65 -0
- package/dist/core/src/sim/sim-state-helper.js +40 -0
- package/dist/core/src/sim/transient-state-registry.js +63 -0
- package/dist/core/src/sim/types.js +4 -0
- package/dist/core/src/sim/worker-loop.js +168 -0
- package/dist/core/src/stores/game-store.js +81 -0
- package/dist/core/src/stores/hot-reload-store.js +19 -0
- package/dist/core/src/telemetry/collector.js +303 -0
- package/dist/core/src/telemetry/gc-controller.js +365 -0
- package/dist/core/src/telemetry/gc-profiler.js +82 -0
- package/dist/core/src/telemetry/gc-tracker.js +63 -0
- package/dist/core/src/telemetry/gpu-profiler.js +554 -0
- package/dist/core/src/telemetry/gpu-resource-tracker.js +234 -0
- package/dist/core/src/telemetry/gpu-timer-pool.js +223 -0
- package/dist/core/src/telemetry/gpu-timer.js +117 -0
- package/dist/core/src/telemetry/gpu-timestamp-gate.js +22 -0
- package/dist/core/src/telemetry/reporter.js +56 -0
- package/dist/core/src/types/engine-types.js +21 -0
- package/dist/core/src/ui-font-scale.js +111 -0
- package/dist/core/src/util/broad-phase-grid.js +77 -0
- package/dist/core/src/util/feature-log.js +176 -0
- package/dist/core/src/util/index.js +4 -0
- package/dist/core/src/util/logger.js +596 -0
- package/dist/core/src/version.js +11 -0
- package/dist/core/src/webgpu-destroy-augmentation.d.ts +25 -0
- package/dist/core/src/worker/base-worker-host.js +212 -0
- package/dist/core/src/worker/buffer-sync.js +454 -0
- package/dist/core/src/worker/crash-recovery.js +153 -0
- package/dist/core/src/worker/entity-sim-worker-host.js +128 -0
- package/dist/core/src/worker/instrumented-worker-host.js +139 -0
- package/dist/core/src/worker/rpc.js +244 -0
- package/dist/core/src/worker/sim-worker-base.js +558 -0
- package/dist/core/src/worker/sim-worker-host.js +271 -0
- package/dist/core/src/worker/task-pool.js +205 -0
- package/dist/core/src/worker/task-worker.js +77 -0
- package/dist/libraries/asset-browser/src/index.js +10 -0
- package/dist/libraries/asset-browser/src/software-thumbnails.js +940 -0
- package/dist/libraries/audio-kira/src/backend.js +405 -0
- package/dist/libraries/audio-kira/src/index.js +4 -0
- package/dist/libraries/audio-kira/src/library.js +54 -0
- package/dist/libraries/blitz-ui/src/index.js +13 -0
- package/dist/libraries/blitz-ui/src/native-module.js +162 -0
- package/dist/libraries/blitz-ui/src/native-osr-ffi.js +409 -0
- package/dist/libraries/blitz-ui/src/router.js +82 -0
- package/dist/libraries/blitz-ui/src/shaders/ui-blit.wgsl.js +32 -0
- package/dist/libraries/blitz-ui/src/ui-blit-pass.js +108 -0
- package/dist/libraries/character/src/character-animator.js +323 -0
- package/dist/libraries/character/src/customization.js +464 -0
- package/dist/libraries/character/src/index.js +19 -0
- package/dist/libraries/character/src/inspectable.js +77 -0
- package/dist/libraries/character/src/loader.js +336 -0
- package/dist/libraries/character/src/locomotion.js +84 -0
- package/dist/libraries/character/src/preview.js +280 -0
- package/dist/libraries/devtools/src/backend.js +449 -0
- package/dist/libraries/devtools/src/cdp-bridge.js +293 -0
- package/dist/libraries/devtools/src/collectors.js +318 -0
- package/dist/libraries/devtools/src/index.js +31 -0
- package/dist/libraries/devtools/src/native-providers.js +749 -0
- package/dist/libraries/devtools/src/snapshot.js +35 -0
- package/dist/libraries/devtools/src/verify.js +139 -0
- package/dist/libraries/devtools/src/web/browser.js +94 -0
- package/dist/libraries/devtools/src/web/host.js +183 -0
- package/dist/libraries/devtools/src/web/mirror.js +26 -0
- package/dist/libraries/devtools/src/web/server.js +187 -0
- package/dist/libraries/entities/src/index.js +3 -0
- package/dist/libraries/entities/src/library.js +46 -0
- package/dist/libraries/entities/src/model-renderer.js +1307 -0
- package/dist/libraries/entities/src/shaders/model.wgsl +375 -0
- package/dist/libraries/gamepad/src/feed-input.js +54 -0
- package/dist/libraries/gamepad/src/filters.js +44 -0
- package/dist/libraries/gamepad/src/glyphs.js +83 -0
- package/dist/libraries/gamepad/src/hub.js +164 -0
- package/dist/libraries/gamepad/src/index.js +10 -0
- package/dist/libraries/gamepad/src/library.js +56 -0
- package/dist/libraries/gamepad/src/source-sab.js +62 -0
- package/dist/libraries/gamepad/src/sysfs-enrich.js +125 -0
- package/dist/libraries/gaussian-splats/src/gpu-sort.js +506 -0
- package/dist/libraries/gaussian-splats/src/index.js +8 -0
- package/dist/libraries/gaussian-splats/src/library.js +44 -0
- package/dist/libraries/gaussian-splats/src/parser.js +297 -0
- package/dist/libraries/gaussian-splats/src/renderer.js +368 -0
- package/dist/libraries/gaussian-splats/src/sh-eval.js +196 -0
- package/dist/libraries/gaussian-splats/src/sorter.js +76 -0
- package/dist/libraries/gaussian-splats/src/tile-raster.js +424 -0
- package/dist/libraries/gpu-kernels/src/index.js +8 -0
- package/dist/libraries/gpu-kernels/src/kernel.js +591 -0
- package/dist/libraries/gpu-kernels/src/transpile.js +896 -0
- package/dist/libraries/html-ui-kit/src/behaviors.js +231 -0
- package/dist/libraries/html-ui-kit/src/components.js +219 -0
- package/dist/libraries/html-ui-kit/src/index.js +17 -0
- package/dist/libraries/html-ui-kit/src/nav/nav-controller.js +215 -0
- package/dist/libraries/html-ui-kit/src/nav/nav-input.js +138 -0
- package/dist/libraries/html-ui-kit/src/nav/osk.js +135 -0
- package/dist/libraries/html-ui-kit/src/nav/router.js +152 -0
- package/dist/libraries/html-ui-kit/src/nav/vcursor.js +177 -0
- package/dist/libraries/html-ui-kit/src/ten-foot.js +138 -0
- package/dist/libraries/html-ui-kit/src/theme.js +235 -0
- package/dist/libraries/inspectable/src/index.js +6 -0
- package/dist/libraries/inspectable/src/inspectable.js +205 -0
- package/dist/libraries/lighting/src/index.js +4 -0
- package/dist/libraries/lighting/src/library.js +48 -0
- package/dist/libraries/lighting/src/light-system.js +395 -0
- package/dist/libraries/lighting/src/lighting-system.js +57 -0
- package/dist/libraries/lighting/src/shaders/light-debug.wgsl +30 -0
- package/dist/libraries/marching-cubes/src/chunked-field.js +130 -0
- package/dist/libraries/marching-cubes/src/deformation.js +41 -0
- package/dist/libraries/marching-cubes/src/generator.js +356 -0
- package/dist/libraries/marching-cubes/src/index.js +13 -0
- package/dist/libraries/marching-cubes/src/library.js +54 -0
- package/dist/libraries/marching-cubes/src/lod.js +95 -0
- package/dist/libraries/marching-cubes/src/marching-tetrahedra.js +261 -0
- package/dist/libraries/marching-cubes/src/sab.js +40 -0
- package/dist/libraries/marching-cubes/src/streaming-config.js +28 -0
- package/dist/libraries/marching-cubes/src/streaming-manager.js +490 -0
- package/dist/libraries/marching-cubes/src/tables.js +314 -0
- package/dist/libraries/marching-cubes/src/types.js +1 -0
- package/dist/libraries/models/src/bake-node-transforms.js +146 -0
- package/dist/libraries/models/src/bind-unskinned-meshes.js +145 -0
- package/dist/libraries/models/src/codecs/basisu-codec.js +134 -0
- package/dist/libraries/models/src/codecs/draco-codec.js +236 -0
- package/dist/libraries/models/src/codecs/draco3dgltf.d.ts +40 -0
- package/dist/libraries/models/src/codecs/index.js +5 -0
- package/dist/libraries/models/src/codecs/meshopt-codec.js +59 -0
- package/dist/libraries/models/src/codecs/registry.js +111 -0
- package/dist/libraries/models/src/dae.js +665 -0
- package/dist/libraries/models/src/default-part-selection.js +58 -0
- package/dist/libraries/models/src/dxf.js +235 -0
- package/dist/libraries/models/src/fbx/index.js +438 -0
- package/dist/libraries/models/src/fbx/interpreter/animation.js +369 -0
- package/dist/libraries/models/src/fbx/interpreter/blend-shapes.js +283 -0
- package/dist/libraries/models/src/fbx/interpreter/connections.js +158 -0
- package/dist/libraries/models/src/fbx/interpreter/diagnostics.js +48 -0
- package/dist/libraries/models/src/fbx/interpreter/geometry.js +430 -0
- package/dist/libraries/models/src/fbx/interpreter/global-settings.js +77 -0
- package/dist/libraries/models/src/fbx/interpreter/materials.js +231 -0
- package/dist/libraries/models/src/fbx/interpreter/nodes.js +168 -0
- package/dist/libraries/models/src/fbx/interpreter/skeleton.js +454 -0
- package/dist/libraries/models/src/fbx/parsers/binary-parser.js +217 -0
- package/dist/libraries/models/src/fbx/types.js +88 -0
- package/dist/libraries/models/src/gltf-extensions.js +121 -0
- package/dist/libraries/models/src/gltf.js +724 -0
- package/dist/libraries/models/src/index.js +46 -0
- package/dist/libraries/models/src/library.js +44 -0
- package/dist/libraries/models/src/loader.js +218 -0
- package/dist/libraries/models/src/material-adapter.js +82 -0
- package/dist/libraries/models/src/mini-xml.js +145 -0
- package/dist/libraries/models/src/normalize.js +168 -0
- package/dist/libraries/models/src/obj.js +247 -0
- package/dist/libraries/models/src/off.js +94 -0
- package/dist/libraries/models/src/ply.js +329 -0
- package/dist/libraries/models/src/pose-sampler.js +183 -0
- package/dist/libraries/models/src/sidecar/blender-extras.js +32 -0
- package/dist/libraries/models/src/sidecar/ddmeta.js +98 -0
- package/dist/libraries/models/src/sidecar/godot-import.js +76 -0
- package/dist/libraries/models/src/sidecar/resolver.js +112 -0
- package/dist/libraries/models/src/sidecar/types.js +8 -0
- package/dist/libraries/models/src/sidecar/unity-meta.js +46 -0
- package/dist/libraries/models/src/skeleton-synthesis.js +122 -0
- package/dist/libraries/models/src/stl.js +129 -0
- package/dist/libraries/models/src/threeds.js +312 -0
- package/dist/libraries/models/src/threemf.js +203 -0
- package/dist/libraries/models/src/types.js +33 -0
- package/dist/libraries/models/src/vtk.js +242 -0
- package/dist/libraries/navmesh/src/crowd-system.js +207 -0
- package/dist/libraries/navmesh/src/debug-viz.js +52 -0
- package/dist/libraries/navmesh/src/index.js +7 -0
- package/dist/libraries/navmesh/src/library.js +47 -0
- package/dist/libraries/navmesh/src/navmesh-generator.js +364 -0
- package/dist/libraries/navmesh/src/navmesh.js +141 -0
- package/dist/libraries/navmesh/src/pathfinder.js +221 -0
- package/dist/libraries/navmesh/src/types.js +1 -0
- package/dist/libraries/networking/src/authority.js +107 -0
- package/dist/libraries/networking/src/connection.js +107 -0
- package/dist/libraries/networking/src/discovery.js +314 -0
- package/dist/libraries/networking/src/enhanced-replication.js +282 -0
- package/dist/libraries/networking/src/epic-adapter.js +101 -0
- package/dist/libraries/networking/src/index.js +16 -0
- package/dist/libraries/networking/src/library.js +45 -0
- package/dist/libraries/networking/src/lobby.js +84 -0
- package/dist/libraries/networking/src/platform-adapter.js +92 -0
- package/dist/libraries/networking/src/rcs-adapter.js +100 -0
- package/dist/libraries/networking/src/remote-input.js +162 -0
- package/dist/libraries/networking/src/replication.js +273 -0
- package/dist/libraries/networking/src/rpc.js +172 -0
- package/dist/libraries/networking/src/session.js +129 -0
- package/dist/libraries/networking/src/transport.js +156 -0
- package/dist/libraries/networking/src/webrtc.js +266 -0
- package/dist/libraries/pathfinding-2d/src/astar-grid.js +255 -0
- package/dist/libraries/pathfinding-2d/src/index.js +9 -0
- package/dist/libraries/persistence/src/autosave-manager.js +94 -0
- package/dist/libraries/persistence/src/binary-record-store.js +205 -0
- package/dist/libraries/persistence/src/browser.js +16 -0
- package/dist/libraries/persistence/src/file-save-store.js +507 -0
- package/dist/libraries/persistence/src/hash-utils.js +54 -0
- package/dist/libraries/persistence/src/index.js +17 -0
- package/dist/libraries/persistence/src/library.js +51 -0
- package/dist/libraries/persistence/src/mock-opfs.js +114 -0
- package/dist/libraries/persistence/src/node-fs-directory-handle.js +168 -0
- package/dist/libraries/persistence/src/opfs-save-store.js +584 -0
- package/dist/libraries/persistence/src/save-worker-proxy.js +96 -0
- package/dist/libraries/persistence/src/save-worker.js +76 -0
- package/dist/libraries/persistence/src/xxh3-ts.d.ts +10 -0
- package/dist/libraries/physics-native/src/backend.js +960 -0
- package/dist/libraries/physics-native/src/broadphase.js +79 -0
- package/dist/libraries/physics-native/src/index.js +6 -0
- package/dist/libraries/physics-native/src/library.js +57 -0
- package/dist/libraries/physics-native/src/narrowphase-shapes.js +348 -0
- package/dist/libraries/physics-native/src/narrowphase.js +60 -0
- package/dist/libraries/physics-native/src/solver.js +105 -0
- package/dist/libraries/physics-native/src/types.js +4 -0
- package/dist/libraries/physics-rapier/src/api.js +352 -0
- package/dist/libraries/physics-rapier/src/auto-backend.js +54 -0
- package/dist/libraries/physics-rapier/src/backend.js +1066 -0
- package/dist/libraries/physics-rapier/src/bulk-ops.js +46 -0
- package/dist/libraries/physics-rapier/src/ffi-backend.js +17 -0
- package/dist/libraries/physics-rapier/src/ffi-lib.js +605 -0
- package/dist/libraries/physics-rapier/src/index.js +9 -0
- package/dist/libraries/physics-rapier/src/library.js +71 -0
- package/dist/libraries/physics-rapier/src/rapier-backend.js +1102 -0
- package/dist/libraries/physics-rapier/src/realm-worker-script.js +233 -0
- package/dist/libraries/postfx/src/index.js +14 -0
- package/dist/libraries/postfx/src/library.js +207 -0
- package/dist/libraries/postfx/src/post-process-stack.js +1641 -0
- package/dist/libraries/postfx/src/shaders/post-process/afterimage.wgsl +14 -0
- package/dist/libraries/postfx/src/shaders/post-process/ascii.wgsl +24 -0
- package/dist/libraries/postfx/src/shaders/post-process/blit.wgsl +12 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-blur.wgsl +21 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-bright.wgsl +15 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-composite.wgsl +14 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-downsample.wgsl +60 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-soft.wgsl +46 -0
- package/dist/libraries/postfx/src/shaders/post-process/bloom-upsample.wgsl +51 -0
- package/dist/libraries/postfx/src/shaders/post-process/channel-mixer.wgsl +36 -0
- package/dist/libraries/postfx/src/shaders/post-process/chromatic-aberration.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/dithering.wgsl +64 -0
- package/dist/libraries/postfx/src/shaders/post-process/dof.wgsl +131 -0
- package/dist/libraries/postfx/src/shaders/post-process/edges.wgsl +45 -0
- package/dist/libraries/postfx/src/shaders/post-process/fullscreen-vs.wgsl +15 -0
- package/dist/libraries/postfx/src/shaders/post-process/fxaa.wgsl +102 -0
- package/dist/libraries/postfx/src/shaders/post-process/gaussian-blur.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/glow-blur.wgsl +33 -0
- package/dist/libraries/postfx/src/shaders/post-process/glow-composite.wgsl +25 -0
- package/dist/libraries/postfx/src/shaders/post-process/grain.wgsl +37 -0
- package/dist/libraries/postfx/src/shaders/post-process/halftone.wgsl +80 -0
- package/dist/libraries/postfx/src/shaders/post-process/highlight-blur.wgsl +33 -0
- package/dist/libraries/postfx/src/shaders/post-process/highlight-composite.wgsl +32 -0
- package/dist/libraries/postfx/src/shaders/post-process/lens-distortion.wgsl +43 -0
- package/dist/libraries/postfx/src/shaders/post-process/lens-flare.wgsl +68 -0
- package/dist/libraries/postfx/src/shaders/post-process/lut.wgsl +30 -0
- package/dist/libraries/postfx/src/shaders/post-process/motion-blur.wgsl +55 -0
- package/dist/libraries/postfx/src/shaders/post-process/occluder-chunk.wgsl +28 -0
- package/dist/libraries/postfx/src/shaders/post-process/outline.wgsl +51 -0
- package/dist/libraries/postfx/src/shaders/post-process/pixelation.wgsl +47 -0
- package/dist/libraries/postfx/src/shaders/post-process/sharpen.wgsl +32 -0
- package/dist/libraries/postfx/src/shaders/post-process/sobel.wgsl +26 -0
- package/dist/libraries/postfx/src/shaders/post-process/split-tone.wgsl +36 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao-blur.wgsl +38 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao-composite.wgsl +25 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssao.wgsl +147 -0
- package/dist/libraries/postfx/src/shaders/post-process/ssr.wgsl +155 -0
- package/dist/libraries/postfx/src/shaders/post-process/taa.wgsl +98 -0
- package/dist/libraries/postfx/src/shaders/post-process/tonemap.wgsl +40 -0
- package/dist/libraries/postfx/src/shaders/post-process/watercolor.wgsl +82 -0
- package/dist/libraries/postfx/src/shaders/post-process/white-balance.wgsl +41 -0
- package/dist/libraries/recast/src/backend.js +124 -0
- package/dist/libraries/recast/src/crowd-system.js +310 -0
- package/dist/libraries/recast/src/index.js +4 -0
- package/dist/libraries/recast/src/library.js +59 -0
- package/dist/libraries/recast/src/types.js +4 -0
- package/dist/libraries/sand/src/cell.js +32 -0
- package/dist/libraries/sand/src/fields.js +27 -0
- package/dist/libraries/sand/src/fluid-grid.js +267 -0
- package/dist/libraries/sand/src/grid-dims.js +47 -0
- package/dist/libraries/sand/src/grid-sim-buffer.js +184 -0
- package/dist/libraries/sand/src/index.js +20 -0
- package/dist/libraries/sand/src/library.js +67 -0
- package/dist/libraries/sand/src/materials.js +437 -0
- package/dist/libraries/sand/src/palette.js +87 -0
- package/dist/libraries/sand/src/particles.js +163 -0
- package/dist/libraries/sand/src/render/sand-grid-pass.js +377 -0
- package/dist/libraries/sand/src/rng.js +44 -0
- package/dist/libraries/sand/src/rules/builtin-rules.js +125 -0
- package/dist/libraries/sand/src/rules/rule-compiler.js +164 -0
- package/dist/libraries/sand/src/rules/rule-engine.js +210 -0
- package/dist/libraries/sand/src/rules/rule-types.js +27 -0
- package/dist/libraries/sand/src/sand-step-pool.js +458 -0
- package/dist/libraries/sand/src/sand-step-worker.js +101 -0
- package/dist/libraries/sand/src/sand-world.js +3382 -0
- package/dist/libraries/stickman/src/index.js +7 -0
- package/dist/libraries/stickman/src/library.js +44 -0
- package/dist/libraries/stickman/src/proportions.js +40 -0
- package/dist/libraries/stickman/src/shader.js +113 -0
- package/dist/libraries/stickman/src/skeleton.js +163 -0
- package/dist/libraries/stickman/src/stickman-pass.js +210 -0
- package/dist/libraries/stickman/src/thick-line.js +120 -0
- package/dist/libraries/surface-nets/src/chunked-field.js +5 -0
- package/dist/libraries/surface-nets/src/deformation.js +29 -0
- package/dist/libraries/surface-nets/src/index.js +15 -0
- package/dist/libraries/surface-nets/src/library.js +41 -0
- package/dist/libraries/surface-nets/src/lod.js +6 -0
- package/dist/libraries/surface-nets/src/sab.js +4 -0
- package/dist/libraries/surface-nets/src/streaming-config.js +4 -0
- package/dist/libraries/surface-nets/src/streaming-manager.js +9 -0
- package/dist/libraries/surface-nets/src/surface-nets.js +361 -0
- package/dist/libraries/surface-nets/src/types.js +1 -0
- package/dist/libraries/water/src/buoyancy.js +80 -0
- package/dist/libraries/water/src/index.js +12 -0
- package/dist/libraries/water/src/library.js +65 -0
- package/dist/libraries/water/src/shore-damping.js +70 -0
- package/dist/libraries/water/src/water-buffer.js +78 -0
- package/dist/libraries/water/src/water-chunks.js +5 -0
- package/dist/libraries/water/src/water-physics.js +315 -0
- package/dist/libraries/water/src/water-renderer.js +736 -0
- package/dist/libraries/water/src/water-sab.js +138 -0
- package/dist/libraries/water/src/wave-sources.js +45 -0
- package/dist/libraries/weather/src/index.js +5 -0
- package/dist/libraries/weather/src/library.js +48 -0
- package/dist/libraries/weather/src/types.js +29 -0
- package/dist/libraries/weather/src/weather-blend.js +108 -0
- package/dist/libraries/weather/src/weather-system.js +275 -0
- package/dist/libraries/weatherfx/src/cloud-provider.js +5 -0
- package/dist/libraries/weatherfx/src/cloud-system.js +294 -0
- package/dist/libraries/weatherfx/src/index.js +4 -0
- package/dist/libraries/weatherfx/src/library.js +41 -0
- package/dist/libraries/weatherfx/src/particle-system.js +350 -0
- package/dist/libraries/weatherfx/src/shaders/cloud.wgsl +111 -0
- package/dist/libraries/weatherfx/src/shaders/particle-compute.wgsl +187 -0
- package/dist/libraries/weatherfx/src/shaders/particle-render.wgsl +95 -0
- package/dist/mcp/src/client.js +669 -0
- package/dist/mcp/src/engine-context.js +99 -0
- package/dist/mcp/src/http-transport.js +423 -0
- package/dist/mcp/src/index.js +5 -0
- package/dist/mcp/src/prompts/add-entity.js +39 -0
- package/dist/mcp/src/prompts/create-scene.js +41 -0
- package/dist/mcp/src/prompts/debug-frame.js +36 -0
- package/dist/mcp/src/prompts/index.js +10 -0
- package/dist/mcp/src/resources/asset-list.js +28 -0
- package/dist/mcp/src/resources/checkpoint-list.js +29 -0
- package/dist/mcp/src/resources/entity-state.js +45 -0
- package/dist/mcp/src/resources/gpu-info.js +90 -0
- package/dist/mcp/src/resources/index.js +51 -0
- package/dist/mcp/src/resources/performance.js +33 -0
- package/dist/mcp/src/resources/scene-tree.js +37 -0
- package/dist/mcp/src/server.js +307 -0
- package/dist/mcp/src/tools/animation.js +102 -0
- package/dist/mcp/src/tools/asset.js +146 -0
- package/dist/mcp/src/tools/audio.js +80 -0
- package/dist/mcp/src/tools/build.js +128 -0
- package/dist/mcp/src/tools/camera.js +157 -0
- package/dist/mcp/src/tools/checkpoint.js +90 -0
- package/dist/mcp/src/tools/component.js +176 -0
- package/dist/mcp/src/tools/debug.js +273 -0
- package/dist/mcp/src/tools/entity.js +309 -0
- package/dist/mcp/src/tools/inspect.js +192 -0
- package/dist/mcp/src/tools/lighting.js +239 -0
- package/dist/mcp/src/tools/material.js +225 -0
- package/dist/mcp/src/tools/mesh.js +210 -0
- package/dist/mcp/src/tools/physics.js +251 -0
- package/dist/mcp/src/tools/scene.js +139 -0
- package/dist/mcp/src/tools/script.js +114 -0
- package/dist/mcp/src/types.js +9 -0
- package/dist/mcp/src/undo-redo.js +64 -0
- package/dist/modules/camera-controls/src/index.js +116 -0
- package/dist/modules/controller-ui/src/index.js +149 -0
- package/dist/modules/devtools/src/api.js +317 -0
- package/dist/modules/devtools/src/asset-utils.js +102 -0
- package/dist/modules/devtools/src/bind-debug-store.js +34 -0
- package/dist/modules/devtools/src/blitz/host.js +391 -0
- package/dist/modules/devtools/src/blitz/module.js +65 -0
- package/dist/modules/devtools/src/blitz/panels/console.js +217 -0
- package/dist/modules/devtools/src/blitz/panels/elements.js +79 -0
- package/dist/modules/devtools/src/blitz/panels/gpu.js +61 -0
- package/dist/modules/devtools/src/blitz/panels/performance.js +113 -0
- package/dist/modules/devtools/src/blitz/panels/snapshot.js +141 -0
- package/dist/modules/devtools/src/blitz/panels/types.js +55 -0
- package/dist/modules/devtools/src/blitz/theme.js +167 -0
- package/dist/modules/devtools/src/data-bridge.js +247 -0
- package/dist/modules/devtools/src/debug-overlay.js +181 -0
- package/dist/modules/devtools/src/debug-raycast.js +296 -0
- package/dist/modules/devtools/src/debug-store.js +28 -0
- package/dist/modules/devtools/src/debug-view-descriptors.js +24 -0
- package/dist/modules/devtools/src/doctor-panel.js +163 -0
- package/dist/modules/devtools/src/gizmo-module.js +95 -0
- package/dist/modules/devtools/src/graph-model.js +387 -0
- package/dist/modules/devtools/src/grid-renderer.js +145 -0
- package/dist/modules/devtools/src/height-ruler-renderer.js +189 -0
- package/dist/modules/devtools/src/index.js +46 -0
- package/dist/modules/devtools/src/init.js +204 -0
- package/dist/modules/devtools/src/label-overlay.js +129 -0
- package/dist/modules/devtools/src/material-stats-panel.js +171 -0
- package/dist/modules/devtools/src/profiling-bridge.js +288 -0
- package/dist/modules/devtools/src/profiling-hooks.js +63 -0
- package/dist/modules/devtools/src/renderer-adapter.js +61 -0
- package/dist/modules/devtools/src/scene-inspector.js +571 -0
- package/dist/modules/devtools/src/scene-store.js +212 -0
- package/dist/modules/devtools/src/scene-sync.js +92 -0
- package/dist/modules/devtools/src/shaders/transform-gizmo.wgsl +38 -0
- package/dist/modules/devtools/src/sim-stats-panel.js +250 -0
- package/dist/modules/devtools/src/sim-stats-provider.js +73 -0
- package/dist/modules/devtools/src/skeleton-renderer.js +326 -0
- package/dist/modules/devtools/src/transform-gizmo.js +677 -0
- package/dist/modules/devtools/src/types.js +5 -0
- package/dist/modules/devtools/src/worker-expose.js +59 -0
- package/dist/modules/devtools/src/worker-sync.js +115 -0
- package/dist/modules/editor/src/commands/component.js +151 -0
- package/dist/modules/editor/src/commands/entity.js +253 -0
- package/dist/modules/editor/src/commands/hierarchy.js +79 -0
- package/dist/modules/editor/src/commands/index.js +57 -0
- package/dist/modules/editor/src/commands/prefab.js +149 -0
- package/dist/modules/editor/src/commands/registry.js +246 -0
- package/dist/modules/editor/src/commands/scene-io.js +206 -0
- package/dist/modules/editor/src/commands/selection.js +71 -0
- package/dist/modules/editor/src/commands/transform.js +88 -0
- package/dist/modules/editor/src/ddscene.js +189 -0
- package/dist/modules/editor/src/document.js +96 -0
- package/dist/modules/editor/src/editor-context.js +115 -0
- package/dist/modules/editor/src/index.js +11 -0
- package/dist/modules/editor/src/mcp.js +91 -0
- package/dist/modules/editor/src/module.js +115 -0
- package/dist/modules/editor/src/selection.js +60 -0
- package/dist/modules/editor/src/shell.js +397 -0
- package/dist/modules/editor/src/viewport.js +186 -0
- package/dist/modules/html-ui/src/composite.js +116 -0
- package/dist/modules/html-ui/src/create-html-ui.js +137 -0
- package/dist/modules/html-ui/src/doc-backend.js +518 -0
- package/dist/modules/html-ui/src/host.js +819 -0
- package/dist/modules/html-ui/src/index.js +5 -0
- package/dist/modules/html-ui/src/jsx-dev-runtime.js +9 -0
- package/dist/modules/html-ui/src/jsx-runtime.js +121 -0
- package/dist/modules/html-ui/src/protocol.js +6 -0
- package/dist/modules/html-ui/src/ui-worker.js +10 -0
- package/dist/modules/mcp/src/index.js +62 -0
- package/dist/modules/movement-2d/src/grid-character.js +345 -0
- package/dist/modules/movement-2d/src/index.js +27 -0
- package/dist/modules/movement-3d/src/character-motor.js +166 -0
- package/dist/modules/movement-3d/src/index.js +27 -0
- package/dist/modules/native-osr/src/index.js +12 -0
- package/dist/modules/native-osr/src/input-router.js +335 -0
- package/dist/modules/native-osr/src/native-osr-manager.js +210 -0
- package/dist/modules/native-osr/src/native-texture-source.js +66 -0
- package/dist/modules/native-osr/src/types.js +8 -0
- package/dist/modules/native-osr/src/world-space-ui-pass.js +313 -0
- package/dist/modules/sailing/src/index.js +39 -0
- package/dist/modules/terrain/src/index.js +44 -0
- package/dist/modules/vitals/src/index.js +25 -0
- package/dist/modules/vitals/src/vitals.js +195 -0
- package/dist/modules/xr/src/camera-rig.js +119 -0
- package/dist/modules/xr/src/debug.js +45 -0
- package/dist/modules/xr/src/frame-loop.js +139 -0
- package/dist/modules/xr/src/index.js +10 -0
- package/dist/modules/xr/src/input.js +164 -0
- package/dist/modules/xr/src/layer.js +100 -0
- package/dist/modules/xr/src/session.js +133 -0
- package/dist/modules/xr/src/sim-module.js +21 -0
- package/dist/modules/xr/src/types.js +4 -0
- package/dist/modules/xr/src/webxr.d.ts +182 -0
- package/dist/modules/xr/src/xr-module.js +134 -0
- package/dist/shader-graph/src/chunks.js +340 -0
- package/dist/shader-graph/src/compiler.js +639 -0
- package/dist/shader-graph/src/compute-compiler.js +384 -0
- package/dist/shader-graph/src/compute-graph.js +55 -0
- package/dist/shader-graph/src/compute-profiles.js +39 -0
- package/dist/shader-graph/src/graph.js +38 -0
- package/dist/shader-graph/src/index.js +15 -0
- package/dist/shader-graph/src/profiles.js +305 -0
- package/dist/shader-graph/src/validator.js +42 -0
- package/dist/shader-graph/src/wgsl-binding-validator.js +110 -0
- package/dist/shader-graph/src/wgsl-struct-validator.js +222 -0
- package/dist/shader-graph/src/wgsl-struct.js +251 -0
- package/dist/test/src/index.js +1 -0
- package/dist/test/src/vision-test.js +228 -0
- package/libraries/asset-browser/src/software-thumbnails.ts +2 -2
- package/libraries/audio-kira/src/backend.ts +2 -1
- package/libraries/blitz-ui/native/src/shell.rs +35 -1
- package/libraries/blitz-ui/native-osr/Cargo.toml +8 -0
- package/libraries/blitz-ui/native-osr/src/lib.rs +213 -22
- package/libraries/blitz-ui/native-osr/src/mutate.rs +109 -14
- package/libraries/blitz-ui/src/native-osr-ffi.ts +71 -8
- package/libraries/blitz-ui/src/router.ts +3 -1
- package/libraries/blitz-ui/src/ui-blit-pass.ts +1 -1
- package/libraries/character/src/character-animator.ts +1 -1
- package/libraries/character/src/customization.ts +513 -0
- package/libraries/character/src/index.ts +6 -0
- package/libraries/character/src/inspectable.ts +111 -0
- package/libraries/character/src/loader.ts +38 -13
- package/libraries/character/src/locomotion.ts +13 -1
- package/libraries/character/src/preview.ts +94 -24
- package/libraries/devtools/README.md +16 -18
- package/libraries/devtools/src/backend.ts +505 -0
- package/libraries/devtools/src/cdp-bridge.ts +8 -8
- package/libraries/devtools/src/collectors.ts +41 -37
- package/libraries/devtools/src/index.ts +32 -14
- package/libraries/devtools/src/native-providers.ts +20 -12
- package/libraries/devtools/src/snapshot.ts +77 -0
- package/libraries/devtools/src/web/host.ts +11 -12
- package/libraries/devtools/src/web/mirror.ts +14 -405
- package/libraries/devtools/src/web/server.ts +4 -3
- package/libraries/entities/src/model-renderer.ts +96 -23
- package/libraries/gamepad/src/feed-input.ts +1 -1
- package/libraries/gamepad/src/hub.ts +2 -2
- package/libraries/gamepad/src/library.ts +2 -1
- package/libraries/gamepad/src/source-sab.ts +2 -2
- package/libraries/gamepad/src/sysfs-enrich.ts +2 -2
- package/libraries/gpu-kernels/src/kernel.ts +2 -2
- package/libraries/gpu-kernels/src/transpile.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-controller.ts +1 -1
- package/libraries/html-ui-kit/src/nav/nav-input.ts +2 -2
- package/libraries/inspectable/src/index.ts +11 -0
- package/libraries/inspectable/src/inspectable.ts +277 -0
- package/libraries/marching-cubes/src/streaming-manager.ts +4 -4
- package/libraries/models/src/bind-unskinned-meshes.ts +131 -0
- package/libraries/models/src/index.ts +1 -0
- package/libraries/models/src/types.ts +22 -0
- package/libraries/networking/src/discovery.ts +8 -8
- package/libraries/persistence/src/autosave-manager.ts +1 -1
- package/libraries/persistence/src/binary-record-store.ts +1 -1
- package/libraries/persistence/src/file-save-store.ts +9 -9
- package/libraries/persistence/src/library.ts +1 -1
- package/libraries/persistence/src/opfs-save-store.ts +7 -7
- package/libraries/persistence/src/save-worker.ts +13 -14
- package/libraries/physics-rapier/src/ffi-lib.ts +2 -1
- package/libraries/physics-rapier/src/realm-worker-script.ts +1 -1
- package/libraries/postfx/src/post-process-stack.ts +1 -1
- package/libraries/sand/src/sand-step-pool.ts +5 -0
- package/libraries/water/src/water-renderer.ts +1 -1
- package/libraries/weatherfx/src/cloud-system.ts +3 -3
- package/mcp/README.md +20 -0
- package/mcp/src/client.ts +75 -22
- package/mcp/src/undo-redo.ts +2 -2
- package/modules/controller-ui/src/index.ts +1 -1
- package/modules/devtools/src/blitz/host.ts +430 -0
- package/modules/devtools/src/blitz/module.ts +101 -0
- package/modules/devtools/src/blitz/panels/console.ts +223 -0
- package/modules/devtools/src/blitz/panels/elements.ts +92 -0
- package/modules/devtools/src/blitz/panels/gpu.ts +70 -0
- package/modules/devtools/src/blitz/panels/performance.ts +120 -0
- package/modules/devtools/src/blitz/panels/snapshot.ts +152 -0
- package/modules/devtools/src/blitz/panels/types.ts +120 -0
- package/modules/devtools/src/blitz/theme.ts +168 -0
- package/modules/devtools/src/data-bridge.ts +1 -1
- package/modules/devtools/src/index.ts +6 -0
- package/modules/devtools/src/profiling-bridge.ts +17 -13
- package/modules/devtools/src/profiling-hooks.ts +22 -13
- package/modules/devtools/src/scene-inspector.ts +1 -1
- package/modules/devtools/src/worker-sync.ts +9 -1
- package/modules/editor/src/commands/registry.ts +2 -2
- package/modules/editor/src/module.ts +1 -1
- package/modules/html-ui/src/composite.ts +1 -1
- package/modules/html-ui/src/create-html-ui.ts +21 -9
- package/modules/html-ui/src/doc-backend.ts +189 -10
- package/modules/html-ui/src/host.ts +354 -40
- package/modules/html-ui/src/index.ts +1 -1
- package/modules/html-ui/src/jsx-runtime.ts +2 -2
- package/modules/html-ui/src/protocol.ts +53 -6
- package/modules/mcp/README.md +6 -0
- package/modules/mcp/src/index.ts +70 -3
- package/modules/movement-3d/src/character-motor.ts +83 -10
- package/package.json +838 -165
- package/shader-graph/README.md +25 -0
- package/test/README.md +16 -0
- package/libraries/blitz-ui/native-osr-spike.ts +0 -70
- package/libraries/devtools/native/Cargo.toml +0 -12
- package/libraries/devtools/native/src/lib.rs +0 -1646
- package/libraries/devtools/native/src/panels/console.rs +0 -338
- package/libraries/devtools/native/src/panels/dom_tree.rs +0 -47
- package/libraries/devtools/native/src/panels/generic.rs +0 -317
- package/libraries/devtools/native/src/panels/gpu.rs +0 -107
- package/libraries/devtools/native/src/panels/input.rs +0 -88
- package/libraries/devtools/native/src/panels/mod.rs +0 -154
- package/libraries/devtools/native/src/panels/perf_metrics.rs +0 -91
- package/libraries/devtools/native/src/panels/perf_recorder.rs +0 -158
- package/libraries/devtools/native/src/panels/scene.rs +0 -72
- package/libraries/devtools/native/src/state.rs +0 -394
- package/libraries/devtools/src/egui-ffi.ts +0 -804
- package/libraries/devtools/src/egui-renderer.ts +0 -407
- package/libraries/devtools/src/host.ts +0 -363
- package/libraries/devtools/src/mirror.ts +0 -794
- package/libraries/models/test-fixtures/fbx/LICENSE-notice.txt +0 -24
- package/libraries/models/test-fixtures/fbx/README.md +0 -46
- package/libraries/models/test-fixtures/fbx/m01_cube_phong.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m02_geo_ngons.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m03_normals.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m04_material_properties.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m05_external_diffuse.png +0 -0
- package/libraries/models/test-fixtures/fbx/m05_textures.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m06_uv_transform.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m07_multimaterial.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m08_transforms.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m09_skinning.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m10_morph.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m11_node_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m12_skeletal_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m13_morph_anim.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m14_multiclip.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m15_camera_lights.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_axis_yup.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_axis_zup.fbx +0 -0
- package/libraries/models/test-fixtures/fbx/m16_units_254.fbx +0 -0
- package/libraries/physics-native/src/ffi-spike-worker.ts +0 -71
- package/libraries/physics-rapier/bench/ffi-bench.ts +0 -79
- package/libraries/physics-rapier/bench/wasm-baseline.ts +0 -90
|
@@ -0,0 +1,1641 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// PostProcess Stack — unified chainable WebGPU postprocessing
|
|
3
|
+
//
|
|
4
|
+
// All 21 effects: taa, ssao, ssr, dof, motion-blur, bloom, bloom-soft,
|
|
5
|
+
// tonemap, fxaa, sharpen, grain, sobel, edges, lens-flare, pixelation,
|
|
6
|
+
// gaussian-blur, afterimage, outline, highlight, glow, ascii.
|
|
7
|
+
//
|
|
8
|
+
// HDR section (taa → bloom-soft) uses rgba16float intermediates.
|
|
9
|
+
// tonemap is the HDR→LDR bridge.
|
|
10
|
+
// LDR section (fxaa → ascii) also uses rgba16float intermediates.
|
|
11
|
+
// A final blit copies the result to the canvas-format swapchain view.
|
|
12
|
+
// ============================================================================
|
|
13
|
+
import { createValidatedShaderModule } from "@downdraft/engine";
|
|
14
|
+
import { createLogger } from "@downdraft/engine/util/logger";
|
|
15
|
+
const AFTERIMAGE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, damp: f32, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var prevTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let cur = textureSample(colorTex, samp, input.uv);\n let prev = textureSample(prevTex, samp, input.uv);\n return mix(cur, prev, u.damp);\n}\n";
|
|
16
|
+
const ASCII_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, cellSize: f32, useColor: f32, screenW: f32, screenH: f32, _p0: f32, _p1: f32, _p2: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var glyphTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\nconst NUM_GLYPHS = 10.0;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let screen = vec2(u.screenW, u.screenH);\n let cellPx = u.cellSize;\n let cellCoord = floor(input.uv * screen / cellPx);\n let cellCenter = (cellCoord + vec2(0.5)) * cellPx / screen;\n let color = textureSample(colorTex, samp, cellCenter);\n let l = lum(color.rgb);\n let charIdx = clamp(floor(l * (NUM_GLYPHS - 1.0)), 0.0, NUM_GLYPHS - 1.0);\n let local = fract(input.uv * screen / cellPx);\n let glyphUV = vec2((charIdx + local.x) / NUM_GLYPHS, local.y);\n let glyph = textureSample(glyphTex, samp, glyphUV).r;\n let outColor = mix(vec3(0.0, 1.0, 0.0), color.rgb, u.useColor);\n return vec4(outColor * glyph, 1.0);\n}\n";
|
|
17
|
+
const BLIT_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, _p6: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n return textureSample(colorTex, samp, input.uv);\n}\n";
|
|
18
|
+
const BLOOM_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, dirX: f32, dirY: f32, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let dir = vec2(u.dirX, u.dirY) * u.texelSize;\n var sum = textureSample(colorTex, samp, uv).rgb * 0.227027;\n sum += textureSample(colorTex, samp, uv + dir * 1.0).rgb * 0.1945946;\n sum += textureSample(colorTex, samp, uv - dir * 1.0).rgb * 0.1945946;\n sum += textureSample(colorTex, samp, uv + dir * 2.0).rgb * 0.1216216;\n sum += textureSample(colorTex, samp, uv - dir * 2.0).rgb * 0.1216216;\n sum += textureSample(colorTex, samp, uv + dir * 3.0).rgb * 0.0675676;\n sum += textureSample(colorTex, samp, uv - dir * 3.0).rgb * 0.0675676;\n return vec4(sum, 1.0);\n}\n";
|
|
19
|
+
const BLOOM_BRIGHT_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, threshold: f32, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let c = textureSample(colorTex, samp, input.uv);\n let l = lum(c.rgb);\n if (l > u.threshold) { return c; }\n return vec4(0.0);\n}\n";
|
|
20
|
+
const BLOOM_COMPOSITE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, strength: f32, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var brightTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let c = textureSample(colorTex, samp, input.uv);\n let b = textureSample(brightTex, samp, input.uv);\n return vec4(c.rgb + b.rgb * u.strength, c.a);\n}\n";
|
|
21
|
+
const BLOOM_DOWNSAMPLE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Bloom downsample — high-quality 13-tap downsample with optional bright-pass.\n// When u.brightPass > 0.5, applies soft-threshold bright extraction (first level).\n// Otherwise, performs a plain downsample for subsequent MIP levels.\nstruct U {\n texelSize: vec2<f32>, // 1/srcW, 1/srcH (source resolution)\n brightPass: f32, // 1.0 = extract bright, 0.0 = plain downsample\n threshold: f32,\n softThreshold: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\nfn softThreshold(color: vec3<f32>) -> vec3<f32> {\n let luma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n let knee = u.threshold * u.softThreshold + 0.00001;\n let softFactor = clamp(luma - u.threshold + knee, 0.0, 2.0 * knee);\n let contribution = softFactor * softFactor / (4.0 * knee + 0.00001) + u.threshold - knee;\n let scale = max(contribution / max(luma, 0.00001), 0.0);\n return color * scale;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let ts = u.texelSize;\n\n // 13-tap downsample filter (from UE4 \"Next Generation Post Processing\" approach).\n // Samples a 3x3 + cross pattern for anti-aliased downsample.\n let a = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, -1.0) * ts).rgb;\n let b = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, -1.0) * ts).rgb;\n let c = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, -1.0) * ts).rgb;\n let d = textureSample(colorTex, samp, uv + vec2<f32>(-0.5, -0.5) * ts).rgb;\n let e = textureSample(colorTex, samp, uv + vec2<f32>( 0.5, -0.5) * ts).rgb;\n let f = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 0.0) * ts).rgb;\n let g = textureSample(colorTex, samp, uv).rgb;\n let h = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 0.0) * ts).rgb;\n let i = textureSample(colorTex, samp, uv + vec2<f32>(-0.5, 0.5) * ts).rgb;\n let j = textureSample(colorTex, samp, uv + vec2<f32>( 0.5, 0.5) * ts).rgb;\n let k = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 1.0) * ts).rgb;\n let l = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, 1.0) * ts).rgb;\n let m = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 1.0) * ts).rgb;\n\n // Weighted group downsample (2 groups of 6+1 for balanced anti-aliasing)\n let group1 = (a + c + f + h + k + m) * 0.2;\n let group2 = (b + d + e + g + i + j + l) * 0.18;\n var color = group1 + group2 + g * 0.15;\n\n if (u.brightPass > 0.5) {\n color = softThreshold(color);\n }\n return vec4<f32>(color, 1.0);\n}\n";
|
|
22
|
+
const BLOOM_SOFT_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Bloom (soft threshold) — bright pass with soft knee + separable blur.\n// When u.dirX > 1.0, performs the bright-pass extraction step.\n// Otherwise, performs a directional blur pass.\nstruct U {\n texelSize: vec2<f32>,\n dirX: f32,\n dirY: f32,\n threshold: f32,\n softThreshold: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let dir = vec2<f32>(u.dirX, u.dirY);\n\n if (u.dirX > 1.0) {\n let c = textureSample(colorTex, samp, uv).rgb;\n let luma = dot(c, vec3<f32>(0.299, 0.587, 0.114));\n let knee = u.threshold * u.softThreshold + 0.00001;\n let softFactor = clamp(luma - u.threshold + knee, 0.0, 2.0 * knee);\n let contribution = softFactor * softFactor / (4.0 * knee + 0.00001) + u.threshold - knee;\n let scale = max(contribution / max(luma, 0.00001), 0.0);\n return vec4<f32>(c * scale, 1.0);\n }\n\n let weights = array<f32, 5>(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);\n let offsets = array<f32, 5>(0.0, 1.3846154, 3.2307692, 5.176923, 7.1076923);\n var color = textureSample(colorTex, samp, uv).rgb * weights[0];\n for (var i = 1u; i < 5u; i = i + 1u) {\n let offset = dir * offsets[i] * u.texelSize;\n color += textureSample(colorTex, samp, uv + offset).rgb * weights[i];\n color += textureSample(colorTex, samp, uv - offset).rgb * weights[i];\n }\n return vec4<f32>(color, 1.0);\n}\n";
|
|
23
|
+
const BLOOM_UPSAMPLE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Bloom upsample — 9-tap bilinear-weighted upsample with additive blend + tint.\n// Reads a lower-resolution MIP and adds it to the higher-res target with weight.\n// Used in the upsample cascade: ⅛ → ¼ → ½ → full.\nstruct U {\n texelSize: vec2<f32>, // 1/srcW, 1/srcH (source = lower-res MIP)\n weight: f32, // blend weight for this MIP level\n _p0: f32,\n tintR: f32,\n tintG: f32,\n tintB: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>; // lower-res MIP\n@group(0) @binding(1) var baseTex: texture_2d<f32>; // higher-res target (for additive read)\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let ts = u.texelSize;\n\n // 9-tap tent filter for smooth upsample\n let weights = array<f32, 9>(\n 0.0625, 0.125, 0.0625,\n 0.125, 0.25, 0.125,\n 0.0625, 0.125, 0.0625,\n );\n let offsets = array<vec2<f32>, 9>(\n vec2<f32>(-1.0, -1.0), vec2<f32>( 0.0, -1.0), vec2<f32>( 1.0, -1.0),\n vec2<f32>(-1.0, 0.0), vec2<f32>( 0.0, 0.0), vec2<f32>( 1.0, 0.0),\n vec2<f32>(-1.0, 1.0), vec2<f32>( 0.0, 1.0), vec2<f32>( 1.0, 1.0),\n );\n\n var upsampled = vec3<f32>(0.0);\n for (var i = 0u; i < 9u; i = i + 1u) {\n upsampled += textureSample(colorTex, samp, uv + offsets[i] * ts).rgb * weights[i];\n }\n\n // Apply tint + weight\n let tint = vec3<f32>(u.tintR, u.tintG, u.tintB);\n let bloom = upsampled * tint * u.weight;\n\n // Additive blend with base (higher-res MIP or original scene)\n let base = textureSample(baseTex, samp, uv).rgb;\n return vec4<f32>(base + bloom, 1.0);\n}\n";
|
|
24
|
+
const CHANNEL_MIXER_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Channel mixer — per-channel R/G/B output weights + monochrome toggle.\n// Allows remapping color channels: output.R = wRR*R + wRG*G + wRB*B, etc.\nstruct U {\n wRR: f32, wRG: f32, wRB: f32, // red output weights\n wGR: f32, wGG: f32, wGB: f32, // green output weights\n wBR: f32, wBG: f32, wBB: f32, // blue output weights\n monochrome: f32, // 1.0 = output as grayscale\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let c = textureSample(colorTex, samp, uv).rgb;\n\n var outR = u.wRR * c.r + u.wRG * c.g + u.wRB * c.b;\n var outG = u.wGR * c.r + u.wGG * c.g + u.wGB * c.b;\n var outB = u.wBR * c.r + u.wBG * c.g + u.wBB * c.b;\n\n if (u.monochrome > 0.5) {\n let luma = outR * 0.299 + outG * 0.587 + outB * 0.114;\n outR = luma; outG = luma; outB = luma;\n }\n\n return vec4<f32>(vec3<f32>(outR, outG, outB), 1.0);\n}\n";
|
|
25
|
+
const CHROMATIC_ABERRATION_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Chromatic aberration — radial RGB channel shift with falloff from center.\n// Simulates lens chromatic aberration by offsetting R and B channels radially.\nstruct U {\n texelSize: vec2<f32>,\n intensity: f32, // overall strength (0–1)\n start: f32, // radial falloff start (0–1, normalized distance from center)\n end: f32, // radial falloff end (0–1)\n centerX: f32, // lens center offset X (0–1, 0.5 = center)\n centerY: f32, // lens center offset Y (0–1, 0.5 = center)\n _p0: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let center = vec2<f32>(u.centerX, u.centerY);\n let dir = uv - center;\n let dist = length(dir);\n // Radial falloff: no aberration near center, full at edges\n let falloff = smoothstep(u.start, u.end, dist);\n let strength = u.intensity * falloff;\n let offset = normalize(dir) * strength;\n\n // Sample R and B at offset positions, G at center\n let r = textureSample(colorTex, samp, uv + offset).r;\n let g = textureSample(colorTex, samp, uv).g;\n let b = textureSample(colorTex, samp, uv - offset).b;\n\n return vec4<f32>(r, g, b, 1.0);\n}\n";
|
|
26
|
+
const DITHERING_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Dithering — ordered (Bayer) + blue-noise modes to reduce color banding.\n// Applies a dithering pattern before quantization to break up smooth gradients.\nstruct U {\n texelSize: vec2<f32>,\n mode: f32, // 0 = off, 1 = Bayer 4x4, 2 = blue-noise\n strength: f32, // dithering intensity (0–1)\n levels: f32, // color quantization levels (0 = no quantization, e.g. 32)\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var noiseTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n// 4x4 Bayer matrix (threshold values 0–15, normalized to 0–1)\nfn bayer4(x: u32, y: u32) -> f32 {\n let m = array<u32, 16>(\n 0u, 8u, 2u, 10u,\n 12u, 4u, 14u, 6u,\n 3u, 11u, 1u, 9u,\n 15u, 7u, 13u, 5u,\n );\n return f32(m[(y % 4u) * 4u + (x % 4u)]) / 16.0;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n var color = textureSample(colorTex, samp, uv).rgb;\n\n if (u.mode < 0.5) {\n return vec4<f32>(color, 1.0);\n }\n\n let dims = vec2<f32>(1.0 / u.texelSize.x, 1.0 / u.texelSize.y);\n let pixelPos = vec2<u32>(u32(i32(uv.x * dims.x)), u32(i32(uv.y * dims.y)));\n\n var dither: f32;\n if (u.mode < 1.5) {\n // Bayer 4x4\n dither = bayer4(pixelPos.x, pixelPos.y) - 0.5;\n } else {\n // Blue noise\n let noiseUV = uv * dims / 128.0; // tile noise texture\n dither = textureSample(noiseTex, samp, noiseUV).r - 0.5;\n }\n\n // Apply dithering: add noise before quantization\n let dithered = color + dither * u.strength * (1.0 / max(u.levels, 1.0));\n\n if (u.levels > 0.5) {\n // Quantize to N levels per channel\n let q = u.levels;\n let quantized = floor(dithered * q + 0.5) / q;\n return vec4<f32>(clamp(quantized, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);\n }\n\n return vec4<f32>(clamp(dithered, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);\n}\n";
|
|
27
|
+
const DOF_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// DOF — depth-of-field with circle-of-confusion + bokeh-shaped disk sampling.\n// Computes CoC from depth, then gathers a rotated disk kernel with shape\n// support (circle / hexagon / octagon) and near/far field separation.\nstruct U {\n texelSize: vec2<f32>,\n focusDist: f32,\n focusRange: f32,\n maxBlur: f32,\n bokehShape: f32, // 0 = circle, 1 = hexagon, 2 = octagon\n sampleCount: f32, // kernel sample count (max 48)\n nearOnly: f32, // 1.0 = blur only near field, 0.0 = blur both\n farOnly: f32, // 1.0 = blur only far field, 0.0 = blur both\n bladeRotation: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var depthTex: texture_depth_2d;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n// Golden-angle spiral disk sampling — well-distributed kernel points.\n// Returns offset direction (unit) for sample index i of N.\nfn diskSample(i: u32, n: f32) -> vec2<f32> {\n let goldenAngle = 2.39996323;\n let r = sqrt((f32(i) + 0.5) / n);\n let theta = f32(i) * goldenAngle + u.bladeRotation;\n return vec2<f32>(cos(theta), sin(theta)) * r;\n}\n\n// Shape mask: modifies sample weight based on bokeh shape.\nfn shapeMask(dir: vec2<f32>) -> f32 {\n if (u.bokehShape < 0.5) {\n // Circle — no modification\n return 1.0;\n } else if (u.bokehShape < 1.5) {\n // Hexagon — clip to hexagonal aperture\n let a = atan2(dir.y, dir.x);\n let hex = abs(cos(a)) * 0.5 + abs(cos(a + 1.0471976)) * 0.5 + abs(cos(a - 1.0471976)) * 0.5;\n return smoothstep(0.45, 0.5, hex);\n } else {\n // Octagon — clip to octagonal aperture\n let a = atan2(dir.y, dir.x);\n let oct = abs(cos(a)) * 0.5 + abs(cos(a + 0.7853982)) * 0.5 + abs(cos(a - 0.7853982)) * 0.5;\n return smoothstep(0.42, 0.5, oct);\n }\n}\n\nfn computeCoC(depth: f32) -> f32 {\n // CoC: 0 at focus distance, grows linearly outside focus range, clamped to maxBlur.\n let diff = depth - u.focusDist;\n let coc = clamp(abs(diff) / u.focusRange, 0.0, 1.0) * u.maxBlur;\n // Sign: positive = far field, negative = near field\n return select(-coc, coc, diff > 0.0);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let depth = textureSample(depthTex, samp, uv);\n let sharp = textureSample(colorTex, samp, uv);\n let centerCoC = computeCoC(depth);\n\n // If CoC is negligible, return the sharp pixel directly\n if (abs(centerCoC) < 0.01) {\n return vec4<f32>(sharp.rgb, 1.0);\n }\n\n let n = u32(clamp(u.sampleCount, 4.0, 48.0));\n let nF = f32(n);\n let cocAbs = abs(centerCoC);\n let isNear = centerCoC < 0.0;\n\n // Field filtering\n if (u.nearOnly > 0.5 && !isNear) {\n return vec4<f32>(sharp.rgb, 1.0);\n }\n if (u.farOnly > 0.5 && isNear) {\n return vec4<f32>(sharp.rgb, 1.0);\n }\n\n var color = vec3<f32>(0.0);\n var totalWeight = 0.0;\n let dims = textureDimensions(colorTex);\n\n for (var i = 0u; i < 48u; i = i + 1u) {\n if (i >= n) { break; }\n let dir = diskSample(i, nF);\n let sampleOff = dir * cocAbs * u.texelSize;\n let sampleUV = uv + sampleOff;\n\n // Bounds check + textureLoad for non-uniform flow\n let coords = vec2<u32>(clamp(\n vec2<i32>(i32(sampleUV.x * f32(dims.x)), i32(sampleUV.y * f32(dims.y))),\n vec2<i32>(0, 0),\n vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let sampleColor = textureLoad(colorTex, coords, 0).rgb;\n\n // Sample CoC — use the larger of center and sample CoC for near field\n // (near field objects should bleed over far field)\n let dcoords = vec2<u32>(clamp(\n vec2<i32>(i32(sampleUV.x * f32(dims.x)), i32(sampleUV.y * f32(dims.y))),\n vec2<i32>(0, 0),\n vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let sampleDepth = textureLoad(depthTex, dcoords, 0);\n let sampleCoC = computeCoC(sampleDepth);\n let effectiveCoC = select(abs(sampleCoC), max(cocAbs, abs(sampleCoC)), isNear);\n\n // Weight by shape mask + CoC influence\n let shapeW = shapeMask(dir);\n let cocW = smoothstep(0.0, 1.0, effectiveCoC / u.maxBlur);\n let weight = shapeW * cocW;\n color += sampleColor * weight;\n totalWeight += weight;\n }\n\n let blurred = color / max(totalWeight, 0.0001);\n // Blend sharp → blurred based on CoC magnitude\n let blend = clamp(cocAbs / u.maxBlur, 0.0, 1.0);\n let result = mix(sharp.rgb, blurred, blend);\n return vec4<f32>(result, 1.0);\n}\n";
|
|
28
|
+
const EDGES_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Edges — normal + depth edge detection, composited via alpha blend.\nstruct U {\n texelSize: vec2<f32>,\n threshold: f32,\n opacity: f32,\n _p0: f32,\n edgeColor: vec3<f32>,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var normalTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_depth_2d;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\nfn decodeNormal(rgb: vec3<f32>) -> vec3<f32> {\n return normalize(rgb * 2.0 - 1.0);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let texel = u.texelSize;\n let color = textureSample(colorTex, samp, uv).rgb;\n\n let nCenter = decodeNormal(textureSample(normalTex, samp, uv).rgb);\n let nRight = decodeNormal(textureSample(normalTex, samp, uv + vec2<f32>( 1.0, 0.0) * texel).rgb);\n let nUp = decodeNormal(textureSample(normalTex, samp, uv + vec2<f32>( 0.0, 1.0) * texel).rgb);\n\n let dCenter = textureSample(depthTex, samp, uv);\n let dRight = textureSample(depthTex, samp, uv + vec2<f32>( 1.0, 0.0) * texel);\n let dUp = textureSample(depthTex, samp, uv + vec2<f32>( 0.0, 1.0) * texel);\n\n let normalEdge = max(1.0 - dot(nCenter, nRight), 1.0 - dot(nCenter, nUp));\n let depthEdge = max(abs(dCenter - dRight), abs(dCenter - dUp));\n\n let edge = clamp(max(normalEdge, depthEdge * 100.0) / u.threshold, 0.0, 1.0);\n let edgeColor = u.edgeColor * edge * u.opacity;\n let alpha = edge * u.opacity;\n return vec4<f32>(mix(color, edgeColor, alpha), 1.0);\n}\n";
|
|
29
|
+
const VS = "struct VertexOutput {\n @builtin(position) clipPos: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n@vertex\nfn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {\n var p = array<vec2<f32>, 3>(\n vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0),\n );\n var o: VertexOutput;\n o.clipPos = vec4(p[vi], 0.0, 1.0);\n o.uv = vec2(p[vi].x * 0.5 + 0.5, 0.5 - p[vi].y * 0.5);\n return o;\n}\nfn lum(c: vec3<f32>) -> f32 { return dot(c, vec3(0.299, 0.587, 0.114)); }\n";
|
|
30
|
+
const FXAA_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, _p6: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\nfn FxaaContrast(a: vec4<f32>, b: vec4<f32>) -> f32 {\n let d = abs(a - b);\n return max(max(max(d.r, d.g), d.b), d.a);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let posM = input.uv;\n let rcpFrame = u.texelSize;\n\n let rgbaM = textureSample(colorTex, samp, posM);\n let rgbaS = textureSample(colorTex, samp, posM + vec2(0.0, rcpFrame.y));\n let rgbaE = textureSample(colorTex, samp, posM + vec2(rcpFrame.x, 0.0));\n let rgbaN = textureSample(colorTex, samp, posM + vec2(0.0, -rcpFrame.y));\n let rgbaW = textureSample(colorTex, samp, posM + vec2(-rcpFrame.x, 0.0));\n\n let earlyExit = max(max(max(\n FxaaContrast(rgbaM, rgbaN),\n FxaaContrast(rgbaM, rgbaS)),\n FxaaContrast(rgbaM, rgbaE)),\n FxaaContrast(rgbaM, rgbaW)) < 0.06;\n if (earlyExit) { return rgbaM; }\n\n let contrastN = FxaaContrast(rgbaM, rgbaN);\n let contrastS = FxaaContrast(rgbaM, rgbaS);\n let contrastE = FxaaContrast(rgbaM, rgbaE);\n let contrastW = FxaaContrast(rgbaM, rgbaW);\n\n var relativeVContrast = (contrastN + contrastS) - (contrastE + contrastW);\n relativeVContrast = relativeVContrast * 5.0;\n\n var horzSpan = relativeVContrast > 0.0;\n\n if (abs(relativeVContrast) < 0.1) {\n let dirToEdgeX = select(-1.0, 1.0, contrastE > contrastW);\n let dirToEdgeY = select(-1.0, 1.0, contrastS > contrastN);\n\n let rgbaAlongH = textureSampleLevel(colorTex, samp, posM + vec2(dirToEdgeX, -dirToEdgeY) * rcpFrame, 0.0);\n let matchAlongH = FxaaContrast(rgbaM, rgbaAlongH);\n\n let rgbaAlongV = textureSampleLevel(colorTex, samp, posM + vec2(-dirToEdgeX, dirToEdgeY) * rcpFrame, 0.0);\n let matchAlongV = FxaaContrast(rgbaM, rgbaAlongV);\n\n relativeVContrast = (matchAlongV - matchAlongH) * 5.0;\n if (abs(relativeVContrast) < 0.1) {\n return mix(rgbaM, (rgbaN + rgbaS + rgbaE + rgbaW) * 0.25, 0.4);\n }\n horzSpan = relativeVContrast > 0.0;\n }\n\n var rgbaN2 = select(rgbaN, rgbaW, horzSpan);\n var rgbaS2 = select(rgbaS, rgbaE, horzSpan);\n\n let pairN = FxaaContrast(rgbaM, rgbaN2) > FxaaContrast(rgbaM, rgbaS2);\n if (!pairN) { rgbaN2 = rgbaS2; }\n\n let offNPX = select(0.0, rcpFrame.x, horzSpan);\n let offNPY = select(rcpFrame.y, 0.0, horzSpan);\n\n var doneN = false;\n var doneP = false;\n var nDist = 0.0;\n var pDist = 0.0;\n var posN = posM;\n var posP = posM;\n\n for (var i = 0u; i < 5u; i++) {\n let inc = f32(i + 1u);\n if (!doneN) {\n nDist = nDist + inc;\n posN = posM + vec2(offNPX, offNPY) * nDist;\n let rgbaEndN = textureSampleLevel(colorTex, samp, posN, 0.0);\n doneN = FxaaContrast(rgbaEndN, rgbaM) > FxaaContrast(rgbaEndN, rgbaN2);\n }\n if (!doneP) {\n pDist = pDist + inc;\n posP = posM - vec2(offNPX, offNPY) * pDist;\n let rgbaEndP = textureSampleLevel(colorTex, samp, posP, 0.0);\n doneP = FxaaContrast(rgbaEndP, rgbaM) > FxaaContrast(rgbaEndP, rgbaN2);\n }\n if (doneN || doneP) { break; }\n }\n\n if (!doneN && !doneP) { return rgbaM; }\n\n let span = nDist + pDist;\n var dist = min(nDist, pDist) / max(span, 0.001);\n dist = 1.0 - dist;\n dist = sqrt(dist);\n\n let result = mix(rgbaM, rgbaN2, dist * 0.5);\n return result;\n}\n";
|
|
31
|
+
const GAUSSIAN_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Gaussian blur — two-pass separable convolution.\n// 9-tap kernel with normalized weights (sum = 1.0) — fixes B1.\nstruct U {\n texelSize: vec2<f32>,\n dirX: f32,\n dirY: f32,\n radius: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let dir = vec2(u.dirX, u.dirY) * u.texelSize * u.radius;\n\n // Normalized 9-tap Gaussian: center 0.227027 + 2*(0.1945946+0.1216216+0.054054+0.016216) = 1.0\n var color = textureSample(colorTex, samp, uv) * 0.227027;\n color += textureSample(colorTex, samp, uv + dir * 1.0) * 0.1945946;\n color += textureSample(colorTex, samp, uv - dir * 1.0) * 0.1945946;\n color += textureSample(colorTex, samp, uv + dir * 2.0) * 0.1216216;\n color += textureSample(colorTex, samp, uv - dir * 2.0) * 0.1216216;\n color += textureSample(colorTex, samp, uv + dir * 3.0) * 0.054054;\n color += textureSample(colorTex, samp, uv - dir * 3.0) * 0.054054;\n color += textureSample(colorTex, samp, uv + dir * 4.0) * 0.016216;\n color += textureSample(colorTex, samp, uv - dir * 4.0) * 0.016216;\n\n return color;\n}\n";
|
|
32
|
+
const GLOW_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Glow blur — separable Gaussian blur of the emissive mask.\nstruct U {\n texelSize: vec2<f32>,\n dirX: f32,\n dirY: f32,\n blurRadius: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var glowTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let weights = array<f32, 5>(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);\n let offsets = array<f32, 5>(0.0, 1.3846154, 3.2307692, 5.176923, 7.1076923);\n let dir = vec2<f32>(u.dirX, u.dirY) * u.blurRadius * u.texelSize;\n\n var color = textureSample(glowTex, samp, uv) * weights[0];\n for (var i = 1u; i < 5u; i = i + 1u) {\n let offset = dir * offsets[i];\n color += textureSample(glowTex, samp, uv + offset) * weights[i];\n color += textureSample(glowTex, samp, uv - offset) * weights[i];\n }\n return color;\n}\n";
|
|
33
|
+
const GLOW_COMPOSITE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Glow composite — add blurred glow to color.\nstruct U {\n intensity: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n _p6: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var glowBlurTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let color = textureSample(colorTex, samp, input.uv).rgb;\n let glow = textureSample(glowBlurTex, samp, input.uv).rgb;\n return vec4<f32>(color + glow * u.intensity, 1.0);\n}\n";
|
|
34
|
+
const GRAIN_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Grain — film grain with optional luminance awareness.\nstruct U {\n texelSize: vec2<f32>,\n intensity: f32,\n size: f32,\n luminanceAware: f32,\n time: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\nfn hash21(p: vec2<f32>) -> f32 {\n let p2 = fract(p * vec2<f32>(443.897, 441.423));\n let p3 = dot(p2, p2 + 19.19);\n return fract(p3);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let color = textureSample(colorTex, samp, input.uv).rgb;\n let grainUV = input.uv * u.size + u.time;\n let grain = hash21(grainUV) - 0.5;\n var amount = u.intensity;\n if (u.luminanceAware > 0.5) {\n let luma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n amount *= 1.0 - luma * 0.5;\n }\n return vec4<f32>(color + grain * amount, 1.0);\n}\n";
|
|
35
|
+
const HALFTONE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Halftone — dot/hex cell pattern for stylized print look.\n// Renders a halftone screen: cells of variable-size dots, with dot size\n// proportional to luminance. Supports rotation and monochrome/color modes.\nstruct U {\n texelSize: vec2<f32>,\n cellSize: f32, // cell size in pixels (e.g. 8)\n dotScale: f32, // dot size multiplier (0–2)\n angle: f32, // screen rotation in radians\n monochrome: f32, // 1.0 = grayscale dots, 0.0 = CMYK-style color dots\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n// Rotated cell coordinate\nfn cellCoord(uv: vec2<f32>, cellSize: f32, angle: f32) -> vec2<f32> {\n let centered = uv - vec2<f32>(0.5);\n let rot = mat2x2<f32>(cos(angle), -sin(angle), sin(angle), cos(angle));\n let rotated = rot * centered + vec2<f32>(0.5);\n return fract(rotated * vec2<f32>(1.0 / cellSize) * u.texelSize * 1000.0);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let color = textureSample(colorTex, samp, uv).rgb;\n let dims = vec2<f32>(1.0 / u.texelSize.x, 1.0 / u.texelSize.y);\n let pixelPos = uv * dims;\n\n // Rotated grid coordinate\n let cosA = cos(u.angle);\n let sinA = sin(u.angle);\n let rot = mat2x2<f32>(cosA, -sinA, sinA, cosA);\n let centered = pixelPos - dims * 0.5;\n let rotated = rot * centered + dims * 0.5;\n\n let cell = floor(rotated / u.cellSize);\n let cellCenter = (cell + vec2<f32>(0.5)) * u.cellSize;\n let cellUV = (rotated - cell * u.cellSize) / u.cellSize;\n\n // Sample color at cell center for stable dot color\n let samplePos = (cellCenter / dims);\n let cellColor = textureSample(colorTex, samp, clamp(samplePos, vec2<f32>(0.0), vec2<f32>(1.0))).rgb;\n\n if (u.monochrome > 0.5) {\n let luma = dot(cellColor, vec3<f32>(0.299, 0.587, 0.114));\n let dotRadius = luma * u.dotScale * 0.5;\n let distFromCenter = length(cellUV - 0.5);\n let dot = 1.0 - smoothstep(dotRadius - 0.05, dotRadius + 0.05, distFromCenter);\n return vec4<f32>(vec3<f32>(luma * dot), 1.0);\n } else {\n // CMYK-style: separate dot per channel with angle offset\n var result = vec3<f32>(0.0);\n let channelAngles = array<f32, 3>(0.0, 1.0471976, 2.0943951); // 0, 60, 120 deg\n for (var i = 0u; i < 3u; i = i + 1u) {\n let chAngle = u.angle + channelAngles[i];\n let chCos = cos(chAngle);\n let chSin = sin(chAngle);\n let chRot = mat2x2<f32>(chCos, -chSin, chSin, chCos);\n let chCentered = pixelPos - dims * 0.5;\n let chRotated = chRot * chCentered + dims * 0.5;\n let chCell = floor(chRotated / u.cellSize);\n let chCellCenter = (chCell + vec2<f32>(0.5)) * u.cellSize;\n let chCellUV = (chRotated - chCell * u.cellSize) / u.cellSize;\n let chSamplePos = clamp(chCellCenter / dims, vec2<f32>(0.0), vec2<f32>(1.0));\n let chColor = textureSample(colorTex, samp, chSamplePos)[i];\n let dotRadius = chColor * u.dotScale * 0.5;\n let distFromCenter = length(chCellUV - 0.5);\n let dot = 1.0 - smoothstep(dotRadius - 0.05, dotRadius + 0.05, distFromCenter);\n result[i] = chColor * dot;\n }\n return vec4<f32>(result, 1.0);\n }\n}\n";
|
|
36
|
+
const HIGHLIGHT_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Highlight blur — separable Gaussian blur of the mask.\nstruct U {\n texelSize: vec2<f32>,\n dirX: f32,\n dirY: f32,\n blurRadius: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var maskTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let weights = array<f32, 5>(0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);\n let offsets = array<f32, 5>(0.0, 1.3846154, 3.2307692, 5.176923, 7.1076923);\n let dir = vec2<f32>(u.dirX, u.dirY) * u.blurRadius * u.texelSize;\n\n var color = textureSample(maskTex, samp, uv) * weights[0];\n for (var i = 1u; i < 5u; i = i + 1u) {\n let offset = dir * offsets[i];\n color += textureSample(maskTex, samp, uv + offset) * weights[i];\n color += textureSample(maskTex, samp, uv - offset) * weights[i];\n }\n return color;\n}\n";
|
|
37
|
+
const HIGHLIGHT_COMPOSITE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Highlight composite — add outer glow + inner fill to color.\nstruct U {\n intensity: f32,\n innerOpacity: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var blurTex: texture_2d<f32>;\n@group(0) @binding(2) var maskTex: texture_2d<f32>;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let color = textureSample(colorTex, samp, uv).rgb;\n let blurred = textureSample(blurTex, samp, uv).rgb;\n let mask = textureSample(maskTex, samp, uv).rgb;\n\n let outerGlow = blurred * u.intensity;\n let inner = mask * u.innerOpacity;\n\n return vec4<f32>(color + outerGlow + inner, 1.0);\n}\n";
|
|
38
|
+
const LENS_DISTORTION_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Lens distortion — barrel/pincushion distortion with optional chromatic split.\n// Applies radial distortion: positive = barrel (fisheye), negative = pincushion.\nstruct U {\n texelSize: vec2<f32>,\n intensity: f32, // -1.0 (pincushion) to 1.0 (barrel)\n scale: f32, // zoom to compensate for distortion (1.0 = no zoom)\n chromaSplit: f32, // chromatic aberration on distortion (0–1)\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let center = vec2<f32>(0.5, 0.5);\n let dir = uv - center;\n let r2 = dot(dir, dir);\n // Barrel/pincushion: r' = r * (1 + k * r^2)\n let distortion = 1.0 + u.intensity * r2;\n var distortedUV = center + dir * distortion * u.scale;\n\n // Clamp to valid range\n distortedUV = clamp(distortedUV, vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 1.0));\n\n if (u.chromaSplit > 0.001) {\n let splitDir = normalize(dir + vec2<f32>(0.0001, 0.0001)) * u.chromaSplit * 0.01;\n let r = textureSample(colorTex, samp, distortedUV + splitDir).r;\n let g = textureSample(colorTex, samp, distortedUV).g;\n let b = textureSample(colorTex, samp, distortedUV - splitDir).b;\n return vec4<f32>(r, g, b, 1.0);\n }\n\n let color = textureSample(colorTex, samp, distortedUV).rgb;\n return vec4<f32>(color, 1.0);\n}\n";
|
|
39
|
+
const LENS_FLARE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Lens flare — ghost chain + halo + anamorphic streak + bright spot.\nstruct U {\n lightScreenPos: vec2<f32>,\n intensity: f32,\n threshold: f32,\n ghostCount: f32,\n ghostSpacing: f32,\n haloWidth: f32,\n starSamples: f32,\n _p0: f32,\n tint: vec3<f32>,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var depthTex: texture_depth_2d;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let center = vec2<f32>(0.5, 0.5);\n let lightPos = u.lightScreenPos;\n let dir = lightPos - center;\n let dist = length(dir);\n let ndir = dir / max(dist, 0.0001);\n\n let color = textureSample(colorTex, samp, uv).rgb;\n var flare = vec3<f32>(0.0);\n\n let ghostCount = u32(u.ghostCount);\n for (var i = 0u; i < 16u; i = i + 1u) {\n if (i >= ghostCount) { break; }\n let t = (f32(i) + 1.0) * u.ghostSpacing;\n let ghostPos = lightPos - ndir * dist * t;\n let ghostDist = length(uv - ghostPos);\n let falloff = 1.0 / (1.0 + ghostDist * ghostDist * 200.0);\n let scale = 1.0 - f32(i) / f32(ghostCount);\n let ghostColor = textureSampleLevel(colorTex, samp, ghostPos, 0.0).rgb;\n flare += ghostColor * falloff * scale * u.intensity * u.tint;\n }\n\n let haloAxis = lightPos - center;\n let haloDist = length(uv - center - haloAxis * 0.5);\n let haloRadius = length(haloAxis) * 0.5;\n let haloRing = abs(haloDist - haloRadius);\n let haloFalloff = 1.0 / (1.0 + haloRing * haloRing * 100.0 / u.haloWidth);\n flare += u.tint * haloFalloff * u.intensity * 0.5;\n\n let toLight = lightPos - uv;\n let horizDist = abs(toLight.x);\n let vertDist = abs(toLight.y);\n let streakFalloff = 1.0 / (1.0 + horizDist * horizDist * 50.0) * (1.0 / (1.0 + vertDist * vertDist * 500.0));\n flare += u.tint * streakFalloff * u.intensity * 0.3;\n\n let lightDist = length(uv - lightPos);\n let lightFalloff = 1.0 / (1.0 + lightDist * lightDist * 300.0);\n let lightColor = textureSampleLevel(colorTex, samp, lightPos, 0.0).rgb;\n let luma = dot(lightColor, vec3<f32>(0.299, 0.587, 0.114));\n if (luma > u.threshold) {\n flare += lightColor * lightFalloff * u.intensity;\n }\n\n return vec4<f32>(color + flare, 1.0);\n}\n";
|
|
40
|
+
const LUT_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// LUT — 3D LUT color grading. Samples a 3D lookup table to apply color grading.\n// Replaces the standalone frame-graph LUT3DPass with an in-chain effect.\n// Uses a dedicated bind group layout with texture_3d (not the shared ccLayout).\nstruct U {\n enabled: f32,\n lutSize: f32,\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var lutTex: texture_3d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n var color = textureSample(colorTex, samp, uv).rgb;\n\n if (u.enabled > 0.5) {\n let lutSize = u.lutSize;\n // Map [0,1] color to LUT 3D coordinates with proper boundary handling\n let lutCoord = color * ((lutSize - 1.0) / lutSize) + 0.5 / lutSize;\n color = textureSample(lutTex, samp, lutCoord).rgb;\n }\n return vec4<f32>(color, 1.0);\n}\n";
|
|
41
|
+
const MOTION_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Motion blur — velocity-based, with depth-aware weighting.\nstruct U {\n texelSize: vec2<f32>,\n intensity: f32,\n maxSamples: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var velocityTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_depth_2d;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n // All textureSample calls must be in uniform control flow (before any branching).\n let velocity = textureSample(velocityTex, samp, uv).xy;\n let baseColor = textureSample(colorTex, samp, uv);\n let depth = textureSample(depthTex, samp, uv);\n let speed = length(velocity);\n\n if (speed < 0.001) { return baseColor; }\n\n let maxSamples = u32(u.maxSamples);\n let maxSamplesF = u.maxSamples;\n let clampedVelocity = velocity * u.intensity;\n let dims = textureDimensions(depthTex);\n let colorDims = textureDimensions(colorTex);\n\n var color = vec3<f32>(0.0);\n var totalWeight = 0.0;\n\n for (var i = 0u; i < 32u; i = i + 1u) {\n if (i >= maxSamples) { break; }\n let t = (f32(i) + 0.5) / maxSamplesF - 0.5;\n let sampleUV = uv + clampedVelocity * t;\n // Use textureLoad (works in non-uniform control flow) instead of textureSample.\n let dcoords = vec2<u32>(clamp(vec2<i32>(i32(sampleUV.x * f32(dims.x)), i32(sampleUV.y * f32(dims.y))), vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1)));\n let sampleDepth = textureLoad(depthTex, dcoords, 0);\n let weight = select(0.1, 1.0, abs(sampleDepth - depth) < 0.1);\n let ccoords = vec2<u32>(clamp(vec2<i32>(i32(sampleUV.x * f32(colorDims.x)), i32(sampleUV.y * f32(colorDims.y))), vec2<i32>(0, 0), vec2<i32>(i32(colorDims.x) - 1, i32(colorDims.y) - 1)));\n color += textureLoad(colorTex, ccoords, 0).rgb * weight;\n totalWeight += weight;\n }\n\n return vec4<f32>(color / max(totalWeight, 1.0), 1.0);\n}\n";
|
|
42
|
+
const OCCLUDER_CHUNK = "// Occluder chunk — prepended to all postfx fragment shaders by makePipeline().\n// Provides the OccluderUniform struct, a group(1) binding, and an isOccluded()\n// test. Each shader's fs_main calls `if (isOccluded(input.uv)) { discard; }`\n// at the top to skip work under opaque UI panels (the pixi-ui overlay composites\n// on top of the game canvas, so the game's output under an opaque panel is\n// never seen by the user).\n//\n// Rects are in UV space (0-1, top-left origin matching fullscreen-vs.wgsl).\n// Up to 8 rects supported; count=0 disables occlusion (isOccluded always false).\nstruct OccluderUniform {\n count: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n rects: array<vec4<f32>, 8>, // x, y, w, h in UV space (0-1)\n};\n@group(1) @binding(0) var<uniform> occluders: OccluderUniform;\n\nfn isOccluded(uv: vec2<f32>) -> bool {\n for (var i = 0u; i < 8u; i = i + 1u) {\n if (f32(i) >= occluders.count) { break; }\n let r = occluders.rects[i];\n if (uv.x >= r.x && uv.x <= r.x + r.z && uv.y >= r.y && uv.y <= r.y + r.w) {\n return true;\n }\n }\n return false;\n}\n";
|
|
43
|
+
const OUTLINE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Outline — detect mask boundary and draw edge ring outside the mask.\nstruct U {\n texelSize: vec2<f32>,\n outlineWidth: f32,\n opacity: f32,\n _p0: f32,\n outlineColor: vec3<f32>,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var maskTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let texel = u.texelSize;\n let w = u.outlineWidth;\n let color = textureSample(colorTex, samp, uv).rgb;\n\n let center = textureSample(maskTex, samp, uv).r;\n\n // DEBUG: visualize mask — red where selected, normal color where not\n if (center > 0.5) {\n return vec4<f32>(1.0, 0.0, 0.0, 1.0);\n }\n\n let offsets = array<vec2<f32>, 8>(\n vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, -1.0),\n vec2<f32>(-1.0, 1.0), vec2<f32>( 1.0, 1.0),\n vec2<f32>(-1.0, 0.0), vec2<f32>( 1.0, 0.0),\n vec2<f32>( 0.0, -1.0), vec2<f32>( 0.0, 1.0),\n );\n\n let dims = textureDimensions(maskTex);\n var edge = 0.0;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let sampleUV = uv + offsets[i] * texel * w;\n let icoords = vec2<u32>(clamp(vec2<i32>(i32(sampleUV.x * f32(dims.x)), i32(sampleUV.y * f32(dims.y))), vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1)));\n let s = textureLoad(maskTex, icoords, 0).r;\n edge = max(edge, abs(center - s));\n }\n\n let outline = clamp(edge * (1.0 - center) * u.opacity, 0.0, 1.0);\n return vec4<f32>(mix(color, u.outlineColor, outline), 1.0);\n}\n";
|
|
44
|
+
const PIXELATION_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Pixelation — blocky low-res look via nearest-neighbor UV quantization\n// + depth edge detection. Samples color and depth at quantized UVs to\n// emulate rendering at reduced resolution without a separate low-res target.\nstruct U {\n texelSize: vec2<f32>,\n pixelSize: f32,\n depthEdgeStrength: f32,\n screenW: f32,\n screenH: f32,\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var depthTex: texture_depth_2d;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let screen = vec2(u.screenW, u.screenH);\n let px = u.pixelSize;\n let cellCoord = floor(input.uv * screen / px);\n let cellCenter = (cellCoord + vec2(0.5)) * px / screen;\n\n let texel = textureSample(colorTex, samp, cellCenter);\n let depth = textureSample(depthTex, samp, cellCenter);\n\n let texelStep = px / screen;\n let depthR = textureSample(depthTex, samp, cellCenter + vec2(texelStep.x, 0.0));\n let depthL = textureSample(depthTex, samp, cellCenter - vec2(texelStep.x, 0.0));\n let depthU = textureSample(depthTex, samp, cellCenter + vec2(0.0, texelStep.y));\n let depthD = textureSample(depthTex, samp, cellCenter - vec2(0.0, texelStep.y));\n\n var diff = 0.0;\n diff += clamp(depthR - depth, 0.0, 1.0);\n diff += clamp(depthL - depth, 0.0, 1.0);\n diff += clamp(depthU - depth, 0.0, 1.0);\n diff += clamp(depthD - depth, 0.0, 1.0);\n\n let dei = floor(smoothstep(0.01, 0.02, diff) * 2.0) / 2.0;\n let strength = select(1.0, 1.0 - dei * u.depthEdgeStrength, dei > 0.0);\n\n return vec4(texel.rgb * strength, texel.a);\n}\n";
|
|
45
|
+
const SHARPEN_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Sharpen — unsharp mask (center + (center - blurred) * sharpness).\nstruct U {\n texelSize: vec2<f32>,\n sharpness: f32,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let texel = u.texelSize;\n let center = textureSample(colorTex, samp, uv).rgb;\n let up = textureSample(colorTex, samp, uv + vec2<f32>(0.0, -1.0) * texel).rgb;\n let down = textureSample(colorTex, samp, uv + vec2<f32>(0.0, 1.0) * texel).rgb;\n let left = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 0.0) * texel).rgb;\n let right = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 0.0) * texel).rgb;\n let blurred = (up + down + left + right) * 0.25;\n let result = center + (center - blurred) * u.sharpness;\n return vec4<f32>(result, 1.0);\n}\n";
|
|
46
|
+
const SOBEL_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\nstruct U { texelSize: vec2<f32>, _p0: f32, _p1: f32, _p2: f32, _p3: f32, _p4: f32, _p5: f32, _p6: f32, };\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv; let t = u.texelSize;\n let tl = lum(textureSample(colorTex, samp, uv + vec2(-t.x, -t.y)).rgb);\n let tm = lum(textureSample(colorTex, samp, uv + vec2(0.0, -t.y)).rgb);\n let tr = lum(textureSample(colorTex, samp, uv + vec2( t.x, -t.y)).rgb);\n let ml = lum(textureSample(colorTex, samp, uv + vec2(-t.x, 0.0)).rgb);\n let mr = lum(textureSample(colorTex, samp, uv + vec2( t.x, 0.0)).rgb);\n let bl = lum(textureSample(colorTex, samp, uv + vec2(-t.x, t.y)).rgb);\n let bm = lum(textureSample(colorTex, samp, uv + vec2(0.0, t.y)).rgb);\n let br = lum(textureSample(colorTex, samp, uv + vec2( t.x, t.y)).rgb);\n let gx = -tl + tr - 2.0 * ml + 2.0 * mr - bl + br;\n let gy = -tl - 2.0 * tm - tr + bl + 2.0 * bm + br;\n let g = sqrt(gx * gx + gy * gy);\n let edge = clamp(g, 0.0, 1.0);\n let c = textureSample(colorTex, samp, uv);\n return vec4(c.rgb * (1.0 - edge) + vec3(edge) * 0.5, c.a);\n}\n";
|
|
47
|
+
const SPLIT_TONE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Split-tone — separate highlight and shadow tinting with balance control.\n// Applies different color tints to shadows and highlights, blended by luminance.\nstruct U {\n shadowR: f32, shadowG: f32, shadowB: f32, // shadow tint color\n highlightR: f32, highlightG: f32, highlightB: f32, // highlight tint color\n balance: f32, // -1.0 = more shadows, 1.0 = more highlights, 0 = even\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n var color = textureSample(colorTex, samp, uv).rgb;\n\n let luma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n // Weight: 0 at shadows, 1 at highlights, shifted by balance\n let weight = smoothstep(0.0, 1.0, luma + u.balance * 0.5);\n\n let shadowTint = vec3<f32>(u.shadowR, u.shadowG, u.shadowB);\n let highlightTint = vec3<f32>(u.highlightR, u.highlightG, u.highlightB);\n\n // Blend: shadows get shadow tint, highlights get highlight tint\n let tint = mix(shadowTint, highlightTint, weight);\n // Additive tint with luminance preservation\n color = color * (vec3<f32>(1.0) + tint * 0.5);\n\n return vec4<f32>(color, 1.0);\n}\n";
|
|
48
|
+
const SSAO_BLUR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// SSAO blur — depth-aware bilateral blur.\nstruct U {\n texelSize: vec2<f32>,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n};\n@group(0) @binding(0) var ssaoTex: texture_2d<f32>;\n@group(0) @binding(1) var depthTex: texture_depth_2d;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let centerDepth = textureSample(depthTex, samp, uv);\n var sum = 0.0;\n var weightSum = 0.0;\n\n for (var y = -2; y <= 2; y = y + 1) {\n for (var x = -2; x <= 2; x = x + 1) {\n let offset = vec2<f32>(f32(x), f32(y)) * u.texelSize;\n let sampleDepth = textureSample(depthTex, samp, uv + offset);\n let weight = select(0.0, 1.0, abs(sampleDepth - centerDepth) < 0.1);\n sum += textureSample(ssaoTex, samp, uv + offset).r * weight;\n weightSum += weight;\n }\n }\n\n let result = sum / max(weightSum, 1.0);\n return vec4<f32>(result, result, result, 1.0);\n}\n";
|
|
49
|
+
const SSAO_COMPOSITE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// SSAO composite — multiply color by AO factor.\nstruct U {\n texelSize: vec2<f32>,\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n _p6: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var aoTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let color = textureSample(colorTex, samp, input.uv);\n let ao = textureSample(aoTex, samp, input.uv).r;\n return vec4<f32>(color.rgb * ao, color.a);\n}\n";
|
|
50
|
+
const SSAO_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// SSAO — Ground-Truth Ambient Occlusion (GTAO).\n// Slice-based horizon scanning with cosine-weighted falloff.\n// Replaces the basic hemisphere approach with a more accurate GTAO that\n// computes per-slice horizons by ray-marching in the view-space slice plane.\nstruct U {\n invProjection: mat4x4<f32>,\n view: mat4x4<f32>,\n projection: mat4x4<f32>,\n directions: f32, // number of slice directions (default 4)\n radius: f32,\n bias: f32,\n noiseScale: vec2<f32>,\n screenSize: vec2<f32>,\n power: f32, // AO contrast power (default 1.5)\n thickness: f32, // depth thickness for horizon test (default 0.1)\n slices: f32, // samples per slice direction (default 8)\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var depthTex: texture_depth_2d;\n@group(0) @binding(1) var normalTex: texture_2d<f32>;\n@group(0) @binding(2) var noiseTex: texture_2d<f32>;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\nfn reconstructViewPos(uv: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec3<f32>(uv * 2.0 - 1.0, depth);\n let clip = vec4<f32>(ndc, 1.0);\n let viewPos = u.invProjection * clip;\n return viewPos.xyz / viewPos.w;\n}\n\nfn projectToScreen(viewPos: vec3<f32>) -> vec3<f32> {\n let clip = u.projection * vec4<f32>(viewPos, 1.0);\n let ndc = clip.xyz / clip.w;\n return vec3<f32>(ndc.xy * 0.5 + 0.5, ndc.z);\n}\n\n// Falloff function: 1 near, 0 at radius\nfn falloff(dist: f32) -> f32 {\n return max(0.0, 1.0 - dist / u.radius);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n // All textureSample calls in uniform control flow\n let depth = textureSample(depthTex, samp, uv);\n let worldNormal = textureSample(normalTex, samp, uv).xyz * 2.0 - 1.0;\n let noiseVal = textureSample(noiseTex, samp, uv * u.noiseScale).xy;\n\n if (depth >= 1.0) { return vec4<f32>(1.0, 1.0, 1.0, 1.0); }\n\n let viewPos = reconstructViewPos(uv, depth);\n let N = normalize((u.view * vec4<f32>(worldNormal, 0.0)).xyz);\n let V = normalize(-viewPos);\n\n let rotAngle = noiseVal.x * 6.2831853;\n let numDirs = u32(clamp(u.directions, 1.0, 8.0));\n let numSamples = u32(clamp(u.slices, 2.0, 16.0));\n let dims = textureDimensions(depthTex);\n\n var occlusion = 0.0;\n\n for (var d = 0u; d < 8u; d = d + 1u) {\n if (d >= numDirs) { break; }\n // Slice direction in screen space, rotated by noise\n let sliceAngle = rotAngle + f32(d) * 6.2831853 / f32(numDirs);\n let sliceDir = vec2<f32>(cos(sliceAngle), sin(sliceAngle));\n\n // Project slice direction to view space\n // The slice plane contains V and the slice direction projected to view space\n let sliceNormalVS = normalize(vec3<f32>(sliceDir, 0.0));\n\n // Horizon search in both directions along the slice\n var horizon1 = -1.0; // max angle in +dir\n var horizon2 = 1.0; // min angle in -dir\n\n for (var s = 1u; s < 16u; s = s + 1u) {\n if (s >= numSamples) { break; }\n let t = f32(s) / f32(numSamples);\n let radius = t * u.radius;\n\n // +direction sample\n let samplePos1 = viewPos + vec3<f32>(sliceDir, 0.0) * radius;\n let screen1 = projectToScreen(samplePos1);\n if (screen1.x >= 0.0 && screen1.x <= 1.0 && screen1.y >= 0.0 && screen1.y <= 1.0) {\n let icoords1 = vec2<u32>(clamp(\n vec2<i32>(i32(screen1.x * f32(dims.x)), i32(screen1.y * f32(dims.y))),\n vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let sampleDepth1 = textureLoad(depthTex, icoords1, 0);\n let sampleViewPos1 = reconstructViewPos(screen1.xy, sampleDepth1);\n let depthDiff1 = samplePos1.z - sampleViewPos1.z;\n if (depthDiff1 > u.bias && depthDiff1 < u.thickness * radius) {\n let omega = atan2(sampleViewPos1.z - viewPos.z, length(sampleViewPos1.xy - viewPos.xy));\n horizon1 = max(horizon1, omega);\n }\n }\n\n // -direction sample\n let samplePos2 = viewPos - vec3<f32>(sliceDir, 0.0) * radius;\n let screen2 = projectToScreen(samplePos2);\n if (screen2.x >= 0.0 && screen2.x <= 1.0 && screen2.y >= 0.0 && screen2.y <= 1.0) {\n let icoords2 = vec2<u32>(clamp(\n vec2<i32>(i32(screen2.x * f32(dims.x)), i32(screen2.y * f32(dims.y))),\n vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let sampleDepth2 = textureLoad(depthTex, icoords2, 0);\n let sampleViewPos2 = reconstructViewPos(screen2.xy, sampleDepth2);\n let depthDiff2 = samplePos2.z - sampleViewPos2.z;\n if (depthDiff2 > u.bias && depthDiff2 < u.thickness * radius) {\n let omega = atan2(sampleViewPos2.z - viewPos.z, length(sampleViewPos2.xy - viewPos.xy));\n horizon2 = min(horizon2, omega);\n }\n }\n }\n\n // Compute the angle between the normal and the slice plane\n // Project normal onto the slice plane\n let sliceNormal3D = normalize(vec3<f32>(sliceDir, 0.0));\n let Nproj = normalize(N - dot(N, sliceNormal3D) * sliceNormal3D);\n let Nangle = atan2(Nproj.z, length(Nproj.xy));\n\n // Clamp horizons to the normal hemisphere\n let h1 = max(horizon1, Nangle);\n let h2 = min(horizon2, Nangle);\n\n // AO contribution from this slice: integral of cos(theta - Nangle) from h2 to h1\n let ao1 = cos(2.0 * h1 - Nangle) - cos(2.0 * Nangle - Nangle);\n let ao2 = cos(2.0 * Nangle - Nangle) - cos(2.0 * h2 - Nangle);\n let sliceAO = clamp((ao1 + ao2) * 0.5, 0.0, 1.0);\n\n // Weight by the slice's alignment with the normal\n let sliceWeight = abs(dot(Nproj, N));\n occlusion += sliceAO * sliceWeight;\n }\n\n occlusion = occlusion / f32(numDirs);\n // Apply power for contrast control\n let ao = 1.0 - pow(clamp(occlusion, 0.0, 1.0), u.power);\n let result = clamp(ao, 0.0, 1.0);\n return vec4<f32>(result, result, result, 1.0);\n}\n";
|
|
51
|
+
const SSR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// SSR — screen-space reflections with DDA ray marching + binary refinement.\n// Upgraded from linear march to pixel-perfect DDA stepping with sub-pixel\n// binary search refinement for accurate hit detection. Includes thickness\n// testing, edge fade, and distance fade.\nstruct U {\n invProjection: mat4x4<f32>,\n view: mat4x4<f32>,\n projection: mat4x4<f32>,\n maxSteps: f32,\n thickness: f32,\n maxDistance: f32,\n fadeStart: f32,\n fadeEnd: f32,\n binarySteps: f32, // number of binary refinement steps (default 10)\n stride: f32, // ray march stride multiplier (default 1.0)\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var normalTex: texture_2d<f32>;\n@group(0) @binding(2) var depthTex: texture_depth_2d;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\nfn reconstructViewPos(uv: vec2<f32>, depth: f32) -> vec3<f32> {\n let ndc = vec3<f32>(uv * 2.0 - 1.0, depth);\n let clip = vec4<f32>(ndc, 1.0);\n let viewPos = u.invProjection * clip;\n return viewPos.xyz / viewPos.w;\n}\n\nfn projectToScreen(viewPos: vec3<f32>) -> vec3<f32> {\n let clip = u.projection * vec4<f32>(viewPos, 1.0);\n let ndc = clip.xyz / clip.w;\n return vec3<f32>(ndc.xy * 0.5 + 0.5, ndc.z);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n // All textureSample calls in uniform control flow\n let depth = textureSample(depthTex, samp, uv);\n let baseColor = textureSample(colorTex, samp, uv);\n let worldNormal = textureSample(normalTex, samp, uv).xyz * 2.0 - 1.0;\n\n if (depth >= 1.0) { return baseColor; }\n\n let viewPos = reconstructViewPos(uv, depth);\n let N = normalize((u.view * vec4<f32>(worldNormal, 0.0)).xyz);\n let V = normalize(-viewPos);\n let reflectDir = reflect(-V, N);\n\n // Skip reflections pointing toward the camera\n if (reflectDir.z >= 0.0) { return baseColor; }\n\n // Project start and end points to screen space\n let startPos = viewPos;\n let endPos = viewPos + reflectDir * u.maxDistance;\n let startScreen = projectToScreen(startPos);\n let endScreen = projectToScreen(endPos);\n\n // DDA setup: march from startScreen to endScreen in pixel space\n let dims = textureDimensions(depthTex);\n let colorDims = textureDimensions(colorTex);\n let pixelStart = vec2<f32>(startScreen.x * f32(dims.x), startScreen.y * f32(dims.y));\n let pixelEnd = vec2<f32>(endScreen.x * f32(dims.x), endScreen.y * f32(dims.y));\n let pixelDir = pixelEnd - pixelStart;\n let pixelLen = length(pixelDir);\n let pixelStep = normalize(pixelDir) * u.stride;\n\n let maxSteps = u32(clamp(u.maxSteps, 1.0, 128.0));\n let binarySteps = u32(clamp(u.binarySteps, 1.0, 16.0));\n\n var hitColor = vec3<f32>(0.0);\n var hitAlpha = 0.0;\n var hit = false;\n\n // ── DDA ray march ──\n var currentPixel = pixelStart;\n var currentViewPos = startPos;\n\n for (var i = 0u; i < 128u; i = i + 1u) {\n if (i >= maxSteps) { break; }\n currentPixel = currentPixel + pixelStep;\n let sampleUV = currentPixel / vec2<f32>(f32(dims.x), f32(dims.y));\n\n // Out of bounds — stop\n if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) { break; }\n\n // Interpolate view-space position along the ray\n let t = f32(i + 1u) / f32(maxSteps);\n currentViewPos = mix(startPos, endPos, t);\n\n let icoords = vec2<u32>(clamp(\n vec2<i32>(i32(sampleUV.x * f32(dims.x)), i32(sampleUV.y * f32(dims.y))),\n vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let sampleDepth = textureLoad(depthTex, icoords, 0);\n let sampleViewPos = reconstructViewPos(sampleUV, sampleDepth);\n\n let depthDiff = currentViewPos.z - sampleViewPos.z;\n // Hit: ray is behind the surface within thickness threshold\n if (depthDiff > 0.0 && depthDiff < u.thickness) {\n hit = true;\n // ── Binary refinement for sub-pixel accuracy ──\n var refineStart = currentPixel - pixelStep;\n var refineEnd = currentPixel;\n for (var b = 0u; b < 16u; b = b + 1u) {\n if (b >= binarySteps) { break; }\n let midPixel = (refineStart + refineEnd) * 0.5;\n let midUV = midPixel / vec2<f32>(f32(dims.x), f32(dims.y));\n let midCoords = vec2<u32>(clamp(\n vec2<i32>(i32(midUV.x * f32(dims.x)), i32(midUV.y * f32(dims.y))),\n vec2<i32>(0, 0), vec2<i32>(i32(dims.x) - 1, i32(dims.y) - 1),\n ));\n let midDepth = textureLoad(depthTex, midCoords, 0);\n let midViewPos = reconstructViewPos(midUV, midDepth);\n\n let bt = length(midPixel - pixelStart) / pixelLen;\n let rayZ = mix(startPos.z, endPos.z, bt);\n let midDiff = rayZ - midViewPos.z;\n if (midDiff > 0.0 && midDiff < u.thickness) {\n refineEnd = midPixel;\n } else {\n refineStart = midPixel;\n }\n }\n\n // Sample color at the refined hit position\n let hitUV = refineEnd / vec2<f32>(f32(dims.x), f32(dims.y));\n let hitCoords = vec2<u32>(clamp(\n vec2<i32>(i32(hitUV.x * f32(colorDims.x)), i32(hitUV.y * f32(colorDims.y))),\n vec2<i32>(0, 0), vec2<i32>(i32(colorDims.x) - 1, i32(colorDims.y) - 1),\n ));\n hitColor = textureLoad(colorTex, hitCoords, 0).rgb;\n\n // Distance fade\n let dist = length(currentViewPos - viewPos);\n let fade = 1.0 - smoothstep(u.fadeStart, u.fadeEnd, dist);\n // Edge fade (screen border)\n let edgeFade = smoothstep(0.0, 0.1, hitUV.x) * smoothstep(0.0, 0.1, 1.0 - hitUV.x) *\n smoothstep(0.0, 0.1, hitUV.y) * smoothstep(0.0, 0.1, 1.0 - hitUV.y);\n hitAlpha = fade * edgeFade;\n break;\n }\n }\n\n if (!hit) { return baseColor; }\n return vec4<f32>(mix(baseColor.rgb, hitColor, hitAlpha), baseColor.a);\n}\n";
|
|
52
|
+
const TAA_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// TAA — Temporal Anti-Aliasing with YCoCg neighborhood clamp + jitter + variance.\n// Upgraded with:\n// - Jitter-aware reprojection (sub-pixel jitter offset for better convergence)\n// - Optional variance-based clamping (reduces ghosting vs pure min/max clamp)\n// - Velocity-based history reprojection (unchanged from original)\nstruct U {\n texelSize: vec2<f32>,\n blendFactor: f32,\n varianceClamp: f32, // 1.0 = enable variance clamping, 0.0 = min/max only\n jitterX: f32, // sub-pixel jitter offset X (in texels)\n jitterY: f32, // sub-pixel jitter offset Y (in texels)\n _p0: f32,\n _p1: f32,\n _p2: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var velocityTex: texture_2d<f32>;\n@group(0) @binding(2) var historyTex: texture_2d<f32>;\n@group(0) @binding(3) var samp: sampler;\n@group(0) @binding(4) var<uniform> u: U;\n\nfn rgbToYCoCg(rgb: vec3<f32>) -> vec3<f32> {\n let co = rgb.r - rgb.b;\n let tmp = rgb.b + co * 0.5;\n let cg = rgb.g - tmp;\n let y = tmp + cg * 0.5;\n return vec3<f32>(y, co, cg);\n}\nfn yCoCgToRGB(ycocg: vec3<f32>) -> vec3<f32> {\n let tmp = ycocg.x - ycocg.z * 0.5;\n let g = ycocg.z + tmp;\n let b = tmp - ycocg.y * 0.5;\n let r = ycocg.y + b;\n return vec3<f32>(r, g, b);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let jitter = vec2<f32>(u.jitterX, u.jitterY) * u.texelSize;\n // Current color — sample at jittered position (the scene was rendered with jitter)\n let current = textureSample(colorTex, samp, uv).rgb;\n let velocity = textureSample(velocityTex, samp, uv).rg;\n // Reproject: history UV accounts for velocity + jitter offset\n let historyUV = uv - velocity - jitter;\n let history = textureSample(historyTex, samp, historyUV).rgb;\n\n // 3x3 neighborhood for clamping\n let ts = u.texelSize;\n let tl = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, -1.0) * ts).rgb;\n let tr = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, -1.0) * ts).rgb;\n let bl = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 1.0) * ts).rgb;\n let br = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 1.0) * ts).rgb;\n let l = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 0.0) * ts).rgb;\n let r2 = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 0.0) * ts).rgb;\n let t = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, -1.0) * ts).rgb;\n let b2 = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, 1.0) * ts).rgb;\n\n let ycocgCenter = rgbToYCoCg(current);\n var minC = ycocgCenter;\n var maxC = ycocgCenter;\n var avgC = ycocgCenter;\n let neighbors = array<vec3<f32>, 8>(tl, tr, bl, br, l, r2, t, b2);\n for (var i = 0u; i < 8u; i = i + 1u) {\n let ycocg = rgbToYCoCg(neighbors[i]);\n minC = min(minC, ycocg);\n maxC = max(maxC, ycocg);\n avgC += ycocg;\n }\n avgC = avgC / 9.0;\n\n var clampedHistory: vec3<f32>;\n if (u.varianceClamp > 0.5) {\n // Variance-based clamping: tighten the clamp box using standard deviation\n var variance = vec3<f32>(0.0);\n for (var i = 0u; i < 8u; i = i + 1u) {\n let d = rgbToYCoCg(neighbors[i]) - avgC;\n variance += d * d;\n }\n variance = variance / 8.0;\n let stdDev = sqrt(variance);\n let tightMin = avgC - stdDev * 1.5;\n let tightMax = avgC + stdDev * 1.5;\n // Use the tighter of min/max and variance-based bounds\n let clampMin = max(minC, tightMin);\n let clampMax = min(maxC, tightMax);\n clampedHistory = yCoCgToRGB(clamp(rgbToYCoCg(history), clampMin, clampMax));\n } else {\n // Standard min/max neighborhood clamp\n clampedHistory = yCoCgToRGB(clamp(rgbToYCoCg(history), minC, maxC));\n }\n\n let result = mix(clampedHistory, current, u.blendFactor);\n return vec4<f32>(result, 1.0);\n}\n";
|
|
53
|
+
const TONEMAP_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// ACES tonemap — HDR→LDR bridge with exposure/gamma/contrast/saturation/vignette.\nstruct U {\n exposure: f32,\n bloomIntensity: f32,\n gamma: f32,\n contrast: f32,\n saturation: f32,\n vignette: f32,\n _p0: f32,\n _p1: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var bloomTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\nfn acesTonemap(color: vec3<f32>) -> vec3<f32> {\n let a = 2.51; let b = 0.03; let c = 2.43; let d = 0.59; let e = 0.14;\n return clamp((color * (a * color + b)) / (color * (c * color + d) + e), vec3<f32>(0.0), vec3<f32>(1.0));\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n var color = textureSample(colorTex, samp, input.uv).rgb;\n let bloom = textureSample(bloomTex, samp, input.uv).rgb;\n color += bloom * u.bloomIntensity;\n color *= u.exposure;\n color = acesTonemap(color);\n color = (color - 0.5) * u.contrast + 0.5;\n let luma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n color = mix(vec3<f32>(luma), color, u.saturation);\n let center = input.uv - 0.5;\n let dist = dot(center, center);\n color *= 1.0 - dist * u.vignette;\n color = pow(color, vec3<f32>(1.0 / u.gamma));\n return vec4<f32>(color, 1.0);\n}\n";
|
|
54
|
+
const WATERCOLOR_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// Watercolor — stylized edge-aware smoothing + paper texture blend.\n// Combines a soft bilateral-like smoothing with edge enhancement and a\n// subtle paper grain overlay for a hand-painted watercolor look.\nstruct U {\n texelSize: vec2<f32>,\n edgeStrength: f32, // edge enhancement intensity (0–2)\n paperScale: f32, // paper texture scale (1–10)\n blend: f32, // watercolor blend amount (0–1)\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var noiseTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n// Hash for procedural paper texture\nfn hash(p: vec2<f32>) -> f32 {\n let h = dot(p, vec2<f32>(127.1, 311.7));\n return fract(sin(h) * 43758.5453);\n}\n\nfn noise2D(p: vec2<f32>) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let a = hash(i);\n let b = hash(i + vec2<f32>(1.0, 0.0));\n let c = hash(i + vec2<f32>(0.0, 1.0));\n let d = hash(i + vec2<f32>(1.0, 1.0));\n let u = f * f * (3.0 - 2.0 * f);\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n let ts = u.texelSize;\n let color = textureSample(colorTex, samp, uv).rgb;\n\n // ── Edge-aware smoothing: 3x3 weighted blur (edge-preserving) ──\n let tl = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, -1.0) * ts).rgb;\n let tr = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, -1.0) * ts).rgb;\n let bl = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 1.0) * ts).rgb;\n let br = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 1.0) * ts).rgb;\n let l = textureSample(colorTex, samp, uv + vec2<f32>(-1.0, 0.0) * ts).rgb;\n let r = textureSample(colorTex, samp, uv + vec2<f32>( 1.0, 0.0) * ts).rgb;\n let t = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, -1.0) * ts).rgb;\n let b = textureSample(colorTex, samp, uv + vec2<f32>( 0.0, 1.0) * ts).rgb;\n\n // Edge weight: closer colors get more weight (bilateral-like)\n let centerLuma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n let neighbors = array<vec3<f32>, 8>(tl, tr, bl, br, l, r, t, b);\n var smoothColor = vec3<f32>(0.0);\n var totalWeight = 0.0;\n for (var i = 0u; i < 8u; i = i + 1u) {\n let nl = dot(neighbors[i], vec3<f32>(0.299, 0.587, 0.114));\n let w = exp(-abs(centerLuma - nl) * 5.0);\n smoothColor += neighbors[i] * w;\n totalWeight += w;\n }\n smoothColor = (smoothColor + color * 2.0) / (totalWeight + 2.0);\n\n // ── Edge enhancement (Sobel) ──\n let gx = abs(dot(tr + 2.0 * r + br - tl - 2.0 * l - bl, vec3<f32>(0.299, 0.587, 0.114)));\n let gy = abs(dot(bl + 2.0 * b + br - tl - 2.0 * t - tr, vec3<f32>(0.299, 0.587, 0.114)));\n let edge = sqrt(gx * gx + gy * gy);\n let edgeFactor = 1.0 - clamp(edge * u.edgeStrength, 0.0, 1.0);\n\n // ── Paper texture ──\n let paperUV = uv * u.paperScale * 50.0;\n let paper = noise2D(paperUV) * 0.15 + 0.85;\n\n // ── Combine: smoothed color darkened at edges, multiplied by paper texture ──\n let watercolor = smoothColor * edgeFactor * paper;\n let result = mix(color, watercolor, u.blend);\n return vec4<f32>(result, 1.0);\n}\n";
|
|
55
|
+
const WHITE_BALANCE_FS = "// wgsl-validate: prelude ./fullscreen-vs.wgsl\n// wgsl-validate: prelude ./occluder-chunk.wgsl\n// White balance — temperature (K) + tint adjustment.\n// Applies a white-balance correction by shifting the color temperature\n// (warm/cool) and tint (green/magenta).\nstruct U {\n temperature: f32, // -1.0 (cool) to 1.0 (warm), 0 = neutral\n tint: f32, // -1.0 (green) to 1.0 (magenta), 0 = neutral\n _p0: f32,\n _p1: f32,\n _p2: f32,\n _p3: f32,\n _p4: f32,\n _p5: f32,\n};\n@group(0) @binding(0) var colorTex: texture_2d<f32>;\n@group(0) @binding(1) var dummyTex: texture_2d<f32>;\n@group(0) @binding(2) var samp: sampler;\n@group(0) @binding(3) var<uniform> u: U;\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n if (isOccluded(input.uv)) { discard; }\n let uv = input.uv;\n var color = textureSample(colorTex, samp, uv).rgb;\n\n // Temperature: shift warm (R+ B-) or cool (R- B+)\n let tempR = 1.0 + u.temperature * 0.1;\n let tempB = 1.0 - u.temperature * 0.1;\n // Tint: shift green (G+) or magenta (G-)\n let tintG = 1.0 - u.tint * 0.1;\n\n color = color * vec3<f32>(tempR, tintG, tempB);\n\n // Normalize luminance to avoid overall brightness shift\n let luma = dot(color, vec3<f32>(0.299, 0.587, 0.114));\n let origLuma = dot(textureSample(colorTex, samp, uv).rgb, vec3<f32>(0.299, 0.587, 0.114));\n color = color * (origLuma / max(luma, 0.0001));\n\n return vec4<f32>(color, 1.0);\n}\n";
|
|
56
|
+
const log = createLogger("info");
|
|
57
|
+
const ALL_EFFECTS = [
|
|
58
|
+
"taa", "ssao", "ssr", "dof", "motion-blur",
|
|
59
|
+
"bloom", "bloom-soft", "tonemap",
|
|
60
|
+
"lut", "white-balance", "channel-mixer", "split-tone",
|
|
61
|
+
"fxaa", "chromatic-aberration", "lens-distortion", "sharpen", "grain",
|
|
62
|
+
"sobel", "edges", "lens-flare",
|
|
63
|
+
"pixelation", "gaussian-blur", "afterimage",
|
|
64
|
+
"halftone", "dithering", "watercolor",
|
|
65
|
+
"outline", "highlight", "glow", "ascii",
|
|
66
|
+
];
|
|
67
|
+
const CHAIN_ORDER = [
|
|
68
|
+
// HDR
|
|
69
|
+
"taa", "ssao", "ssr", "dof", "motion-blur",
|
|
70
|
+
"bloom", "bloom-soft", "tonemap",
|
|
71
|
+
// Color grading (LDR, post-tonemap)
|
|
72
|
+
"lut", "white-balance", "channel-mixer", "split-tone",
|
|
73
|
+
// Camera/lens (LDR)
|
|
74
|
+
"fxaa", "chromatic-aberration", "lens-distortion", "sharpen", "grain",
|
|
75
|
+
// Stylized (LDR, late chain)
|
|
76
|
+
"sobel", "edges", "lens-flare",
|
|
77
|
+
"pixelation", "gaussian-blur", "afterimage",
|
|
78
|
+
"halftone", "dithering", "watercolor",
|
|
79
|
+
"outline", "highlight", "glow", "ascii",
|
|
80
|
+
];
|
|
81
|
+
const EFFECT_NAMES = {
|
|
82
|
+
taa: "TAA", ssao: "SSAO", ssr: "SSR", dof: "DOF", "motion-blur": "Motion Blur",
|
|
83
|
+
bloom: "Bloom", "bloom-soft": "Bloom (Soft)", tonemap: "Tonemap",
|
|
84
|
+
lut: "LUT (3D)", "white-balance": "White Balance", "channel-mixer": "Channel Mixer",
|
|
85
|
+
"split-tone": "Split Tone",
|
|
86
|
+
fxaa: "FXAA", "chromatic-aberration": "Chromatic Aberration",
|
|
87
|
+
"lens-distortion": "Lens Distortion", sharpen: "Sharpen", grain: "Grain",
|
|
88
|
+
sobel: "Sobel", edges: "Edges", "lens-flare": "Lens Flare",
|
|
89
|
+
pixelation: "Pixelation", "gaussian-blur": "Gaussian Blur", afterimage: "Afterimage",
|
|
90
|
+
halftone: "Halftone", dithering: "Dithering", watercolor: "Watercolor",
|
|
91
|
+
outline: "Outline", highlight: "Highlight", glow: "Glow", ascii: "ASCII",
|
|
92
|
+
};
|
|
93
|
+
const HDR_FORMAT = "rgba16float";
|
|
94
|
+
// ── PostProcessStack ────────────────────────────────────────────────────────
|
|
95
|
+
export class PostProcessStack {
|
|
96
|
+
device;
|
|
97
|
+
format;
|
|
98
|
+
depthFormat;
|
|
99
|
+
sceneFormat;
|
|
100
|
+
// Bind group layouts (5 shared layouts)
|
|
101
|
+
ccLayout; // float + float + filtering + uniform
|
|
102
|
+
cdLayout; // float + depth + non-filtering + uniform
|
|
103
|
+
cvvhLayout; // float + float + float + filtering + uniform
|
|
104
|
+
cvdLayout; // float + float + depth + non-filtering + uniform
|
|
105
|
+
dnfnLayout; // depth + float + float + non-filtering + uniform
|
|
106
|
+
linearSampler;
|
|
107
|
+
nearestSampler;
|
|
108
|
+
// Pipelines + uniform buffers (keyed by pipeline name)
|
|
109
|
+
pipelines = {};
|
|
110
|
+
uniforms = {};
|
|
111
|
+
// Occlusion culling — shared bind group (group 1) bound on every pass.
|
|
112
|
+
// Contains up to 8 opaque UI panel rects in UV space; shaders discard
|
|
113
|
+
// fragments inside these rects to skip work where the output is invisible.
|
|
114
|
+
occluderLayout;
|
|
115
|
+
occluderUniform;
|
|
116
|
+
occluderBindGroup;
|
|
117
|
+
occluderRects = [];
|
|
118
|
+
occluderKey = "";
|
|
119
|
+
taaHistoryReset = false;
|
|
120
|
+
// Render targets
|
|
121
|
+
sceneColor = null;
|
|
122
|
+
sceneDepth = null;
|
|
123
|
+
sceneNormals = null;
|
|
124
|
+
sceneVelocity = null;
|
|
125
|
+
sceneMask = null;
|
|
126
|
+
pingPong = [null, null];
|
|
127
|
+
// Effect-specific targets
|
|
128
|
+
afterimageTex = null;
|
|
129
|
+
taaHistory = null;
|
|
130
|
+
taaHistory2 = null;
|
|
131
|
+
bloomBright = null;
|
|
132
|
+
bloomBlurH = null;
|
|
133
|
+
bloomBlurV = null;
|
|
134
|
+
bloomMip = []; // multi-MIP pyramid (downsample cascade)
|
|
135
|
+
bloomTemp = []; // temp targets for upsample (avoid read+write same texture)
|
|
136
|
+
bloomBase = null; // full-res scene copy for the final composite (avoid read+write same texture)
|
|
137
|
+
halfResA = null; // shared by bloom-soft
|
|
138
|
+
halfResB = null;
|
|
139
|
+
ssaoA = null;
|
|
140
|
+
ssaoB = null;
|
|
141
|
+
blurA = null; // shared by gaussian-blur, highlight, glow
|
|
142
|
+
blurB = null;
|
|
143
|
+
// Static textures
|
|
144
|
+
dummyTex;
|
|
145
|
+
glyphTex = null;
|
|
146
|
+
noiseTex = null;
|
|
147
|
+
// Bloom-soft result (stored for tonemap to read)
|
|
148
|
+
bloomSoftResult = null;
|
|
149
|
+
// Dimensions
|
|
150
|
+
texW = 0;
|
|
151
|
+
texH = 0;
|
|
152
|
+
// Enabled state
|
|
153
|
+
enabled = Object.fromEntries(ALL_EFFECTS.map(e => [e, false]));
|
|
154
|
+
prevEnabled = { ...this.enabled };
|
|
155
|
+
// Custom (mod-defined) effects: id → { effect, pipeline, uniform, enabled }
|
|
156
|
+
customEffects = new Map();
|
|
157
|
+
// Settings
|
|
158
|
+
grainTime = 0;
|
|
159
|
+
lightScreenPos = [0.5, 0.5];
|
|
160
|
+
camProj = null;
|
|
161
|
+
camInvProj = null;
|
|
162
|
+
camView = null;
|
|
163
|
+
// DOF
|
|
164
|
+
dofFocusDist = 0.5;
|
|
165
|
+
dofFocusRange = 0.3;
|
|
166
|
+
dofMaxBlur = 8.0;
|
|
167
|
+
dofBokehShape = 0; // 0=circle, 1=hexagon, 2=octagon
|
|
168
|
+
dofSampleCount = 32;
|
|
169
|
+
dofNearOnly = false;
|
|
170
|
+
dofFarOnly = false;
|
|
171
|
+
dofBladeRotation = 0.0;
|
|
172
|
+
// Afterimage
|
|
173
|
+
afterimageDamp = 0.96;
|
|
174
|
+
// Bloom
|
|
175
|
+
bloomThreshold = 0.8;
|
|
176
|
+
bloomStrength = 1.0;
|
|
177
|
+
bloomMipCount = 5; // number of downsample levels (full→½→¼→⅛→1/16)
|
|
178
|
+
bloomTint = [1.0, 1.0, 1.0];
|
|
179
|
+
bloomSoftKnee = 0.7; // soft threshold knee for bright pass
|
|
180
|
+
// Per-MIP upsample weights (index 0 = largest MIP, last = smallest)
|
|
181
|
+
bloomMipWeights = [0.0, 0.0, 0.4, 0.6, 0.8, 1.0];
|
|
182
|
+
// Bloom-soft
|
|
183
|
+
bloomSoftThreshold = 1.0;
|
|
184
|
+
bloomSoftSoftThreshold = 0.5;
|
|
185
|
+
bloomSoftIntensity = 0.3;
|
|
186
|
+
// Tonemap
|
|
187
|
+
exposure = 1.0;
|
|
188
|
+
gamma = 2.2;
|
|
189
|
+
contrast = 1.0;
|
|
190
|
+
saturation = 1.0;
|
|
191
|
+
vignette = 0.3;
|
|
192
|
+
// TAA
|
|
193
|
+
taaBlendFactor = 0.1;
|
|
194
|
+
taaVarianceClamp = false;
|
|
195
|
+
taaJitterX = 0;
|
|
196
|
+
taaJitterY = 0;
|
|
197
|
+
// SSAO (GTAO)
|
|
198
|
+
ssaoRadius = 0.5;
|
|
199
|
+
ssaoBias = 0.025;
|
|
200
|
+
ssaoKernelSize = 32; // compat alias → maps to ssaoDirections
|
|
201
|
+
ssaoDirections = 4; // GTAO slice directions
|
|
202
|
+
ssaoSlices = 8; // GTAO samples per slice
|
|
203
|
+
ssaoPower = 1.5; // AO contrast power
|
|
204
|
+
ssaoThickness = 0.1; // horizon depth thickness
|
|
205
|
+
// SSR (DDA + binary refinement)
|
|
206
|
+
ssrMaxSteps = 64;
|
|
207
|
+
ssrThickness = 0.05;
|
|
208
|
+
ssrMaxDistance = 100.0;
|
|
209
|
+
ssrFadeStart = 10.0;
|
|
210
|
+
ssrFadeEnd = 50.0;
|
|
211
|
+
ssrBinarySteps = 10;
|
|
212
|
+
ssrStride = 1.0;
|
|
213
|
+
// Motion blur
|
|
214
|
+
motionBlurIntensity = 1.0;
|
|
215
|
+
motionBlurMaxSamples = 16;
|
|
216
|
+
// Sharpen
|
|
217
|
+
sharpenSharpness = 0.5;
|
|
218
|
+
// Grain
|
|
219
|
+
grainIntensity = 0.15;
|
|
220
|
+
grainSize = 2.0;
|
|
221
|
+
grainLuminanceAware = false;
|
|
222
|
+
// Edges
|
|
223
|
+
edgesThreshold = 0.4;
|
|
224
|
+
edgesOpacity = 1.0;
|
|
225
|
+
edgesEdgeColor = [1.0, 1.0, 1.0];
|
|
226
|
+
// Lens flare
|
|
227
|
+
lensFlareIntensity = 0.8;
|
|
228
|
+
lensFlareThreshold = 0.9;
|
|
229
|
+
lensFlareGhostCount = 8;
|
|
230
|
+
lensFlareGhostSpacing = 0.15;
|
|
231
|
+
lensFlareHaloWidth = 0.2;
|
|
232
|
+
lensFlareTint = [1.0, 0.9, 0.8];
|
|
233
|
+
// Pixelation
|
|
234
|
+
pixelationPixelSize = 6;
|
|
235
|
+
pixelationDepthEdgeStrength = 0.4;
|
|
236
|
+
// Gaussian blur
|
|
237
|
+
gaussianBlurRadius = 2.0;
|
|
238
|
+
// Outline
|
|
239
|
+
outlineColor = [1.0, 0.8, 0.2];
|
|
240
|
+
outlineWidth = 2.0;
|
|
241
|
+
outlineOpacity = 1.0;
|
|
242
|
+
// Highlight
|
|
243
|
+
highlightIntensity = 0.8;
|
|
244
|
+
highlightInnerOpacity = 0.3;
|
|
245
|
+
highlightBlurRadius = 3.0;
|
|
246
|
+
// Glow
|
|
247
|
+
glowIntensity = 1.0;
|
|
248
|
+
glowBlurRadius = 4.0;
|
|
249
|
+
// ASCII
|
|
250
|
+
asciiCellSize = 8;
|
|
251
|
+
asciiUseColor = 1.0;
|
|
252
|
+
// LUT (3D color grading)
|
|
253
|
+
lutTexture = null;
|
|
254
|
+
lutView = null;
|
|
255
|
+
lutSize = 32;
|
|
256
|
+
lutEnabled = true;
|
|
257
|
+
lutLayout;
|
|
258
|
+
// White balance
|
|
259
|
+
wbTemperature = 0.0;
|
|
260
|
+
wbTint = 0.0;
|
|
261
|
+
// Channel mixer
|
|
262
|
+
cmWeights = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
263
|
+
cmMonochrome = false;
|
|
264
|
+
// Split-tone
|
|
265
|
+
stShadow = [0.0, 0.0, 0.0];
|
|
266
|
+
stHighlight = [0.0, 0.0, 0.0];
|
|
267
|
+
stBalance = 0.0;
|
|
268
|
+
// Chromatic aberration
|
|
269
|
+
caIntensity = 0.5;
|
|
270
|
+
caStart = 0.0;
|
|
271
|
+
caEnd = 1.0;
|
|
272
|
+
caCenterX = 0.5;
|
|
273
|
+
caCenterY = 0.5;
|
|
274
|
+
// Lens distortion
|
|
275
|
+
ldIntensity = 0.0;
|
|
276
|
+
ldScale = 1.0;
|
|
277
|
+
ldChromaSplit = 0.0;
|
|
278
|
+
// Halftone
|
|
279
|
+
htCellSize = 8.0;
|
|
280
|
+
htDotScale = 1.0;
|
|
281
|
+
htAngle = 0.0;
|
|
282
|
+
htMonochrome = true;
|
|
283
|
+
// Dithering
|
|
284
|
+
dMode = 1; // 0=off, 1=bayer, 2=bluenoise
|
|
285
|
+
dStrength = 0.5;
|
|
286
|
+
dLevels = 0; // 0 = no quantization
|
|
287
|
+
// Watercolor
|
|
288
|
+
wcEdgeStrength = 1.0;
|
|
289
|
+
wcPaperScale = 2.0;
|
|
290
|
+
wcBlend = 0.7;
|
|
291
|
+
constructor(device, format, options) {
|
|
292
|
+
this.device = device;
|
|
293
|
+
this.format = format;
|
|
294
|
+
this.depthFormat = options?.depthFormat ?? "depth32float";
|
|
295
|
+
this.sceneFormat = options?.sceneFormat ?? HDR_FORMAT;
|
|
296
|
+
}
|
|
297
|
+
// ── Init ──────────────────────────────────────────────────────────────────
|
|
298
|
+
init() {
|
|
299
|
+
this.linearSampler = this.device.createSampler({
|
|
300
|
+
magFilter: "linear", minFilter: "linear",
|
|
301
|
+
addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge",
|
|
302
|
+
});
|
|
303
|
+
this.nearestSampler = this.device.createSampler({
|
|
304
|
+
magFilter: "nearest", minFilter: "nearest",
|
|
305
|
+
addressModeU: "clamp-to-edge", addressModeV: "clamp-to-edge",
|
|
306
|
+
});
|
|
307
|
+
// 5 shared bind group layouts
|
|
308
|
+
this.ccLayout = this.device.createBindGroupLayout({ entries: [
|
|
309
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
310
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
311
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
312
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
313
|
+
] });
|
|
314
|
+
this.cdLayout = this.device.createBindGroupLayout({ entries: [
|
|
315
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
316
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
317
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "non-filtering" } },
|
|
318
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
319
|
+
] });
|
|
320
|
+
this.cvvhLayout = this.device.createBindGroupLayout({ entries: [
|
|
321
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
322
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
323
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
324
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
325
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
326
|
+
] });
|
|
327
|
+
this.cvdLayout = this.device.createBindGroupLayout({ entries: [
|
|
328
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
329
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
330
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
331
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "non-filtering" } },
|
|
332
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
333
|
+
] });
|
|
334
|
+
this.dnfnLayout = this.device.createBindGroupLayout({ entries: [
|
|
335
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
336
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
337
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
338
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "non-filtering" } },
|
|
339
|
+
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
340
|
+
] });
|
|
341
|
+
// LUT layout: color2D + lut3D + sampler + uniform
|
|
342
|
+
this.lutLayout = this.device.createBindGroupLayout({ entries: [
|
|
343
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
344
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "3d" } },
|
|
345
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
346
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
347
|
+
] });
|
|
348
|
+
// Dummy texture (1×1 black)
|
|
349
|
+
this.dummyTex = this.device.createTexture({
|
|
350
|
+
size: [1, 1], format: "rgba8unorm",
|
|
351
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
352
|
+
});
|
|
353
|
+
this.device.queue.writeTexture({ texture: this.dummyTex }, new Uint8Array([0, 0, 0, 255]), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1]);
|
|
354
|
+
// Occlusion culling bind group (group 1, shared by all postfx pipelines).
|
|
355
|
+
// Layout: single uniform buffer at binding 0, FRAGMENT visibility.
|
|
356
|
+
// Buffer: 16-byte header (count + padding) + 8 × vec4 rects = 144 bytes.
|
|
357
|
+
this.occluderLayout = this.device.createBindGroupLayout({ entries: [
|
|
358
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
359
|
+
] });
|
|
360
|
+
this.occluderUniform = this.device.createBuffer({
|
|
361
|
+
size: 144,
|
|
362
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
363
|
+
});
|
|
364
|
+
this.occluderBindGroup = this.device.createBindGroup({
|
|
365
|
+
layout: this.occluderLayout,
|
|
366
|
+
entries: [{ binding: 0, resource: { buffer: this.occluderUniform } }],
|
|
367
|
+
});
|
|
368
|
+
// Initialize to zero occluders (no occlusion).
|
|
369
|
+
this.device.queue.writeBuffer(this.occluderUniform, 0, new Float32Array(36));
|
|
370
|
+
// Uniform buffers — 48 bytes for simple effects, 256 for matrix effects
|
|
371
|
+
const simpleKeys = [
|
|
372
|
+
"fxaa", "dof", "sobel", "afterimage", "bloom-bright", "bloom-blur", "bloom-composite",
|
|
373
|
+
"bloom-downsample", "bloom-upsample",
|
|
374
|
+
"ascii", "blit", "taa", "motion-blur", "bloom-soft", "tonemap", "sharpen", "grain",
|
|
375
|
+
"edges", "lens-flare", "pixelation", "gaussian-blur", "outline",
|
|
376
|
+
"ssao-blur", "ssao-composite", "highlight-blur", "highlight-composite",
|
|
377
|
+
"glow-blur", "glow-composite",
|
|
378
|
+
// New effects
|
|
379
|
+
"lut", "white-balance", "channel-mixer", "split-tone",
|
|
380
|
+
"chromatic-aberration", "lens-distortion",
|
|
381
|
+
"halftone", "dithering", "watercolor",
|
|
382
|
+
];
|
|
383
|
+
simpleKeys.forEach((k) => { this.uniforms[k] = this.device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); ; });
|
|
384
|
+
this.uniforms["ssao"] = this.device.createBuffer({ size: 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
385
|
+
this.uniforms["ssr"] = this.device.createBuffer({ size: 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
386
|
+
// Pipelines
|
|
387
|
+
const mk = (fs, layout, fmt = HDR_FORMAT) => this.makePipeline(fs, layout, fmt);
|
|
388
|
+
this.pipelines["fxaa"] = mk(FXAA_FS, this.ccLayout);
|
|
389
|
+
this.pipelines["dof"] = mk(DOF_FS, this.cdLayout);
|
|
390
|
+
this.pipelines["sobel"] = mk(SOBEL_FS, this.ccLayout);
|
|
391
|
+
this.pipelines["afterimage"] = mk(AFTERIMAGE_FS, this.ccLayout);
|
|
392
|
+
this.pipelines["bloom-bright"] = mk(BLOOM_BRIGHT_FS, this.ccLayout);
|
|
393
|
+
this.pipelines["bloom-blur"] = mk(BLOOM_BLUR_FS, this.ccLayout);
|
|
394
|
+
this.pipelines["bloom-composite"] = mk(BLOOM_COMPOSITE_FS, this.ccLayout);
|
|
395
|
+
this.pipelines["bloom-downsample"] = mk(BLOOM_DOWNSAMPLE_FS, this.ccLayout);
|
|
396
|
+
this.pipelines["bloom-upsample"] = mk(BLOOM_UPSAMPLE_FS, this.ccLayout);
|
|
397
|
+
this.pipelines["ascii"] = mk(ASCII_FS, this.ccLayout);
|
|
398
|
+
this.pipelines["blit"] = mk(BLIT_FS, this.ccLayout, this.format);
|
|
399
|
+
// HDR blit variant for intermediate passes inside the HDR chain section
|
|
400
|
+
this.pipelines["blit-hdr"] = mk(BLIT_FS, this.ccLayout, HDR_FORMAT);
|
|
401
|
+
this.pipelines["taa"] = mk(TAA_FS, this.cvvhLayout);
|
|
402
|
+
this.pipelines["ssao"] = mk(SSAO_FS, this.dnfnLayout);
|
|
403
|
+
this.pipelines["ssao-blur"] = mk(SSAO_BLUR_FS, this.cdLayout);
|
|
404
|
+
this.pipelines["ssao-composite"] = mk(SSAO_COMPOSITE_FS, this.ccLayout);
|
|
405
|
+
this.pipelines["ssr"] = mk(SSR_FS, this.cvdLayout);
|
|
406
|
+
this.pipelines["motion-blur"] = mk(MOTION_BLUR_FS, this.cvdLayout);
|
|
407
|
+
this.pipelines["bloom-soft"] = mk(BLOOM_SOFT_FS, this.ccLayout);
|
|
408
|
+
this.pipelines["tonemap"] = mk(TONEMAP_FS, this.ccLayout);
|
|
409
|
+
this.pipelines["sharpen"] = mk(SHARPEN_FS, this.ccLayout);
|
|
410
|
+
this.pipelines["grain"] = mk(GRAIN_FS, this.ccLayout);
|
|
411
|
+
this.pipelines["edges"] = mk(EDGES_FS, this.cvdLayout);
|
|
412
|
+
this.pipelines["lens-flare"] = mk(LENS_FLARE_FS, this.cdLayout);
|
|
413
|
+
this.pipelines["pixelation"] = mk(PIXELATION_FS, this.cdLayout);
|
|
414
|
+
this.pipelines["gaussian-blur"] = mk(GAUSSIAN_BLUR_FS, this.ccLayout);
|
|
415
|
+
this.pipelines["outline"] = mk(OUTLINE_FS, this.ccLayout);
|
|
416
|
+
this.pipelines["highlight-blur"] = mk(HIGHLIGHT_BLUR_FS, this.ccLayout);
|
|
417
|
+
this.pipelines["highlight-composite"] = mk(HIGHLIGHT_COMPOSITE_FS, this.cvvhLayout);
|
|
418
|
+
this.pipelines["glow-blur"] = mk(GLOW_BLUR_FS, this.ccLayout);
|
|
419
|
+
this.pipelines["glow-composite"] = mk(GLOW_COMPOSITE_FS, this.ccLayout);
|
|
420
|
+
// New effects
|
|
421
|
+
this.pipelines["lut"] = mk(LUT_FS, this.lutLayout);
|
|
422
|
+
this.pipelines["white-balance"] = mk(WHITE_BALANCE_FS, this.ccLayout);
|
|
423
|
+
this.pipelines["channel-mixer"] = mk(CHANNEL_MIXER_FS, this.ccLayout);
|
|
424
|
+
this.pipelines["split-tone"] = mk(SPLIT_TONE_FS, this.ccLayout);
|
|
425
|
+
this.pipelines["chromatic-aberration"] = mk(CHROMATIC_ABERRATION_FS, this.ccLayout);
|
|
426
|
+
this.pipelines["lens-distortion"] = mk(LENS_DISTORTION_FS, this.ccLayout);
|
|
427
|
+
this.pipelines["halftone"] = mk(HALFTONE_FS, this.ccLayout);
|
|
428
|
+
this.pipelines["dithering"] = mk(DITHERING_FS, this.ccLayout); // color + noise + sampler + uniform
|
|
429
|
+
this.pipelines["watercolor"] = mk(WATERCOLOR_FS, this.ccLayout); // color + noise + sampler + uniform
|
|
430
|
+
this.createGlyphAtlas();
|
|
431
|
+
this.createNoiseTexture();
|
|
432
|
+
}
|
|
433
|
+
makePipeline(fsCode, layout, targetFormat) {
|
|
434
|
+
const shader = createValidatedShaderModule(this.device, { code: VS + "\n" + OCCLUDER_CHUNK + "\n" + fsCode, label: "PostProcessStack" });
|
|
435
|
+
const pl = this.device.createPipelineLayout({ bindGroupLayouts: [layout, this.occluderLayout] });
|
|
436
|
+
return this.device.createRenderPipeline({
|
|
437
|
+
layout: pl,
|
|
438
|
+
vertex: { module: shader, entryPoint: "vs_main" },
|
|
439
|
+
fragment: { module: shader, entryPoint: "fs_main", targets: [{ format: targetFormat }] },
|
|
440
|
+
primitive: { topology: "triangle-list" },
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
createGlyphAtlas() {
|
|
444
|
+
const chars = " .:-=+*#%@";
|
|
445
|
+
const gw = 16, gh = 16, cols = chars.length;
|
|
446
|
+
const canvas = document.createElement("canvas");
|
|
447
|
+
canvas.width = cols * gw;
|
|
448
|
+
canvas.height = gh;
|
|
449
|
+
const ctx = canvas.getContext("2d");
|
|
450
|
+
ctx.fillStyle = "black";
|
|
451
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
452
|
+
ctx.fillStyle = "white";
|
|
453
|
+
ctx.font = `bold ${gh - 2}px monospace`;
|
|
454
|
+
ctx.textAlign = "center";
|
|
455
|
+
ctx.textBaseline = "middle";
|
|
456
|
+
for (let i = 0; i < chars.length; i++)
|
|
457
|
+
ctx.fillText(chars[i], i * gw + gw / 2, gh / 2);
|
|
458
|
+
this.glyphTex = this.device.createTexture({
|
|
459
|
+
size: [canvas.width, canvas.height], format: "rgba8unorm",
|
|
460
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
461
|
+
});
|
|
462
|
+
this.device.queue.copyExternalImageToTexture({ source: canvas }, { texture: this.glyphTex }, [canvas.width, canvas.height]);
|
|
463
|
+
}
|
|
464
|
+
createNoiseTexture() {
|
|
465
|
+
const size = 4;
|
|
466
|
+
const data = new Uint8Array(size * size * 4);
|
|
467
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
468
|
+
data[i] = Math.floor(Math.random() * 255);
|
|
469
|
+
data[i + 1] = Math.floor(Math.random() * 255);
|
|
470
|
+
data[i + 2] = 0;
|
|
471
|
+
data[i + 3] = 255;
|
|
472
|
+
}
|
|
473
|
+
this.noiseTex = this.device.createTexture({
|
|
474
|
+
size: [size, size], format: "rgba8unorm",
|
|
475
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
476
|
+
});
|
|
477
|
+
this.device.queue.writeTexture({ texture: this.noiseTex }, data, { bytesPerRow: size * 4 }, [size, size]);
|
|
478
|
+
}
|
|
479
|
+
// ── Public API: queries ───────────────────────────────────────────────────
|
|
480
|
+
hasEnabledEffects() {
|
|
481
|
+
if (Object.values(this.enabled).some(v => v))
|
|
482
|
+
return true;
|
|
483
|
+
for (const [, e] of this.customEffects.entries())
|
|
484
|
+
if (e.enabled)
|
|
485
|
+
return true;
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
getEnabledEffects() {
|
|
489
|
+
const builtin = CHAIN_ORDER.filter(id => this.enabled[id]).map(id => EFFECT_NAMES[id]);
|
|
490
|
+
const custom = [...this.customEffects.values()].filter(e => e.enabled).map(e => e.effect.name);
|
|
491
|
+
return [...builtin, ...custom];
|
|
492
|
+
}
|
|
493
|
+
isEnabled(id) { return this.enabled[id]; }
|
|
494
|
+
// ── Public API: occlusion culling ──────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* Set opaque UI panel rects (in UV space, 0-1, top-left origin) that the
|
|
497
|
+
* postfx shaders should skip. Up to 8 rects; extras are clamped. When the
|
|
498
|
+
* rects change, TAA history is reset for one frame to avoid stale-history
|
|
499
|
+
* artifacts in the previously-occluded region.
|
|
500
|
+
*/
|
|
501
|
+
setOccluderRects(rects) {
|
|
502
|
+
const clamped = rects.slice(0, 8);
|
|
503
|
+
const key = clamped.map(r => `${r.x.toFixed(4)},${r.y.toFixed(4)},${r.w.toFixed(4)},${r.h.toFixed(4)}`).join("|");
|
|
504
|
+
if (key !== this.occluderKey) {
|
|
505
|
+
this.occluderKey = key;
|
|
506
|
+
this.taaHistoryReset = true;
|
|
507
|
+
}
|
|
508
|
+
this.occluderRects = clamped;
|
|
509
|
+
// Build uniform data: [count, 0, 0, 0, rect0.xyzw, rect1.xyzw, ...]
|
|
510
|
+
const data = new Float32Array(36);
|
|
511
|
+
data[0] = clamped.length;
|
|
512
|
+
for (let i = 0; i < clamped.length && i < 8; i++) {
|
|
513
|
+
data[4 + i * 4] = clamped[i].x;
|
|
514
|
+
data[4 + i * 4 + 1] = clamped[i].y;
|
|
515
|
+
data[4 + i * 4 + 2] = clamped[i].w;
|
|
516
|
+
data[4 + i * 4 + 3] = clamped[i].h;
|
|
517
|
+
}
|
|
518
|
+
this.device.queue.writeBuffer(this.occluderUniform, 0, data);
|
|
519
|
+
}
|
|
520
|
+
/** Returns true if any single occluder rect covers ≥95% of UV space. */
|
|
521
|
+
isFullyOccluded() {
|
|
522
|
+
return this.occluderRects.some(r => r.w * r.h >= 0.95);
|
|
523
|
+
}
|
|
524
|
+
getOccluderRects() {
|
|
525
|
+
return this.occluderRects;
|
|
526
|
+
}
|
|
527
|
+
// ── Public API: enable/disable ────────────────────────────────────────────
|
|
528
|
+
setEnabled(id, enabled) {
|
|
529
|
+
this.enabled[id] = enabled;
|
|
530
|
+
}
|
|
531
|
+
// ── Public API: custom (mod-defined) effects ──────────────────────────────
|
|
532
|
+
/** Register a mod-defined custom effect. Creates a render pipeline from the
|
|
533
|
+
* provided WGSL fragment shader. The effect is disabled by default; call
|
|
534
|
+
* `setCustomEffectEnabled` to turn it on. Throws if the id is already
|
|
535
|
+
* registered or the WGSL fails to compile. */
|
|
536
|
+
registerCustomEffect(effect) {
|
|
537
|
+
if (this.customEffects.has(effect.id)) {
|
|
538
|
+
throw new Error(`Custom effect "${effect.id}" is already registered`);
|
|
539
|
+
}
|
|
540
|
+
if (effect.layout !== "cc") {
|
|
541
|
+
throw new Error(`Custom effect "${effect.id}": only layout "cc" is supported (got "${effect.layout}")`);
|
|
542
|
+
}
|
|
543
|
+
const uniformSize = effect.uniforms ?? 64;
|
|
544
|
+
const uniform = this.device.createBuffer({
|
|
545
|
+
size: uniformSize,
|
|
546
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
547
|
+
});
|
|
548
|
+
let pipeline;
|
|
549
|
+
try {
|
|
550
|
+
pipeline = this.makePipeline(effect.wgsl, this.ccLayout, HDR_FORMAT);
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
log.error("post-process-stack", `Failed to compile custom effect "${effect.id}": ${e}`);
|
|
554
|
+
throw e;
|
|
555
|
+
}
|
|
556
|
+
this.customEffects.set(effect.id, { effect, pipeline, uniform, uniformValues: null, enabled: false });
|
|
557
|
+
}
|
|
558
|
+
/** Unregister a custom effect. Destroys its pipeline + uniform buffer. */
|
|
559
|
+
unregisterCustomEffect(id) {
|
|
560
|
+
const e = this.customEffects.get(id);
|
|
561
|
+
if (!e)
|
|
562
|
+
return;
|
|
563
|
+
e.pipeline.destroy?.();
|
|
564
|
+
e.uniform.destroy();
|
|
565
|
+
this.customEffects.delete(id);
|
|
566
|
+
}
|
|
567
|
+
/** Enable/disable a custom effect. */
|
|
568
|
+
setCustomEffectEnabled(id, enabled) {
|
|
569
|
+
const e = this.customEffects.get(id);
|
|
570
|
+
if (!e)
|
|
571
|
+
throw new Error(`Custom effect "${id}" is not registered`);
|
|
572
|
+
e.enabled = enabled;
|
|
573
|
+
}
|
|
574
|
+
/** Check if a custom effect is enabled. */
|
|
575
|
+
isCustomEffectEnabled(id) {
|
|
576
|
+
return this.customEffects.get(id)?.enabled ?? false;
|
|
577
|
+
}
|
|
578
|
+
/** Get all registered custom effect ids. */
|
|
579
|
+
getCustomEffectIds() {
|
|
580
|
+
return [...this.customEffects.keys()];
|
|
581
|
+
}
|
|
582
|
+
/** Get a custom effect's metadata (name, settings, enabled state, uniform size). */
|
|
583
|
+
getCustomEffectInfo(id) {
|
|
584
|
+
const e = this.customEffects.get(id);
|
|
585
|
+
if (!e)
|
|
586
|
+
return null;
|
|
587
|
+
return { name: e.effect.name, enabled: e.enabled, uniforms: e.uniform.size, settings: e.effect.settings };
|
|
588
|
+
}
|
|
589
|
+
/** Get all custom effects with their metadata. */
|
|
590
|
+
getCustomEffects() {
|
|
591
|
+
return [...this.customEffects.entries()].map(([id, e]) => ({
|
|
592
|
+
id, name: e.effect.name, enabled: e.enabled, settings: e.effect.settings,
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
/** Write uniform data for a custom effect. */
|
|
596
|
+
setCustomEffectUniform(id, data) {
|
|
597
|
+
const e = this.customEffects.get(id);
|
|
598
|
+
if (!e)
|
|
599
|
+
throw new Error(`Custom effect "${id}" is not registered`);
|
|
600
|
+
// Store user settings (offset 3+); per-frame values (0-2) are written in applyCustomEffect.
|
|
601
|
+
const stored = new Float32Array(e.uniform.size / 4);
|
|
602
|
+
for (let i = 0; i < data.length && i < stored.length; i++)
|
|
603
|
+
stored[i] = data[i];
|
|
604
|
+
e.uniformValues = stored;
|
|
605
|
+
this.device.queue.writeBuffer(e.uniform, 0, stored);
|
|
606
|
+
}
|
|
607
|
+
// ── Public API: per-effect setters ────────────────────────────────────────
|
|
608
|
+
setDOFFocusDist(v) { this.dofFocusDist = v; }
|
|
609
|
+
setDOFFocusRange(v) { this.dofFocusRange = v; }
|
|
610
|
+
setDOFMaxBlur(v) { this.dofMaxBlur = v; }
|
|
611
|
+
setDOFBokehShape(v) { this.dofBokehShape = Math.floor(v); }
|
|
612
|
+
setDOFSampleCount(v) { this.dofSampleCount = Math.floor(v); }
|
|
613
|
+
setDOFNearOnly(v) { this.dofNearOnly = v; }
|
|
614
|
+
setDOFFarOnly(v) { this.dofFarOnly = v; }
|
|
615
|
+
setDOFBladeRotation(v) { this.dofBladeRotation = v; }
|
|
616
|
+
setAfterimageDamp(v) { this.afterimageDamp = v; }
|
|
617
|
+
setBloomThreshold(v) { this.bloomThreshold = v; }
|
|
618
|
+
setBloomStrength(v) { this.bloomStrength = v; }
|
|
619
|
+
setBloomMipCount(v) { this.bloomMipCount = Math.max(1, Math.min(6, Math.floor(v))); }
|
|
620
|
+
setBloomTint(r, g, b) { this.bloomTint = [r, g, b]; }
|
|
621
|
+
setBloomSoftKnee(v) { this.bloomSoftKnee = v; }
|
|
622
|
+
setBloomMipWeights(weights) { this.bloomMipWeights = weights; }
|
|
623
|
+
setBloomSoftThreshold(v) { this.bloomSoftThreshold = v; }
|
|
624
|
+
setBloomSoftSoftThreshold(v) { this.bloomSoftSoftThreshold = v; }
|
|
625
|
+
setBloomSoftIntensity(v) { this.bloomSoftIntensity = v; }
|
|
626
|
+
setExposure(v) { this.exposure = v; }
|
|
627
|
+
setGamma(v) { this.gamma = v; }
|
|
628
|
+
setContrast(v) { this.contrast = v; }
|
|
629
|
+
setSaturation(v) { this.saturation = v; }
|
|
630
|
+
setVignette(v) { this.vignette = v; }
|
|
631
|
+
setTAABlendFactor(v) { this.taaBlendFactor = v; }
|
|
632
|
+
setTAAVarianceClamp(v) { this.taaVarianceClamp = v; }
|
|
633
|
+
setJitter(x, y) { this.taaJitterX = x; this.taaJitterY = y; }
|
|
634
|
+
setSSAORadius(v) { this.ssaoRadius = v; }
|
|
635
|
+
setSSAOBias(v) { this.ssaoBias = v; }
|
|
636
|
+
setSSAOKernelSize(v) { this.ssaoKernelSize = Math.floor(v); this.ssaoDirections = Math.floor(v); }
|
|
637
|
+
setSSAODirections(v) { this.ssaoDirections = Math.floor(v); }
|
|
638
|
+
setSSAOSlices(v) { this.ssaoSlices = Math.floor(v); }
|
|
639
|
+
setSSAOPower(v) { this.ssaoPower = v; }
|
|
640
|
+
setSSAOThickness(v) { this.ssaoThickness = v; }
|
|
641
|
+
setSSRMaxSteps(v) { this.ssrMaxSteps = Math.floor(v); }
|
|
642
|
+
setSSRThickness(v) { this.ssrThickness = v; }
|
|
643
|
+
setSSRMaxDistance(v) { this.ssrMaxDistance = v; }
|
|
644
|
+
setSSRFadeStart(v) { this.ssrFadeStart = v; }
|
|
645
|
+
setSSRFadeEnd(v) { this.ssrFadeEnd = v; }
|
|
646
|
+
setSSRBinarySteps(v) { this.ssrBinarySteps = Math.floor(v); }
|
|
647
|
+
setSSRStride(v) { this.ssrStride = v; }
|
|
648
|
+
setMotionBlurIntensity(v) { this.motionBlurIntensity = v; }
|
|
649
|
+
setMotionBlurMaxSamples(v) { this.motionBlurMaxSamples = Math.floor(v); }
|
|
650
|
+
setSharpenSharpness(v) { this.sharpenSharpness = v; }
|
|
651
|
+
setGrainIntensity(v) { this.grainIntensity = v; }
|
|
652
|
+
setGrainSize(v) { this.grainSize = v; }
|
|
653
|
+
setGrainLuminanceAware(v) { this.grainLuminanceAware = v; }
|
|
654
|
+
setEdgesThreshold(v) { this.edgesThreshold = v; }
|
|
655
|
+
setEdgesOpacity(v) { this.edgesOpacity = v; }
|
|
656
|
+
setEdgesEdgeColor(r, g, b) { this.edgesEdgeColor = [r, g, b]; }
|
|
657
|
+
setLensFlareIntensity(v) { this.lensFlareIntensity = v; }
|
|
658
|
+
setLensFlareThreshold(v) { this.lensFlareThreshold = v; }
|
|
659
|
+
setLensFlareGhostCount(v) { this.lensFlareGhostCount = Math.floor(v); }
|
|
660
|
+
setLensFlareGhostSpacing(v) { this.lensFlareGhostSpacing = v; }
|
|
661
|
+
setLensFlareHaloWidth(v) { this.lensFlareHaloWidth = v; }
|
|
662
|
+
setLensFlareTint(r, g, b) { this.lensFlareTint = [r, g, b]; }
|
|
663
|
+
setPixelationPixelSize(v) { this.pixelationPixelSize = Math.max(1, Math.floor(v)); }
|
|
664
|
+
getPixelationPixelSize() { return this.pixelationPixelSize; }
|
|
665
|
+
setPixelationDepthEdgeStrength(v) { this.pixelationDepthEdgeStrength = v; }
|
|
666
|
+
setGaussianBlurRadius(v) { this.gaussianBlurRadius = v; }
|
|
667
|
+
setOutlineColor(r, g, b) { this.outlineColor = [r, g, b]; }
|
|
668
|
+
setOutlineWidth(v) { this.outlineWidth = v; }
|
|
669
|
+
setOutlineOpacity(v) { this.outlineOpacity = v; }
|
|
670
|
+
setHighlightIntensity(v) { this.highlightIntensity = v; }
|
|
671
|
+
setHighlightInnerOpacity(v) { this.highlightInnerOpacity = v; }
|
|
672
|
+
setHighlightBlurRadius(v) { this.highlightBlurRadius = v; }
|
|
673
|
+
setGlowIntensity(v) { this.glowIntensity = v; }
|
|
674
|
+
setGlowBlurRadius(v) { this.glowBlurRadius = v; }
|
|
675
|
+
setASCIICellSize(v) { this.asciiCellSize = Math.max(2, Math.floor(v)); }
|
|
676
|
+
setASCIIUseColor(v) { this.asciiUseColor = v ? 1.0 : 0.0; }
|
|
677
|
+
// ── New effect setters ──
|
|
678
|
+
setLUT(data, size) {
|
|
679
|
+
this.lutSize = size;
|
|
680
|
+
this.lutTexture?.destroy();
|
|
681
|
+
this.lutTexture = this.device.createTexture({
|
|
682
|
+
dimension: "3d",
|
|
683
|
+
size: [size, size, size], format: "rgba8unorm",
|
|
684
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
685
|
+
});
|
|
686
|
+
// Copy into a fresh ArrayBuffer to satisfy GPUAllowSharedBufferSource typing
|
|
687
|
+
const buf = new Uint8Array(data.length);
|
|
688
|
+
buf.set(data);
|
|
689
|
+
this.device.queue.writeTexture({ texture: this.lutTexture }, buf.buffer, { bytesPerRow: size * 4, rowsPerImage: size }, [size, size, size]);
|
|
690
|
+
this.lutView = this.lutTexture.createView({ dimension: "3d" });
|
|
691
|
+
}
|
|
692
|
+
setLUTEnabled(v) { this.lutEnabled = v; }
|
|
693
|
+
setWhiteBalance(temperature, tint) { this.wbTemperature = temperature; this.wbTint = tint; }
|
|
694
|
+
setChannelMixer(rr, rg, rb, gr, gg, gb, br, bg, bb) {
|
|
695
|
+
this.cmWeights = [rr, rg, rb, gr, gg, gb, br, bg, bb];
|
|
696
|
+
}
|
|
697
|
+
setChannelMixerMonochrome(v) { this.cmMonochrome = v; }
|
|
698
|
+
setSplitTone(shadowR, shadowG, shadowB, highlightR, highlightG, highlightB, balance) {
|
|
699
|
+
this.stShadow = [shadowR, shadowG, shadowB];
|
|
700
|
+
this.stHighlight = [highlightR, highlightG, highlightB];
|
|
701
|
+
this.stBalance = balance;
|
|
702
|
+
}
|
|
703
|
+
setChromaticAberration(intensity, start, end) {
|
|
704
|
+
this.caIntensity = intensity;
|
|
705
|
+
this.caStart = start;
|
|
706
|
+
this.caEnd = end;
|
|
707
|
+
}
|
|
708
|
+
setChromaticAberrationCenter(x, y) { this.caCenterX = x; this.caCenterY = y; }
|
|
709
|
+
setLensDistortion(intensity, scale, chromaSplit) {
|
|
710
|
+
this.ldIntensity = intensity;
|
|
711
|
+
this.ldScale = scale;
|
|
712
|
+
this.ldChromaSplit = chromaSplit;
|
|
713
|
+
}
|
|
714
|
+
setHalftone(cellSize, dotScale, angle, monochrome) {
|
|
715
|
+
this.htCellSize = cellSize;
|
|
716
|
+
this.htDotScale = dotScale;
|
|
717
|
+
this.htAngle = angle;
|
|
718
|
+
this.htMonochrome = monochrome;
|
|
719
|
+
}
|
|
720
|
+
setDithering(mode, strength, levels) {
|
|
721
|
+
this.dMode = Math.floor(mode);
|
|
722
|
+
this.dStrength = strength;
|
|
723
|
+
this.dLevels = Math.floor(levels);
|
|
724
|
+
}
|
|
725
|
+
setWatercolor(edgeStrength, paperScale, blend) {
|
|
726
|
+
this.wcEdgeStrength = edgeStrength;
|
|
727
|
+
this.wcPaperScale = paperScale;
|
|
728
|
+
this.wcBlend = blend;
|
|
729
|
+
}
|
|
730
|
+
// ── Public API: per-frame inputs ──────────────────────────────────────────
|
|
731
|
+
update(dt) {
|
|
732
|
+
this.grainTime += dt;
|
|
733
|
+
}
|
|
734
|
+
setCameraMatrices(proj, invProj, view) {
|
|
735
|
+
this.camProj = proj;
|
|
736
|
+
this.camInvProj = invProj;
|
|
737
|
+
this.camView = view;
|
|
738
|
+
}
|
|
739
|
+
setLightScreenPos(x, y) {
|
|
740
|
+
this.lightScreenPos = [x, y];
|
|
741
|
+
}
|
|
742
|
+
// ── Target management ─────────────────────────────────────────────────────
|
|
743
|
+
needsNormals() { return this.enabled["edges"] || this.enabled["ssao"] || this.enabled["ssr"]; }
|
|
744
|
+
needsVelocity() { return this.enabled["taa"] || this.enabled["motion-blur"]; }
|
|
745
|
+
needsMask() { return this.enabled["outline"] || this.enabled["highlight"] || this.enabled["glow"]; }
|
|
746
|
+
effectsChanged() {
|
|
747
|
+
for (let _i = 0, _it = ALL_EFFECTS, _n = _it.length; _i < _n; _i++) {
|
|
748
|
+
const id = _it[_i];
|
|
749
|
+
if (this.enabled[id] !== this.prevEnabled[id]) {
|
|
750
|
+
this.prevEnabled = { ...this.enabled };
|
|
751
|
+
return true;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
ensureTargets(w, h) {
|
|
757
|
+
const sizeChanged = (w !== this.texW || h !== this.texH || !this.sceneColor);
|
|
758
|
+
const fxChanged = this.effectsChanged();
|
|
759
|
+
if (!sizeChanged && !fxChanged)
|
|
760
|
+
return;
|
|
761
|
+
if (sizeChanged) {
|
|
762
|
+
this.destroyAllTargets();
|
|
763
|
+
const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST;
|
|
764
|
+
this.sceneColor = this.device.createTexture({ size: [w, h], format: this.sceneFormat, usage });
|
|
765
|
+
this.sceneDepth = this.device.createTexture({ size: [w, h], format: this.depthFormat, usage });
|
|
766
|
+
this.pingPong[0] = this.device.createTexture({ size: [w, h], format: HDR_FORMAT, usage });
|
|
767
|
+
this.pingPong[1] = this.device.createTexture({ size: [w, h], format: HDR_FORMAT, usage });
|
|
768
|
+
}
|
|
769
|
+
// Always (re)create optional targets when effects or size change
|
|
770
|
+
this.destroyOptionalTargets();
|
|
771
|
+
this.createOptionalTargets(w, h);
|
|
772
|
+
this.texW = w;
|
|
773
|
+
this.texH = h;
|
|
774
|
+
}
|
|
775
|
+
createOptionalTargets(w, h) {
|
|
776
|
+
const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST;
|
|
777
|
+
const mkTex = (fmt, tw, th) => this.device.createTexture({ size: [tw, th], format: fmt, usage });
|
|
778
|
+
if (this.needsNormals())
|
|
779
|
+
this.sceneNormals = mkTex("rgba8unorm", w, h);
|
|
780
|
+
if (this.needsVelocity())
|
|
781
|
+
this.sceneVelocity = mkTex("rg16float", w, h);
|
|
782
|
+
if (this.needsMask())
|
|
783
|
+
this.sceneMask = mkTex("rgba8unorm", w, h);
|
|
784
|
+
if (this.enabled["taa"]) {
|
|
785
|
+
this.taaHistory = mkTex(HDR_FORMAT, w, h);
|
|
786
|
+
this.taaHistory2 = mkTex(HDR_FORMAT, w, h);
|
|
787
|
+
}
|
|
788
|
+
if (this.enabled["afterimage"])
|
|
789
|
+
this.afterimageTex = mkTex(HDR_FORMAT, w, h);
|
|
790
|
+
if (this.enabled["bloom"]) {
|
|
791
|
+
const hw = Math.max(1, w >> 1), hh = Math.max(1, h >> 1);
|
|
792
|
+
this.bloomBright = mkTex(HDR_FORMAT, hw, hh);
|
|
793
|
+
this.bloomBlurH = mkTex(HDR_FORMAT, hw, hh);
|
|
794
|
+
this.bloomBlurV = mkTex(HDR_FORMAT, hw, hh);
|
|
795
|
+
this.bloomBase = mkTex(HDR_FORMAT, w, h);
|
|
796
|
+
// Multi-MIP pyramid: bloomMip[0] = ½res, [1] = ¼res, [2] = ⅛res, [3] = 1/16, [4] = 1/32
|
|
797
|
+
this.bloomMip = [];
|
|
798
|
+
this.bloomTemp = [];
|
|
799
|
+
let mw = hw, mh = hh;
|
|
800
|
+
for (let i = 0; i < this.bloomMipCount; i++) {
|
|
801
|
+
this.bloomMip.push(mkTex(HDR_FORMAT, mw, mh));
|
|
802
|
+
this.bloomTemp.push(mkTex(HDR_FORMAT, mw, mh));
|
|
803
|
+
mw = Math.max(1, mw >> 1);
|
|
804
|
+
mh = Math.max(1, mh >> 1);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (this.enabled["bloom-soft"]) {
|
|
808
|
+
const hw = Math.max(1, w >> 1), hh = Math.max(1, h >> 1);
|
|
809
|
+
this.halfResA = mkTex(HDR_FORMAT, hw, hh);
|
|
810
|
+
this.halfResB = mkTex(HDR_FORMAT, hw, hh);
|
|
811
|
+
}
|
|
812
|
+
if (this.enabled["ssao"]) {
|
|
813
|
+
this.ssaoA = mkTex(HDR_FORMAT, w, h);
|
|
814
|
+
this.ssaoB = mkTex(HDR_FORMAT, w, h);
|
|
815
|
+
}
|
|
816
|
+
if (this.enabled["gaussian-blur"]) {
|
|
817
|
+
this.blurA = mkTex(HDR_FORMAT, w, h);
|
|
818
|
+
this.blurB = mkTex(HDR_FORMAT, w, h);
|
|
819
|
+
}
|
|
820
|
+
if (this.enabled["highlight"]) {
|
|
821
|
+
this.blurA = this.blurA ?? mkTex(HDR_FORMAT, w, h);
|
|
822
|
+
this.blurB = this.blurB ?? mkTex(HDR_FORMAT, w, h);
|
|
823
|
+
}
|
|
824
|
+
if (this.enabled["glow"]) {
|
|
825
|
+
this.blurA = this.blurA ?? mkTex(HDR_FORMAT, w, h);
|
|
826
|
+
this.blurB = this.blurB ?? mkTex(HDR_FORMAT, w, h);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
destroyOptionalTargets() {
|
|
830
|
+
const destroy = (t) => { t?.destroy(); };
|
|
831
|
+
destroy(this.sceneNormals);
|
|
832
|
+
this.sceneNormals = null;
|
|
833
|
+
destroy(this.sceneVelocity);
|
|
834
|
+
this.sceneVelocity = null;
|
|
835
|
+
destroy(this.sceneMask);
|
|
836
|
+
this.sceneMask = null;
|
|
837
|
+
destroy(this.taaHistory);
|
|
838
|
+
this.taaHistory = null;
|
|
839
|
+
destroy(this.taaHistory2);
|
|
840
|
+
this.taaHistory2 = null;
|
|
841
|
+
destroy(this.afterimageTex);
|
|
842
|
+
this.afterimageTex = null;
|
|
843
|
+
destroy(this.bloomBright);
|
|
844
|
+
this.bloomBright = null;
|
|
845
|
+
destroy(this.bloomBlurH);
|
|
846
|
+
this.bloomBlurH = null;
|
|
847
|
+
destroy(this.bloomBlurV);
|
|
848
|
+
this.bloomBlurV = null;
|
|
849
|
+
this.bloomMip.forEach((m) => { m.destroy(); ; });
|
|
850
|
+
this.bloomMip = [];
|
|
851
|
+
this.bloomTemp.forEach((m) => { m.destroy(); ; });
|
|
852
|
+
this.bloomTemp = [];
|
|
853
|
+
destroy(this.bloomBase);
|
|
854
|
+
this.bloomBase = null;
|
|
855
|
+
destroy(this.halfResA);
|
|
856
|
+
this.halfResA = null;
|
|
857
|
+
destroy(this.halfResB);
|
|
858
|
+
this.halfResB = null;
|
|
859
|
+
destroy(this.ssaoA);
|
|
860
|
+
this.ssaoA = null;
|
|
861
|
+
destroy(this.ssaoB);
|
|
862
|
+
this.ssaoB = null;
|
|
863
|
+
destroy(this.blurA);
|
|
864
|
+
this.blurA = null;
|
|
865
|
+
destroy(this.blurB);
|
|
866
|
+
this.blurB = null;
|
|
867
|
+
this.bloomSoftResult = null;
|
|
868
|
+
}
|
|
869
|
+
destroyAllTargets() {
|
|
870
|
+
this.clearResourceCaches();
|
|
871
|
+
this.destroyOptionalTargets();
|
|
872
|
+
this.sceneColor?.destroy();
|
|
873
|
+
this.sceneColor = null;
|
|
874
|
+
this.sceneDepth?.destroy();
|
|
875
|
+
this.sceneDepth = null;
|
|
876
|
+
this.pingPong[0]?.destroy();
|
|
877
|
+
this.pingPong[0] = null;
|
|
878
|
+
this.pingPong[1]?.destroy();
|
|
879
|
+
this.pingPong[1] = null;
|
|
880
|
+
}
|
|
881
|
+
// ── Target getters (game renders into these) ──────────────────────────────
|
|
882
|
+
getSceneColorView() {
|
|
883
|
+
if (!this.sceneColor)
|
|
884
|
+
throw new Error("PostProcess targets not created");
|
|
885
|
+
return this.viewOf(this.sceneColor);
|
|
886
|
+
}
|
|
887
|
+
getSceneColorTexture() {
|
|
888
|
+
if (!this.sceneColor)
|
|
889
|
+
throw new Error("PostProcess targets not created");
|
|
890
|
+
return this.sceneColor;
|
|
891
|
+
}
|
|
892
|
+
getSceneDepthView() {
|
|
893
|
+
if (!this.sceneDepth)
|
|
894
|
+
throw new Error("PostProcess targets not created");
|
|
895
|
+
return this.viewOf(this.sceneDepth);
|
|
896
|
+
}
|
|
897
|
+
getSceneNormalsView() {
|
|
898
|
+
if (!this.sceneNormals)
|
|
899
|
+
throw new Error("Normals target not created — enable edges/ssao/ssr first");
|
|
900
|
+
return this.viewOf(this.sceneNormals);
|
|
901
|
+
}
|
|
902
|
+
getSceneVelocityView() {
|
|
903
|
+
if (!this.sceneVelocity)
|
|
904
|
+
throw new Error("Velocity target not created — enable taa/motion-blur first");
|
|
905
|
+
return this.viewOf(this.sceneVelocity);
|
|
906
|
+
}
|
|
907
|
+
getSceneMaskView() {
|
|
908
|
+
if (!this.sceneMask)
|
|
909
|
+
throw new Error("Mask target not created — enable outline/highlight/glow first");
|
|
910
|
+
return this.viewOf(this.sceneMask);
|
|
911
|
+
}
|
|
912
|
+
// ── Main chain ────────────────────────────────────────────────────────────
|
|
913
|
+
applyChain(encoder, depthView, canvasView, w, h, time = 0) {
|
|
914
|
+
const active = CHAIN_ORDER.filter(id => this.enabled[id]);
|
|
915
|
+
// Collect enabled custom effects grouped by order.
|
|
916
|
+
const customByOrder = {
|
|
917
|
+
"hdr": [], "color-grading": [], "camera": [], "stylized": [],
|
|
918
|
+
};
|
|
919
|
+
for (const [id, e] of this.customEffects.entries()) {
|
|
920
|
+
if (e.enabled)
|
|
921
|
+
customByOrder[e.effect.order].push(id);
|
|
922
|
+
}
|
|
923
|
+
const hasCustom = (customByOrder["hdr"].length + customByOrder["color-grading"].length +
|
|
924
|
+
customByOrder["camera"].length + customByOrder["stylized"].length) > 0;
|
|
925
|
+
if (active.length === 0 && !hasCustom) {
|
|
926
|
+
// Empty chain: still blit the scene color to the canvas. Callers that
|
|
927
|
+
// render scene pipelines compiled for the HDR scene format must always
|
|
928
|
+
// go through this stack — a direct-to-surface fallback binds those
|
|
929
|
+
// pipelines against the swapchain format (e.g. bgra8unorm), which is a
|
|
930
|
+
// validation error on native and renders nothing on Chrome.
|
|
931
|
+
if (this.sceneColor)
|
|
932
|
+
this.applyBlit(encoder, this.viewOf(this.sceneColor), canvasView, w, h);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const dv = depthView ?? (this.sceneDepth ? this.viewOf(this.sceneDepth) : undefined) ?? null;
|
|
936
|
+
let inputView = this.viewOf(this.sceneColor);
|
|
937
|
+
let pingIdx = 0;
|
|
938
|
+
// Build a combined chain: built-in effects with custom effects inserted
|
|
939
|
+
// at the end of each order group. The CHAIN_ORDER groups are:
|
|
940
|
+
// [0..7] = HDR (taa..tonemap)
|
|
941
|
+
// [8..11] = Color grading (lut..split-tone)
|
|
942
|
+
// [12..16] = Camera/lens (fxaa..grain)
|
|
943
|
+
// [17..end] = Stylized (sobel..ascii)
|
|
944
|
+
const HDR_END = 8, COLOR_END = 12, CAMERA_END = 17;
|
|
945
|
+
const chain = [];
|
|
946
|
+
// Iterate the full CHAIN_ORDER so custom effects are inserted at the
|
|
947
|
+
// correct group boundaries even when few built-in effects are enabled.
|
|
948
|
+
for (let i = 0; i < CHAIN_ORDER.length; i++) {
|
|
949
|
+
if (this.enabled[CHAIN_ORDER[i]]) {
|
|
950
|
+
chain.push({ type: "builtin", id: CHAIN_ORDER[i] });
|
|
951
|
+
}
|
|
952
|
+
// Insert custom effects at group boundaries (after the last effect in each group).
|
|
953
|
+
if (i === HDR_END - 1)
|
|
954
|
+
customByOrder["hdr"].forEach((cid) => { chain.push({ type: "custom", id: cid }); ; });
|
|
955
|
+
if (i === COLOR_END - 1)
|
|
956
|
+
customByOrder["color-grading"].forEach((cid) => { chain.push({ type: "custom", id: cid }); ; });
|
|
957
|
+
if (i === CAMERA_END - 1)
|
|
958
|
+
customByOrder["camera"].forEach((cid) => { chain.push({ type: "custom", id: cid }); ; });
|
|
959
|
+
}
|
|
960
|
+
// Stylized custom effects run after all built-in stylized effects.
|
|
961
|
+
customByOrder["stylized"].forEach((cid) => { chain.push({ type: "custom", id: cid }); ; });
|
|
962
|
+
// If there are no built-in effects but there are custom effects, run them.
|
|
963
|
+
if (active.length === 0) {
|
|
964
|
+
["hdr", "color-grading", "camera", "stylized"].forEach((order) => {
|
|
965
|
+
customByOrder[order].forEach((cid) => { chain.push({ type: "custom", id: cid }); ; });
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
chain.forEach((entry) => {
|
|
969
|
+
const outputTex = this.pingPong[pingIdx];
|
|
970
|
+
const outputView = this.viewOf(outputTex);
|
|
971
|
+
if (entry.type === "builtin") {
|
|
972
|
+
this.applyEffect(encoder, entry.id, inputView, outputView, dv, w, h);
|
|
973
|
+
if (entry.id === "afterimage") {
|
|
974
|
+
encoder.copyTextureToTexture({ texture: outputTex }, { texture: this.afterimageTex }, [w, h]);
|
|
975
|
+
}
|
|
976
|
+
else if (entry.id === "taa") {
|
|
977
|
+
encoder.copyTextureToTexture({ texture: outputTex }, { texture: this.taaHistory2 }, [w, h]);
|
|
978
|
+
const tmp = this.taaHistory;
|
|
979
|
+
this.taaHistory = this.taaHistory2;
|
|
980
|
+
this.taaHistory2 = tmp;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
this.applyCustomEffect(encoder, entry.id, inputView, outputView, w, h, time);
|
|
985
|
+
}
|
|
986
|
+
inputView = outputView;
|
|
987
|
+
pingIdx = 1 - pingIdx;
|
|
988
|
+
});
|
|
989
|
+
// Blit final result to canvas
|
|
990
|
+
this.applyBlit(encoder, inputView, canvasView, w, h);
|
|
991
|
+
}
|
|
992
|
+
/** Apply a custom (mod-defined) effect as a full-screen pass. */
|
|
993
|
+
applyCustomEffect(encoder, id, inputView, outputView, w, h, time) {
|
|
994
|
+
const e = this.customEffects.get(id);
|
|
995
|
+
if (!e)
|
|
996
|
+
return;
|
|
997
|
+
// Write per-frame values (inv_w, inv_h, time) to the first 3 floats.
|
|
998
|
+
// User settings start at offset 3 (set via setCustomEffectUniform).
|
|
999
|
+
const perFrame = new Float32Array(e.uniform.size / 4);
|
|
1000
|
+
perFrame[0] = 1 / w;
|
|
1001
|
+
perFrame[1] = 1 / h;
|
|
1002
|
+
perFrame[2] = time;
|
|
1003
|
+
// Preserve existing user settings (offset 3+).
|
|
1004
|
+
const existing = e.uniformValues;
|
|
1005
|
+
if (existing) {
|
|
1006
|
+
for (let i = 3; i < existing.length && i < perFrame.length; i++) {
|
|
1007
|
+
perFrame[i] = existing[i];
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
e.uniformValues = perFrame;
|
|
1011
|
+
this.device.queue.writeBuffer(e.uniform, 0, perFrame);
|
|
1012
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1013
|
+
{ binding: 0, resource: inputView },
|
|
1014
|
+
{ binding: 1, resource: this.viewOf(this.dummyTex) },
|
|
1015
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1016
|
+
{ binding: 3, resource: { buffer: e.uniform } },
|
|
1017
|
+
]);
|
|
1018
|
+
this.pass(encoder, e.pipeline, bg, outputView, w, h);
|
|
1019
|
+
}
|
|
1020
|
+
applyEffect(encoder, effect, inputView, outputView, depthView, w, h) {
|
|
1021
|
+
switch (effect) {
|
|
1022
|
+
case "fxaa":
|
|
1023
|
+
this.applyCC(encoder, "fxaa", inputView, outputView, w, h, [1 / w, 1 / h]);
|
|
1024
|
+
break;
|
|
1025
|
+
case "sobel":
|
|
1026
|
+
this.applyCC(encoder, "sobel", inputView, outputView, w, h, [1 / w, 1 / h]);
|
|
1027
|
+
break;
|
|
1028
|
+
case "sharpen":
|
|
1029
|
+
this.applyCC(encoder, "sharpen", inputView, outputView, w, h, [1 / w, 1 / h, this.sharpenSharpness]);
|
|
1030
|
+
break;
|
|
1031
|
+
case "grain":
|
|
1032
|
+
this.applyCC(encoder, "grain", inputView, outputView, w, h, [1 / w, 1 / h, this.grainIntensity, this.grainSize, this.grainLuminanceAware ? 1 : 0, this.grainTime]);
|
|
1033
|
+
break;
|
|
1034
|
+
case "dof":
|
|
1035
|
+
this.applyCD(encoder, "dof", inputView, outputView, depthView, w, h, [1 / w, 1 / h, this.dofFocusDist, this.dofFocusRange, this.dofMaxBlur, this.dofBokehShape, this.dofSampleCount, this.dofNearOnly ? 1 : 0, this.dofFarOnly ? 1 : 0, this.dofBladeRotation, 0, 0, 0, 0, 0, 0]);
|
|
1036
|
+
break;
|
|
1037
|
+
case "pixelation":
|
|
1038
|
+
this.applyCD(encoder, "pixelation", inputView, outputView, depthView, w, h, [1 / w, 1 / h, this.pixelationPixelSize, this.pixelationDepthEdgeStrength, w, h]);
|
|
1039
|
+
break;
|
|
1040
|
+
case "lens-flare":
|
|
1041
|
+
this.applyCD(encoder, "lens-flare", inputView, outputView, depthView, w, h, [this.lightScreenPos[0], this.lightScreenPos[1], this.lensFlareIntensity, this.lensFlareThreshold, this.lensFlareGhostCount, this.lensFlareGhostSpacing, this.lensFlareHaloWidth, 16, 0, 0, 0, 0, ...this.lensFlareTint, 0]);
|
|
1042
|
+
break;
|
|
1043
|
+
case "afterimage":
|
|
1044
|
+
this.applyAfterimage(encoder, inputView, outputView, w, h);
|
|
1045
|
+
break;
|
|
1046
|
+
case "ascii":
|
|
1047
|
+
this.applyASCII(encoder, inputView, outputView, w, h);
|
|
1048
|
+
break;
|
|
1049
|
+
case "bloom":
|
|
1050
|
+
this.applyBloom(encoder, inputView, outputView, w, h);
|
|
1051
|
+
break;
|
|
1052
|
+
case "bloom-soft":
|
|
1053
|
+
this.applyBloomSoft(encoder, inputView, outputView, w, h);
|
|
1054
|
+
break;
|
|
1055
|
+
case "tonemap":
|
|
1056
|
+
this.applyTonemap(encoder, inputView, outputView, w, h);
|
|
1057
|
+
break;
|
|
1058
|
+
case "taa":
|
|
1059
|
+
this.applyTAA(encoder, inputView, outputView, w, h);
|
|
1060
|
+
break;
|
|
1061
|
+
case "ssao":
|
|
1062
|
+
this.applySSAO(encoder, inputView, outputView, depthView, w, h);
|
|
1063
|
+
break;
|
|
1064
|
+
case "ssr":
|
|
1065
|
+
this.applySSR(encoder, inputView, outputView, depthView, w, h);
|
|
1066
|
+
break;
|
|
1067
|
+
case "motion-blur":
|
|
1068
|
+
this.applyMotionBlur(encoder, inputView, outputView, depthView, w, h);
|
|
1069
|
+
break;
|
|
1070
|
+
case "edges":
|
|
1071
|
+
this.applyEdges(encoder, inputView, outputView, depthView, w, h);
|
|
1072
|
+
break;
|
|
1073
|
+
case "gaussian-blur":
|
|
1074
|
+
this.applyGaussianBlur(encoder, inputView, outputView, w, h);
|
|
1075
|
+
break;
|
|
1076
|
+
case "outline":
|
|
1077
|
+
this.applyOutline(encoder, inputView, outputView, w, h);
|
|
1078
|
+
break;
|
|
1079
|
+
case "highlight":
|
|
1080
|
+
this.applyHighlight(encoder, inputView, outputView, w, h);
|
|
1081
|
+
break;
|
|
1082
|
+
case "glow":
|
|
1083
|
+
this.applyGlow(encoder, inputView, outputView, w, h);
|
|
1084
|
+
break;
|
|
1085
|
+
// New effects
|
|
1086
|
+
case "lut":
|
|
1087
|
+
this.applyLUT(encoder, inputView, outputView, w, h);
|
|
1088
|
+
break;
|
|
1089
|
+
case "white-balance":
|
|
1090
|
+
this.applyCC(encoder, "white-balance", inputView, outputView, w, h, [this.wbTemperature, this.wbTint, 0, 0, 0, 0, 0, 0]);
|
|
1091
|
+
break;
|
|
1092
|
+
case "channel-mixer":
|
|
1093
|
+
this.applyCC(encoder, "channel-mixer", inputView, outputView, w, h, [...this.cmWeights, this.cmMonochrome ? 1 : 0, 0, 0, 0]);
|
|
1094
|
+
break;
|
|
1095
|
+
case "split-tone":
|
|
1096
|
+
this.applyCC(encoder, "split-tone", inputView, outputView, w, h, [...this.stShadow, ...this.stHighlight, this.stBalance, 0]);
|
|
1097
|
+
break;
|
|
1098
|
+
case "chromatic-aberration":
|
|
1099
|
+
this.applyCC(encoder, "chromatic-aberration", inputView, outputView, w, h, [1 / w, 1 / h, this.caIntensity, this.caStart, this.caEnd, this.caCenterX, this.caCenterY, 0]);
|
|
1100
|
+
break;
|
|
1101
|
+
case "lens-distortion":
|
|
1102
|
+
this.applyCC(encoder, "lens-distortion", inputView, outputView, w, h, [1 / w, 1 / h, this.ldIntensity, this.ldScale, this.ldChromaSplit, 0, 0, 0]);
|
|
1103
|
+
break;
|
|
1104
|
+
case "halftone":
|
|
1105
|
+
this.applyCC(encoder, "halftone", inputView, outputView, w, h, [1 / w, 1 / h, this.htCellSize, this.htDotScale, this.htAngle, this.htMonochrome ? 1 : 0, 0, 0]);
|
|
1106
|
+
break;
|
|
1107
|
+
case "dithering":
|
|
1108
|
+
this.applyDithering(encoder, inputView, outputView, w, h);
|
|
1109
|
+
break;
|
|
1110
|
+
case "watercolor":
|
|
1111
|
+
this.applyWatercolor(encoder, inputView, outputView, w, h);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
1116
|
+
// Scratch buffer for number[] uniform data — writeBuffer() captures the
|
|
1117
|
+
// contents synchronously at call time, so a single reusable scratch is safe
|
|
1118
|
+
// and avoids allocating a Float32Array per effect per frame.
|
|
1119
|
+
wuScratch = new Float32Array(64);
|
|
1120
|
+
wu(key, data) {
|
|
1121
|
+
let arr;
|
|
1122
|
+
if (data instanceof Float32Array) {
|
|
1123
|
+
arr = data;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
if (this.wuScratch.length < data.length)
|
|
1127
|
+
this.wuScratch = new Float32Array(data.length);
|
|
1128
|
+
this.wuScratch.set(data);
|
|
1129
|
+
arr = this.wuScratch.subarray(0, data.length);
|
|
1130
|
+
}
|
|
1131
|
+
this.device.queue.writeBuffer(this.uniforms[key], 0, arr);
|
|
1132
|
+
}
|
|
1133
|
+
bg(layout, entries) {
|
|
1134
|
+
return this.device.createBindGroup({ layout, entries });
|
|
1135
|
+
}
|
|
1136
|
+
// ── Per-frame allocation caches ───────────────────────────────────────────
|
|
1137
|
+
// createView()/createBindGroup() were called per effect per frame (~20+/frame).
|
|
1138
|
+
// Views are immutable descriptors → cache per texture. Bind groups are pure
|
|
1139
|
+
// functions of (layout, resources) → cache keyed by resource identity.
|
|
1140
|
+
// Both caches are invalidated when targets are destroyed (resize/dispose).
|
|
1141
|
+
viewCache = new Map();
|
|
1142
|
+
bgCache = new Map();
|
|
1143
|
+
objIds = new WeakMap();
|
|
1144
|
+
nextObjId = 1;
|
|
1145
|
+
idOf(o) {
|
|
1146
|
+
let id = this.objIds.get(o);
|
|
1147
|
+
if (id === undefined)
|
|
1148
|
+
this.objIds.set(o, (id = this.nextObjId++));
|
|
1149
|
+
return id;
|
|
1150
|
+
}
|
|
1151
|
+
/** Cached createView() for a stable texture (cleared on target rebuild). */
|
|
1152
|
+
viewOf(tex) {
|
|
1153
|
+
let v = this.viewCache.get(tex);
|
|
1154
|
+
if (!v)
|
|
1155
|
+
this.viewCache.set(tex, (v = tex.createView()));
|
|
1156
|
+
return v;
|
|
1157
|
+
}
|
|
1158
|
+
/** Cached createBindGroup — entries must reference stable resources
|
|
1159
|
+
* (views from viewOf(), long-lived buffers/samplers). */
|
|
1160
|
+
bgCached(layout, entries) {
|
|
1161
|
+
let key = `${this.idOf(layout)}`;
|
|
1162
|
+
entries.forEach((e) => {
|
|
1163
|
+
const r = e.resource;
|
|
1164
|
+
key += `|${e.binding}:`;
|
|
1165
|
+
// GPUBufferBinding literals are fresh objects per call — key on the
|
|
1166
|
+
// underlying buffer + offset/size instead.
|
|
1167
|
+
if (r && typeof r === "object" && "buffer" in r) {
|
|
1168
|
+
const b = r;
|
|
1169
|
+
key += `b${this.idOf(b.buffer)}@${b.offset ?? 0}+${b.size ?? -1}`;
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
key += `o${this.idOf(r)}`;
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
let bg = this.bgCache.get(key);
|
|
1176
|
+
if (!bg)
|
|
1177
|
+
this.bgCache.set(key, (bg = this.bg(layout, entries)));
|
|
1178
|
+
return bg;
|
|
1179
|
+
}
|
|
1180
|
+
clearResourceCaches() {
|
|
1181
|
+
this.viewCache.clear();
|
|
1182
|
+
this.bgCache.clear();
|
|
1183
|
+
}
|
|
1184
|
+
pass(encoder, pipeline, bindGroup, outputView, w, h) {
|
|
1185
|
+
const p = encoder.beginRenderPass({
|
|
1186
|
+
colorAttachments: [{ view: outputView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" }],
|
|
1187
|
+
});
|
|
1188
|
+
p.setViewport(0, 0, w, h, 0, 1);
|
|
1189
|
+
p.setScissorRect(0, 0, w, h);
|
|
1190
|
+
p.setPipeline(pipeline);
|
|
1191
|
+
p.setBindGroup(0, bindGroup);
|
|
1192
|
+
p.setBindGroup(1, this.occluderBindGroup);
|
|
1193
|
+
p.draw(3);
|
|
1194
|
+
p.end();
|
|
1195
|
+
}
|
|
1196
|
+
// ── Simple cc-layout effects (color + dummy + sampler + uniform) ──────────
|
|
1197
|
+
applyCC(encoder, key, inputView, outputView, w, h, data) {
|
|
1198
|
+
this.wu(key, data);
|
|
1199
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1200
|
+
{ binding: 0, resource: inputView },
|
|
1201
|
+
{ binding: 1, resource: this.viewOf(this.dummyTex) },
|
|
1202
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1203
|
+
{ binding: 3, resource: { buffer: this.uniforms[key] } },
|
|
1204
|
+
]);
|
|
1205
|
+
this.pass(encoder, this.pipelines[key], bg, outputView, w, h);
|
|
1206
|
+
}
|
|
1207
|
+
// ── Simple cd-layout effects (color + depth + sampler + uniform) ──────────
|
|
1208
|
+
applyCD(encoder, key, inputView, outputView, depthView, w, h, data) {
|
|
1209
|
+
this.wu(key, data);
|
|
1210
|
+
const bg = this.bgCached(this.cdLayout, [
|
|
1211
|
+
{ binding: 0, resource: inputView },
|
|
1212
|
+
{ binding: 1, resource: depthView },
|
|
1213
|
+
{ binding: 2, resource: this.nearestSampler },
|
|
1214
|
+
{ binding: 3, resource: { buffer: this.uniforms[key] } },
|
|
1215
|
+
]);
|
|
1216
|
+
this.pass(encoder, this.pipelines[key], bg, outputView, w, h);
|
|
1217
|
+
}
|
|
1218
|
+
// ── Afterimage ────────────────────────────────────────────────────────────
|
|
1219
|
+
applyAfterimage(encoder, inputView, outputView, w, h) {
|
|
1220
|
+
this.wu("afterimage", [1 / w, 1 / h, this.afterimageDamp]);
|
|
1221
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1222
|
+
{ binding: 0, resource: inputView },
|
|
1223
|
+
{ binding: 1, resource: this.viewOf(this.afterimageTex) },
|
|
1224
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1225
|
+
{ binding: 3, resource: { buffer: this.uniforms["afterimage"] } },
|
|
1226
|
+
]);
|
|
1227
|
+
this.pass(encoder, this.pipelines["afterimage"], bg, outputView, w, h);
|
|
1228
|
+
}
|
|
1229
|
+
// ── ASCII ─────────────────────────────────────────────────────────────────
|
|
1230
|
+
applyASCII(encoder, inputView, outputView, w, h) {
|
|
1231
|
+
this.wu("ascii", [1 / w, 1 / h, this.asciiCellSize, this.asciiUseColor, w, h]);
|
|
1232
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1233
|
+
{ binding: 0, resource: inputView },
|
|
1234
|
+
{ binding: 1, resource: this.viewOf(this.glyphTex) },
|
|
1235
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1236
|
+
{ binding: 3, resource: { buffer: this.uniforms["ascii"] } },
|
|
1237
|
+
]);
|
|
1238
|
+
this.pass(encoder, this.pipelines["ascii"], bg, outputView, w, h);
|
|
1239
|
+
}
|
|
1240
|
+
// ── Bloom (multi-MIP pyramid: downsample cascade + upsample composite) ───
|
|
1241
|
+
applyBloom(encoder, inputView, outputView, w, h) {
|
|
1242
|
+
const dummy = this.viewOf(this.dummyTex);
|
|
1243
|
+
const mipCount = this.bloomMip.length;
|
|
1244
|
+
if (mipCount === 0) {
|
|
1245
|
+
// Fallback: no MIP targets allocated — blit input to output
|
|
1246
|
+
this.applyBlitHDR(encoder, inputView, outputView, w, h);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
// Track dimensions for each MIP level
|
|
1250
|
+
const mipW = [Math.max(1, w >> 1)];
|
|
1251
|
+
const mipH = [Math.max(1, h >> 1)];
|
|
1252
|
+
for (let i = 1; i < mipCount; i++) {
|
|
1253
|
+
mipW.push(Math.max(1, mipW[i - 1] >> 1));
|
|
1254
|
+
mipH.push(Math.max(1, mipH[i - 1] >> 1));
|
|
1255
|
+
}
|
|
1256
|
+
// ── Downsample cascade: full → ½ → ¼ → ⅛ → ... ──
|
|
1257
|
+
// First downsample: bright-pass extraction from the full-res input
|
|
1258
|
+
this.wu("bloom-downsample", [
|
|
1259
|
+
1 / w, 1 / h, 1.0, this.bloomThreshold, this.bloomSoftKnee, 0, 0, 0,
|
|
1260
|
+
]);
|
|
1261
|
+
this.pass(encoder, this.pipelines["bloom-downsample"], this.bgCached(this.ccLayout, [
|
|
1262
|
+
{ binding: 0, resource: inputView }, { binding: 1, resource: dummy },
|
|
1263
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["bloom-downsample"] } },
|
|
1264
|
+
]), this.viewOf(this.bloomMip[0]), mipW[0], mipH[0]);
|
|
1265
|
+
// Subsequent downsamples: plain downsample from previous MIP
|
|
1266
|
+
for (let i = 1; i < mipCount; i++) {
|
|
1267
|
+
const srcW = mipW[i - 1], srcH = mipH[i - 1];
|
|
1268
|
+
this.wu("bloom-downsample", [
|
|
1269
|
+
1 / srcW, 1 / srcH, 0.0, this.bloomThreshold, this.bloomSoftKnee, 0, 0, 0,
|
|
1270
|
+
]);
|
|
1271
|
+
this.pass(encoder, this.pipelines["bloom-downsample"], this.bgCached(this.ccLayout, [
|
|
1272
|
+
{ binding: 0, resource: this.viewOf(this.bloomMip[i - 1]) }, { binding: 1, resource: dummy },
|
|
1273
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["bloom-downsample"] } },
|
|
1274
|
+
]), this.viewOf(this.bloomMip[i]), mipW[i], mipH[i]);
|
|
1275
|
+
}
|
|
1276
|
+
// ── Upsample cascade: smallest MIP → ... → ½ → composite to output ──
|
|
1277
|
+
// Start from the smallest MIP, progressively upsample + additively blend
|
|
1278
|
+
// into the next larger MIP. The upsample shader reads the lower-res MIP
|
|
1279
|
+
// and adds it to the higher-res MIP (baseTex binding).
|
|
1280
|
+
// We use a temp texture for the base to avoid reading+writing the same
|
|
1281
|
+
// texture in the same render pass (WebGPU sync scope violation).
|
|
1282
|
+
for (let i = mipCount - 1; i >= 1; i--) {
|
|
1283
|
+
const srcW = mipW[i], srcH = mipH[i];
|
|
1284
|
+
const dstW = mipW[i - 1], dstH = mipH[i - 1];
|
|
1285
|
+
const weight = this.bloomMipWeights[Math.min(i, this.bloomMipWeights.length - 1)] ?? 0.5;
|
|
1286
|
+
this.wu("bloom-upsample", [
|
|
1287
|
+
1 / srcW, 1 / srcH, weight, 0,
|
|
1288
|
+
this.bloomTint[0], this.bloomTint[1], this.bloomTint[2], 0,
|
|
1289
|
+
]);
|
|
1290
|
+
// Blit bloomMip[i-1] → bloomTemp[i-1] to avoid read+write same texture
|
|
1291
|
+
this.applyBlitHDR(encoder, this.viewOf(this.bloomMip[i - 1]), this.viewOf(this.bloomTemp[i - 1]), dstW, dstH);
|
|
1292
|
+
this.pass(encoder, this.pipelines["bloom-upsample"], this.bgCached(this.ccLayout, [
|
|
1293
|
+
{ binding: 0, resource: this.viewOf(this.bloomMip[i]) },
|
|
1294
|
+
{ binding: 1, resource: this.viewOf(this.bloomTemp[i - 1]) },
|
|
1295
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1296
|
+
{ binding: 3, resource: { buffer: this.uniforms["bloom-upsample"] } },
|
|
1297
|
+
]), this.viewOf(this.bloomMip[i - 1]), dstW, dstH);
|
|
1298
|
+
}
|
|
1299
|
+
// Final composite: upsample ½-res bloom → full-res, add to scene color
|
|
1300
|
+
const srcW = mipW[0], srcH = mipH[0];
|
|
1301
|
+
const finalWeight = this.bloomStrength * (this.bloomMipWeights[0] ?? 0.5);
|
|
1302
|
+
this.wu("bloom-upsample", [
|
|
1303
|
+
1 / srcW, 1 / srcH, finalWeight, 0,
|
|
1304
|
+
this.bloomTint[0], this.bloomTint[1], this.bloomTint[2], 0,
|
|
1305
|
+
]);
|
|
1306
|
+
// For the final pass, baseTex = inputView (scene color), output = outputView
|
|
1307
|
+
// We need to read inputView as base — but the upsample shader writes base+bloom.
|
|
1308
|
+
// Copy inputView to a dedicated scratch (bloomBase): outputView may be any
|
|
1309
|
+
// ping-pong texture, so neither pingPong[0] nor pingPong[1] is guaranteed
|
|
1310
|
+
// safe here (read+write same texture = WebGPU sync scope violation).
|
|
1311
|
+
this.applyBlitHDR(encoder, inputView, this.viewOf(this.bloomBase), w, h);
|
|
1312
|
+
this.pass(encoder, this.pipelines["bloom-upsample"], this.bgCached(this.ccLayout, [
|
|
1313
|
+
{ binding: 0, resource: this.viewOf(this.bloomMip[0]) },
|
|
1314
|
+
{ binding: 1, resource: this.viewOf(this.bloomBase) },
|
|
1315
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1316
|
+
{ binding: 3, resource: { buffer: this.uniforms["bloom-upsample"] } },
|
|
1317
|
+
]), outputView, w, h);
|
|
1318
|
+
}
|
|
1319
|
+
// ── Bloom-soft (bright + blur H + blur V, stores result for tonemap) ──────
|
|
1320
|
+
applyBloomSoft(encoder, inputView, outputView, w, h) {
|
|
1321
|
+
const hw = Math.max(1, w >> 1), hh = Math.max(1, h >> 1);
|
|
1322
|
+
const dummy = this.viewOf(this.dummyTex);
|
|
1323
|
+
// Bright pass (dirX > 1.0 triggers bright-pass extraction)
|
|
1324
|
+
this.wu("bloom-soft", [1 / w, 1 / h, 2.0, 0.0, this.bloomSoftThreshold, this.bloomSoftSoftThreshold]);
|
|
1325
|
+
this.pass(encoder, this.pipelines["bloom-soft"], this.bgCached(this.ccLayout, [
|
|
1326
|
+
{ binding: 0, resource: inputView }, { binding: 1, resource: dummy },
|
|
1327
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["bloom-soft"] } },
|
|
1328
|
+
]), this.viewOf(this.halfResA), hw, hh);
|
|
1329
|
+
// Blur H
|
|
1330
|
+
this.wu("bloom-soft", [1 / hw, 1 / hh, 1.0, 0.0, this.bloomSoftThreshold, this.bloomSoftSoftThreshold]);
|
|
1331
|
+
this.pass(encoder, this.pipelines["bloom-soft"], this.bgCached(this.ccLayout, [
|
|
1332
|
+
{ binding: 0, resource: this.viewOf(this.halfResA) }, { binding: 1, resource: dummy },
|
|
1333
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["bloom-soft"] } },
|
|
1334
|
+
]), this.viewOf(this.halfResB), hw, hh);
|
|
1335
|
+
// Blur V
|
|
1336
|
+
this.wu("bloom-soft", [1 / hw, 1 / hh, 0.0, 1.0, this.bloomSoftThreshold, this.bloomSoftSoftThreshold]);
|
|
1337
|
+
this.pass(encoder, this.pipelines["bloom-soft"], this.bgCached(this.ccLayout, [
|
|
1338
|
+
{ binding: 0, resource: this.viewOf(this.halfResB) }, { binding: 1, resource: dummy },
|
|
1339
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["bloom-soft"] } },
|
|
1340
|
+
]), this.viewOf(this.halfResA), hw, hh);
|
|
1341
|
+
// Store result for tonemap
|
|
1342
|
+
this.bloomSoftResult = this.viewOf(this.halfResA);
|
|
1343
|
+
// Blit input to output (bloom-soft doesn't composite — tonemap does that)
|
|
1344
|
+
// Use HDR blit since outputView is an HDR ping-pong texture in the chain
|
|
1345
|
+
this.applyBlitHDR(encoder, inputView, outputView, w, h);
|
|
1346
|
+
}
|
|
1347
|
+
// ── Tonemap (color + bloom + ACES + exposure/gamma/contrast/sat/vignette) ─
|
|
1348
|
+
applyTonemap(encoder, inputView, outputView, w, h) {
|
|
1349
|
+
this.wu("tonemap", [
|
|
1350
|
+
this.exposure, this.bloomSoftIntensity, this.gamma, this.contrast, this.saturation, this.vignette, 0, 0,
|
|
1351
|
+
]);
|
|
1352
|
+
const bloomView = this.bloomSoftResult ?? this.viewOf(this.dummyTex);
|
|
1353
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1354
|
+
{ binding: 0, resource: inputView },
|
|
1355
|
+
{ binding: 1, resource: bloomView },
|
|
1356
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1357
|
+
{ binding: 3, resource: { buffer: this.uniforms["tonemap"] } },
|
|
1358
|
+
]);
|
|
1359
|
+
this.pass(encoder, this.pipelines["tonemap"], bg, outputView, w, h);
|
|
1360
|
+
}
|
|
1361
|
+
// ── TAA (color + velocity + history + YCoCg neighborhood clamp) ───────────
|
|
1362
|
+
applyTAA(encoder, inputView, outputView, w, h) {
|
|
1363
|
+
// When occluder rects changed, use blendFactor=1.0 for one frame to
|
|
1364
|
+
// ignore stale history in the previously-occluded region (standard TAA
|
|
1365
|
+
// reset pattern, same as camera cuts).
|
|
1366
|
+
const blend = this.taaHistoryReset ? 1.0 : this.taaBlendFactor;
|
|
1367
|
+
this.wu("taa", [1 / w, 1 / h, blend, this.taaVarianceClamp ? 1 : 0, this.taaJitterX, this.taaJitterY, 0, 0]);
|
|
1368
|
+
const bg = this.bgCached(this.cvvhLayout, [
|
|
1369
|
+
{ binding: 0, resource: inputView },
|
|
1370
|
+
{ binding: 1, resource: this.viewOf(this.sceneVelocity) },
|
|
1371
|
+
{ binding: 2, resource: this.viewOf(this.taaHistory) },
|
|
1372
|
+
{ binding: 3, resource: this.linearSampler },
|
|
1373
|
+
{ binding: 4, resource: { buffer: this.uniforms["taa"] } },
|
|
1374
|
+
]);
|
|
1375
|
+
this.pass(encoder, this.pipelines["taa"], bg, outputView, w, h);
|
|
1376
|
+
this.taaHistoryReset = false;
|
|
1377
|
+
}
|
|
1378
|
+
// ── SSAO (compute + blur + composite) ─────────────────────────────────────
|
|
1379
|
+
applySSAO(encoder, inputView, outputView, depthView, w, h) {
|
|
1380
|
+
// Write SSAO uniform (matrices + params)
|
|
1381
|
+
this.writeSSAOUniform(w, h);
|
|
1382
|
+
// Compute SSAO
|
|
1383
|
+
const bg0 = this.bgCached(this.dnfnLayout, [
|
|
1384
|
+
{ binding: 0, resource: depthView },
|
|
1385
|
+
{ binding: 1, resource: this.viewOf(this.sceneNormals) },
|
|
1386
|
+
{ binding: 2, resource: this.viewOf(this.noiseTex) },
|
|
1387
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
1388
|
+
{ binding: 4, resource: { buffer: this.uniforms["ssao"] } },
|
|
1389
|
+
]);
|
|
1390
|
+
this.pass(encoder, this.pipelines["ssao"], bg0, this.viewOf(this.ssaoA), w, h);
|
|
1391
|
+
// Blur SSAO
|
|
1392
|
+
this.wu("ssao-blur", [1 / w, 1 / h]);
|
|
1393
|
+
const bg1 = this.bgCached(this.cdLayout, [
|
|
1394
|
+
{ binding: 0, resource: this.viewOf(this.ssaoA) },
|
|
1395
|
+
{ binding: 1, resource: depthView },
|
|
1396
|
+
{ binding: 2, resource: this.nearestSampler },
|
|
1397
|
+
{ binding: 3, resource: { buffer: this.uniforms["ssao-blur"] } },
|
|
1398
|
+
]);
|
|
1399
|
+
this.pass(encoder, this.pipelines["ssao-blur"], bg1, this.viewOf(this.ssaoB), w, h);
|
|
1400
|
+
// Composite AO × color
|
|
1401
|
+
this.wu("ssao-composite", [1 / w, 1 / h]);
|
|
1402
|
+
const bg2 = this.bgCached(this.ccLayout, [
|
|
1403
|
+
{ binding: 0, resource: inputView },
|
|
1404
|
+
{ binding: 1, resource: this.viewOf(this.ssaoB) },
|
|
1405
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1406
|
+
{ binding: 3, resource: { buffer: this.uniforms["ssao-composite"] } },
|
|
1407
|
+
]);
|
|
1408
|
+
this.pass(encoder, this.pipelines["ssao-composite"], bg2, outputView, w, h);
|
|
1409
|
+
}
|
|
1410
|
+
writeSSAOUniform(w, h) {
|
|
1411
|
+
// Layout: invProjection(16) + view(16) + projection(16) + directions + radius + bias
|
|
1412
|
+
// + noiseScale(2) + screenSize(2) + power + thickness + slices + pad(2) = 56 floats
|
|
1413
|
+
const data = new Float32Array(64);
|
|
1414
|
+
if (this.camInvProj)
|
|
1415
|
+
data.set(this.camInvProj, 0);
|
|
1416
|
+
if (this.camView)
|
|
1417
|
+
data.set(this.camView, 16);
|
|
1418
|
+
if (this.camProj)
|
|
1419
|
+
data.set(this.camProj, 32);
|
|
1420
|
+
data[48] = this.ssaoDirections;
|
|
1421
|
+
data[49] = this.ssaoRadius;
|
|
1422
|
+
data[50] = this.ssaoBias;
|
|
1423
|
+
data[51] = 4; // noiseSize X
|
|
1424
|
+
data[52] = 4; // noiseSize Y
|
|
1425
|
+
data[53] = w;
|
|
1426
|
+
data[54] = h;
|
|
1427
|
+
data[55] = this.ssaoPower;
|
|
1428
|
+
data[56] = this.ssaoThickness;
|
|
1429
|
+
data[57] = this.ssaoSlices;
|
|
1430
|
+
this.device.queue.writeBuffer(this.uniforms["ssao"], 0, data);
|
|
1431
|
+
}
|
|
1432
|
+
// ── SSR (color + normal + depth + ray march) ──────────────────────────────
|
|
1433
|
+
applySSR(encoder, inputView, outputView, depthView, w, h) {
|
|
1434
|
+
const data = new Float32Array(64);
|
|
1435
|
+
if (this.camInvProj)
|
|
1436
|
+
data.set(this.camInvProj, 0);
|
|
1437
|
+
if (this.camView)
|
|
1438
|
+
data.set(this.camView, 16);
|
|
1439
|
+
if (this.camProj)
|
|
1440
|
+
data.set(this.camProj, 32);
|
|
1441
|
+
data[48] = this.ssrMaxSteps;
|
|
1442
|
+
data[49] = this.ssrThickness;
|
|
1443
|
+
data[50] = this.ssrMaxDistance;
|
|
1444
|
+
data[51] = this.ssrFadeStart;
|
|
1445
|
+
data[52] = this.ssrFadeEnd;
|
|
1446
|
+
data[53] = this.ssrBinarySteps;
|
|
1447
|
+
data[54] = this.ssrStride;
|
|
1448
|
+
this.device.queue.writeBuffer(this.uniforms["ssr"], 0, data);
|
|
1449
|
+
const bg = this.bgCached(this.cvdLayout, [
|
|
1450
|
+
{ binding: 0, resource: inputView },
|
|
1451
|
+
{ binding: 1, resource: this.viewOf(this.sceneNormals) },
|
|
1452
|
+
{ binding: 2, resource: depthView },
|
|
1453
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
1454
|
+
{ binding: 4, resource: { buffer: this.uniforms["ssr"] } },
|
|
1455
|
+
]);
|
|
1456
|
+
this.pass(encoder, this.pipelines["ssr"], bg, outputView, w, h);
|
|
1457
|
+
}
|
|
1458
|
+
// ── Motion blur (color + velocity + depth) ────────────────────────────────
|
|
1459
|
+
applyMotionBlur(encoder, inputView, outputView, depthView, w, h) {
|
|
1460
|
+
this.wu("motion-blur", [1 / w, 1 / h, this.motionBlurIntensity, this.motionBlurMaxSamples]);
|
|
1461
|
+
const bg = this.bgCached(this.cvdLayout, [
|
|
1462
|
+
{ binding: 0, resource: inputView },
|
|
1463
|
+
{ binding: 1, resource: this.viewOf(this.sceneVelocity) },
|
|
1464
|
+
{ binding: 2, resource: depthView },
|
|
1465
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
1466
|
+
{ binding: 4, resource: { buffer: this.uniforms["motion-blur"] } },
|
|
1467
|
+
]);
|
|
1468
|
+
this.pass(encoder, this.pipelines["motion-blur"], bg, outputView, w, h);
|
|
1469
|
+
}
|
|
1470
|
+
// ── Edges (color + normal + depth) ────────────────────────────────────────
|
|
1471
|
+
applyEdges(encoder, inputView, outputView, depthView, w, h) {
|
|
1472
|
+
this.wu("edges", [1 / w, 1 / h, this.edgesThreshold, this.edgesOpacity, 0, 0, 0, 0, ...this.edgesEdgeColor, 0, 0]);
|
|
1473
|
+
const bg = this.bgCached(this.cvdLayout, [
|
|
1474
|
+
{ binding: 0, resource: inputView },
|
|
1475
|
+
{ binding: 1, resource: this.viewOf(this.sceneNormals) },
|
|
1476
|
+
{ binding: 2, resource: depthView },
|
|
1477
|
+
{ binding: 3, resource: this.nearestSampler },
|
|
1478
|
+
{ binding: 4, resource: { buffer: this.uniforms["edges"] } },
|
|
1479
|
+
]);
|
|
1480
|
+
this.pass(encoder, this.pipelines["edges"], bg, outputView, w, h);
|
|
1481
|
+
}
|
|
1482
|
+
// ── Gaussian blur (two-pass: H then V) ────────────────────────────────────
|
|
1483
|
+
applyGaussianBlur(encoder, inputView, outputView, w, h) {
|
|
1484
|
+
const dummy = this.viewOf(this.dummyTex);
|
|
1485
|
+
// H pass
|
|
1486
|
+
this.wu("gaussian-blur", [1 / w, 1 / h, 1.0, 0.0, this.gaussianBlurRadius]);
|
|
1487
|
+
this.pass(encoder, this.pipelines["gaussian-blur"], this.bgCached(this.ccLayout, [
|
|
1488
|
+
{ binding: 0, resource: inputView }, { binding: 1, resource: dummy },
|
|
1489
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["gaussian-blur"] } },
|
|
1490
|
+
]), this.viewOf(this.blurA), w, h);
|
|
1491
|
+
// V pass
|
|
1492
|
+
this.wu("gaussian-blur", [1 / w, 1 / h, 0.0, 1.0, this.gaussianBlurRadius]);
|
|
1493
|
+
this.pass(encoder, this.pipelines["gaussian-blur"], this.bgCached(this.ccLayout, [
|
|
1494
|
+
{ binding: 0, resource: this.viewOf(this.blurA) }, { binding: 1, resource: dummy },
|
|
1495
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["gaussian-blur"] } },
|
|
1496
|
+
]), outputView, w, h);
|
|
1497
|
+
}
|
|
1498
|
+
// ── Outline (color + mask) ────────────────────────────────────────────────
|
|
1499
|
+
applyOutline(encoder, inputView, outputView, w, h) {
|
|
1500
|
+
this.wu("outline", [1 / w, 1 / h, this.outlineWidth, this.outlineOpacity, 0, 0, 0, 0, ...this.outlineColor, 0, 0]);
|
|
1501
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1502
|
+
{ binding: 0, resource: inputView },
|
|
1503
|
+
{ binding: 1, resource: this.viewOf(this.sceneMask) },
|
|
1504
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1505
|
+
{ binding: 3, resource: { buffer: this.uniforms["outline"] } },
|
|
1506
|
+
]);
|
|
1507
|
+
this.pass(encoder, this.pipelines["outline"], bg, outputView, w, h);
|
|
1508
|
+
}
|
|
1509
|
+
// ── Highlight (blur H + blur V + composite) ───────────────────────────────
|
|
1510
|
+
applyHighlight(encoder, inputView, outputView, w, h) {
|
|
1511
|
+
const dummy = this.viewOf(this.dummyTex);
|
|
1512
|
+
const maskView = this.viewOf(this.sceneMask);
|
|
1513
|
+
// Blur H
|
|
1514
|
+
this.wu("highlight-blur", [1 / w, 1 / h, 1.0, 0.0, this.highlightBlurRadius]);
|
|
1515
|
+
this.pass(encoder, this.pipelines["highlight-blur"], this.bgCached(this.ccLayout, [
|
|
1516
|
+
{ binding: 0, resource: maskView }, { binding: 1, resource: dummy },
|
|
1517
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["highlight-blur"] } },
|
|
1518
|
+
]), this.viewOf(this.blurA), w, h);
|
|
1519
|
+
// Blur V
|
|
1520
|
+
this.wu("highlight-blur", [1 / w, 1 / h, 0.0, 1.0, this.highlightBlurRadius]);
|
|
1521
|
+
this.pass(encoder, this.pipelines["highlight-blur"], this.bgCached(this.ccLayout, [
|
|
1522
|
+
{ binding: 0, resource: this.viewOf(this.blurA) }, { binding: 1, resource: dummy },
|
|
1523
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["highlight-blur"] } },
|
|
1524
|
+
]), this.viewOf(this.blurB), w, h);
|
|
1525
|
+
// Composite
|
|
1526
|
+
this.wu("highlight-composite", [this.highlightIntensity, this.highlightInnerOpacity]);
|
|
1527
|
+
const bg = this.bgCached(this.cvvhLayout, [
|
|
1528
|
+
{ binding: 0, resource: inputView },
|
|
1529
|
+
{ binding: 1, resource: this.viewOf(this.blurB) },
|
|
1530
|
+
{ binding: 2, resource: maskView },
|
|
1531
|
+
{ binding: 3, resource: this.linearSampler },
|
|
1532
|
+
{ binding: 4, resource: { buffer: this.uniforms["highlight-composite"] } },
|
|
1533
|
+
]);
|
|
1534
|
+
this.pass(encoder, this.pipelines["highlight-composite"], bg, outputView, w, h);
|
|
1535
|
+
}
|
|
1536
|
+
// ── Glow (blur H + blur V + composite) ────────────────────────────────────
|
|
1537
|
+
applyGlow(encoder, inputView, outputView, w, h) {
|
|
1538
|
+
const dummy = this.viewOf(this.dummyTex);
|
|
1539
|
+
const maskView = this.viewOf(this.sceneMask);
|
|
1540
|
+
// Blur H
|
|
1541
|
+
this.wu("glow-blur", [1 / w, 1 / h, 1.0, 0.0, this.glowBlurRadius]);
|
|
1542
|
+
this.pass(encoder, this.pipelines["glow-blur"], this.bgCached(this.ccLayout, [
|
|
1543
|
+
{ binding: 0, resource: maskView }, { binding: 1, resource: dummy },
|
|
1544
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["glow-blur"] } },
|
|
1545
|
+
]), this.viewOf(this.blurA), w, h);
|
|
1546
|
+
// Blur V
|
|
1547
|
+
this.wu("glow-blur", [1 / w, 1 / h, 0.0, 1.0, this.glowBlurRadius]);
|
|
1548
|
+
this.pass(encoder, this.pipelines["glow-blur"], this.bgCached(this.ccLayout, [
|
|
1549
|
+
{ binding: 0, resource: this.viewOf(this.blurA) }, { binding: 1, resource: dummy },
|
|
1550
|
+
{ binding: 2, resource: this.linearSampler }, { binding: 3, resource: { buffer: this.uniforms["glow-blur"] } },
|
|
1551
|
+
]), this.viewOf(this.blurB), w, h);
|
|
1552
|
+
// Composite
|
|
1553
|
+
this.wu("glow-composite", [this.glowIntensity]);
|
|
1554
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1555
|
+
{ binding: 0, resource: inputView },
|
|
1556
|
+
{ binding: 1, resource: this.viewOf(this.blurB) },
|
|
1557
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1558
|
+
{ binding: 3, resource: { buffer: this.uniforms["glow-composite"] } },
|
|
1559
|
+
]);
|
|
1560
|
+
this.pass(encoder, this.pipelines["glow-composite"], bg, outputView, w, h);
|
|
1561
|
+
}
|
|
1562
|
+
// ── LUT (3D color grading — dedicated lutLayout with texture_3d) ──────────
|
|
1563
|
+
applyLUT(encoder, inputView, outputView, w, h) {
|
|
1564
|
+
if (!this.lutView) {
|
|
1565
|
+
// No LUT loaded — blit input to output (HDR ping-pong in chain)
|
|
1566
|
+
this.applyBlitHDR(encoder, inputView, outputView, w, h);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
this.wu("lut", [this.lutEnabled ? 1 : 0, this.lutSize, 0, 0]);
|
|
1570
|
+
const bg = this.bgCached(this.lutLayout, [
|
|
1571
|
+
{ binding: 0, resource: inputView },
|
|
1572
|
+
{ binding: 1, resource: this.lutView },
|
|
1573
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1574
|
+
{ binding: 3, resource: { buffer: this.uniforms["lut"] } },
|
|
1575
|
+
]);
|
|
1576
|
+
this.pass(encoder, this.pipelines["lut"], bg, outputView, w, h);
|
|
1577
|
+
}
|
|
1578
|
+
// ── Dithering (color + noise + sampler + uniform, ccLayout) ───────────────
|
|
1579
|
+
applyDithering(encoder, inputView, outputView, w, h) {
|
|
1580
|
+
this.wu("dithering", [1 / w, 1 / h, this.dMode, this.dStrength, this.dLevels, 0, 0, 0]);
|
|
1581
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1582
|
+
{ binding: 0, resource: inputView },
|
|
1583
|
+
{ binding: 1, resource: this.viewOf(this.noiseTex) },
|
|
1584
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1585
|
+
{ binding: 3, resource: { buffer: this.uniforms["dithering"] } },
|
|
1586
|
+
]);
|
|
1587
|
+
this.pass(encoder, this.pipelines["dithering"], bg, outputView, w, h);
|
|
1588
|
+
}
|
|
1589
|
+
// ── Watercolor (color + noise + sampler + uniform, ccLayout) ──────────────
|
|
1590
|
+
applyWatercolor(encoder, inputView, outputView, w, h) {
|
|
1591
|
+
this.wu("watercolor", [1 / w, 1 / h, this.wcEdgeStrength, this.wcPaperScale, this.wcBlend, 0, 0, 0]);
|
|
1592
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1593
|
+
{ binding: 0, resource: inputView },
|
|
1594
|
+
{ binding: 1, resource: this.viewOf(this.noiseTex) },
|
|
1595
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1596
|
+
{ binding: 3, resource: { buffer: this.uniforms["watercolor"] } },
|
|
1597
|
+
]);
|
|
1598
|
+
this.pass(encoder, this.pipelines["watercolor"], bg, outputView, w, h);
|
|
1599
|
+
}
|
|
1600
|
+
// ── Blit (copy rgba16float → canvas format) ───────────────────────────────
|
|
1601
|
+
applyBlit(encoder, inputView, outputView, w, h) {
|
|
1602
|
+
this.wu("blit", [1 / w, 1 / h]);
|
|
1603
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1604
|
+
{ binding: 0, resource: inputView },
|
|
1605
|
+
{ binding: 1, resource: this.viewOf(this.dummyTex) },
|
|
1606
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1607
|
+
{ binding: 3, resource: { buffer: this.uniforms["blit"] } },
|
|
1608
|
+
]);
|
|
1609
|
+
this.pass(encoder, this.pipelines["blit"], bg, outputView, w, h);
|
|
1610
|
+
}
|
|
1611
|
+
/** HDR blit variant for intermediate chain passes (writes to rgba16float ping-pong). */
|
|
1612
|
+
applyBlitHDR(encoder, inputView, outputView, w, h) {
|
|
1613
|
+
this.wu("blit", [1 / w, 1 / h]);
|
|
1614
|
+
const bg = this.bgCached(this.ccLayout, [
|
|
1615
|
+
{ binding: 0, resource: inputView },
|
|
1616
|
+
{ binding: 1, resource: this.viewOf(this.dummyTex) },
|
|
1617
|
+
{ binding: 2, resource: this.linearSampler },
|
|
1618
|
+
{ binding: 3, resource: { buffer: this.uniforms["blit"] } },
|
|
1619
|
+
]);
|
|
1620
|
+
this.pass(encoder, this.pipelines["blit-hdr"], bg, outputView, w, h);
|
|
1621
|
+
}
|
|
1622
|
+
// ── Destroy ───────────────────────────────────────────────────────────────
|
|
1623
|
+
destroy() {
|
|
1624
|
+
this.destroyAllTargets();
|
|
1625
|
+
this.dummyTex?.destroy();
|
|
1626
|
+
this.glyphTex?.destroy();
|
|
1627
|
+
this.noiseTex?.destroy();
|
|
1628
|
+
this.lutTexture?.destroy();
|
|
1629
|
+
this.occluderUniform?.destroy();
|
|
1630
|
+
for (const key in this.uniforms)
|
|
1631
|
+
this.uniforms[key]?.destroy();
|
|
1632
|
+
for (const key in this.pipelines)
|
|
1633
|
+
this.pipelines[key]?.destroy?.();
|
|
1634
|
+
// Custom effects
|
|
1635
|
+
for (const [, e] of this.customEffects.entries()) {
|
|
1636
|
+
e.pipeline.destroy?.();
|
|
1637
|
+
e.uniform.destroy();
|
|
1638
|
+
}
|
|
1639
|
+
this.customEffects.clear();
|
|
1640
|
+
}
|
|
1641
|
+
}
|