@forgeax/engine-render 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +379 -33
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-3RTCJTH4.mjs} +12 -9
- package/dist/chunk-3RTCJTH4.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-LEAXOMYJ.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-LEAXOMYJ.mjs.map} +1 -1
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-NB2S7ZRX.mjs} +155 -11
- package/dist/chunk-NB2S7ZRX.mjs.map +1 -0
- package/dist/{chunk-LULCA3OF.mjs → chunk-NNQL7CB3.mjs} +4 -4
- package/dist/{chunk-LULCA3OF.mjs.map → chunk-NNQL7CB3.mjs.map} +1 -1
- package/dist/{chunk-4NNZQCL3.mjs → chunk-OO3DRYK7.mjs} +66 -200
- package/dist/chunk-OO3DRYK7.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-PLAXOOX5.mjs} +232 -7
- package/dist/chunk-PLAXOOX5.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-VOZWDM3L.mjs} +283 -193
- package/dist/chunk-VOZWDM3L.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-ZDQWGQW7.mjs} +10 -4
- package/dist/chunk-ZDQWGQW7.mjs.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +2865 -526
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/render.d.ts +6 -2
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +7 -0
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/error-reporting.d.ts +13 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +5 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +7 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +3 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +9 -0
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +6 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/inspection-types.d.ts +43 -2
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +8 -1
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/record/frame-snapshot.d.ts +3 -0
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +5 -2
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +12 -28
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +5 -3
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +1 -1
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +24 -4
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +3 -1
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +8 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +7 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +2 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +10 -7
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +0 -1
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +331 -68
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +1 -1
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +11 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +91 -0
- package/src/__tests__/instances-store.unit.test.ts +75 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/motion-blur-compute.dawn.test.ts +583 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +2 -0
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +24 -0
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +31 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +7 -1
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +18 -8
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/volume-optics-oracle.unit.test.ts +3 -1
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +177 -13
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +447 -61
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +19 -3
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/renderer-facade.ts +2 -0
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready.ts +181 -245
- package/src/assembly/webgpu-renderer.ts +16 -31
- package/src/bloom-admission.ts +21 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/camera.ts +66 -10
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/device/gpu-residency.ts +56 -12
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/render.ts +14 -1
- package/src/extract/camera.ts +20 -7
- package/src/extract/contracts.ts +7 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/error-reporting.ts +78 -0
- package/src/features/host.ts +25 -12
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +25 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +2 -0
- package/src/gpu-driven/material-artifacts.ts +2 -7
- package/src/gpu-driven/prepared-draw.ts +15 -10
- package/src/gpu-driven/production-raster.ts +83 -7
- package/src/gpu-driven/view-gpu.ts +17 -0
- package/src/gpu-scene.ts +188 -21
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +60 -2
- package/src/instances.ts +80 -1
- package/src/pipeline/standard-post.ts +307 -78
- package/src/pipeline-spec.ts +5 -5
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +2 -1
- package/src/record/frame-snapshot.ts +5 -1
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +163 -22
- package/src/record/main-pass-geometry.ts +30 -1
- package/src/record/main-pass-sprite-draws.ts +6 -1
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/render-context.ts +12 -28
- package/src/record/shadow-pass.ts +6 -1
- package/src/record/skybox-post-pass.ts +122 -196
- package/src/record/typed-frame-graph.ts +153 -9
- package/src/recovery/render-system-candidate.ts +22 -10
- package/src/render-contract.ts +23 -4
- package/src/render-pipeline.ts +6 -1
- package/src/render-system-extract-tail.ts +16 -0
- package/src/render-system-extract.ts +45 -13
- package/src/render-system.ts +255 -120
- package/src/scene/render-scene.ts +85 -4
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/view.ts +13 -2
- package/src/typed-render-graph-primitives.ts +121 -48
- package/src/volume/component.ts +0 -1
- package/src/volume/passes.ts +8 -4
- package/src/volume/resources.ts +1 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
|
@@ -7,7 +7,10 @@ import { addTypedFullscreenPass } from '../../typed-render-graph-primitives';
|
|
|
7
7
|
import type { RenderFeaturePlan, RenderFeaturePlanContext } from '../plan';
|
|
8
8
|
import type { RenderFeature, RenderFeatureExtractContext } from '../types';
|
|
9
9
|
import {
|
|
10
|
+
effectiveMotionBlurSampleCount,
|
|
11
|
+
MOTION_BLUR_PARAMS_BYTE_SIZE,
|
|
10
12
|
type MotionBlurParams,
|
|
13
|
+
motionBlurExposureScale,
|
|
11
14
|
motionBlurTemporalDemand,
|
|
12
15
|
validateMotionBlurParams,
|
|
13
16
|
} from './motion-blur-params';
|
|
@@ -32,26 +35,1516 @@ export function addMotionBlurPass(
|
|
|
32
35
|
export interface MotionBlurFeatureFrame {
|
|
33
36
|
readonly params: MotionBlurParams | undefined;
|
|
34
37
|
readonly demanded: boolean;
|
|
38
|
+
/** Raw accepted-submit interval; it is never replaced by ECS clamp time. */
|
|
39
|
+
readonly frameDeltaSeconds: number;
|
|
40
|
+
/** Temporal reset already admitted by the shared renderer transaction. */
|
|
41
|
+
readonly reset: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MotionBlurFeatureInput {
|
|
45
|
+
readonly params: Partial<MotionBlurParams> | undefined;
|
|
46
|
+
readonly frameDeltaSeconds?: number;
|
|
47
|
+
readonly reset?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compute owner for the long-vector lane. The summary is one 32-byte record
|
|
52
|
+
* per 16x16 tile and the reconstruction is fused: a long vector reuses an
|
|
53
|
+
* even-pixel (half-resolution) source lattice and performs depth-guided
|
|
54
|
+
* acceptance in the same dispatch. There is no scatter, atomic, or
|
|
55
|
+
* radius-squared neighbourhood work.
|
|
56
|
+
*/
|
|
57
|
+
export const MOTION_BLUR_COMPUTE_WGSL = `struct MotionBlurComputeParams {
|
|
58
|
+
shutterAngle : f32,
|
|
59
|
+
maxRadiusPixels : f32,
|
|
60
|
+
sampleCount : u32,
|
|
61
|
+
reset : u32,
|
|
62
|
+
targetFps : f32,
|
|
63
|
+
exposureScale : f32,
|
|
64
|
+
frameDeltaSeconds : f32,
|
|
65
|
+
flags : u32,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
struct MotionBlurTileSummary {
|
|
69
|
+
// The two motion lanes retain the source pixel that supplied the direction.
|
|
70
|
+
// Those anchors let a bounded midpoint tap hit a sparse source without
|
|
71
|
+
// adding a color read or growing the 32-byte per-tile summary.
|
|
72
|
+
maxMotionAndLength : vec4<f32>,
|
|
73
|
+
secondaryMotionAndDepth : vec4<f32>,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
struct TemporalSample {
|
|
77
|
+
motion : vec2<f32>,
|
|
78
|
+
depth : f32,
|
|
79
|
+
reactive : f32,
|
|
80
|
+
validDepth : bool,
|
|
81
|
+
motionValid : bool,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
struct MotionBlurSupport {
|
|
85
|
+
primaryMotion : vec2<f32>,
|
|
86
|
+
secondaryMotion : vec2<f32>,
|
|
87
|
+
primaryAnchor : vec2<i32>,
|
|
88
|
+
secondaryAnchor : vec2<i32>,
|
|
89
|
+
primaryRadius : f32,
|
|
90
|
+
secondaryRadius : f32,
|
|
91
|
+
valid : bool,
|
|
92
|
+
longVector : bool,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
struct MotionBlurTap {
|
|
96
|
+
pixel : vec2<i32>,
|
|
97
|
+
fallbackPixel : vec2<i32>,
|
|
98
|
+
valid : bool,
|
|
99
|
+
fallbackValid : bool,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
struct MotionBlurFilter {
|
|
103
|
+
color0 : vec4<f32>,
|
|
104
|
+
color1 : vec4<f32>,
|
|
105
|
+
color2 : vec4<f32>,
|
|
106
|
+
color3 : vec4<f32>,
|
|
107
|
+
depth0 : f32,
|
|
108
|
+
depth1 : f32,
|
|
109
|
+
depth2 : f32,
|
|
110
|
+
depth3 : f32,
|
|
111
|
+
coverage0 : f32,
|
|
112
|
+
coverage1 : f32,
|
|
113
|
+
coverage2 : f32,
|
|
114
|
+
coverage3 : f32,
|
|
115
|
+
receiver : vec2<i32>,
|
|
116
|
+
motion : vec2<f32>,
|
|
117
|
+
radius : f32,
|
|
118
|
+
receiverMask : u32,
|
|
119
|
+
validMask : u32,
|
|
120
|
+
valid : bool,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
struct MotionBlurCacheSample {
|
|
124
|
+
color : vec4<f32>,
|
|
125
|
+
coverage : f32,
|
|
126
|
+
valid : bool,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
struct MotionBlurSource {
|
|
130
|
+
pixel : vec2<i32>,
|
|
131
|
+
motion : vec2<f32>,
|
|
132
|
+
depth : f32,
|
|
133
|
+
radius : f32,
|
|
134
|
+
coverage0 : f32,
|
|
135
|
+
coverage1 : f32,
|
|
136
|
+
coverage2 : f32,
|
|
137
|
+
coverage3 : f32,
|
|
138
|
+
receiverMask : u32,
|
|
139
|
+
valid : bool,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
@group(0) @binding(0) var currentColor : texture_2d<f32>;
|
|
143
|
+
@group(0) @binding(1) var sceneTemporal : texture_2d<f32>;
|
|
144
|
+
@group(0) @binding(2) var<storage, read_write> tileSummary : array<MotionBlurTileSummary>;
|
|
145
|
+
@group(0) @binding(3) var outputColor : texture_storage_2d<rgba16float, write>;
|
|
146
|
+
@group(0) @binding(4) var<uniform> params : MotionBlurComputeParams;
|
|
147
|
+
|
|
148
|
+
// Reconstruct a bounded part of a partially covered opaque edge and fund it
|
|
149
|
+
// by attenuating the matching empty-background trail. The fixed factor keeps
|
|
150
|
+
// the work and the energy adjustment independent of the blur radius.
|
|
151
|
+
const EDGE_FILL_FACTOR : f32 = 0.99;
|
|
152
|
+
|
|
153
|
+
// One 8x8 workgroup covers a 16x16 tile. Each invocation reads a 2x2
|
|
154
|
+
// footprint, then the workgroup reduction combines the values without
|
|
155
|
+
// write contention or a serial 256-pixel loop.
|
|
156
|
+
var<workgroup> tileMotionScratch : array<vec2<f32>, 64>;
|
|
157
|
+
var<workgroup> tileLengthScratch : array<f32, 64>;
|
|
158
|
+
var<workgroup> tileSourceScratch : array<vec2<i32>, 64>;
|
|
159
|
+
var<workgroup> tileSecondaryMotionScratch : array<vec2<f32>, 64>;
|
|
160
|
+
var<workgroup> tileSecondaryLengthScratch : array<f32, 64>;
|
|
161
|
+
var<workgroup> tileSecondarySourceScratch : array<vec2<i32>, 64>;
|
|
162
|
+
// The reconstruction workgroup owns a 4x4 half-resolution result cache for
|
|
163
|
+
// its 8x8 output footprint. Each owner computes the complete bounded support
|
|
164
|
+
// filter once. Full-resolution lanes only select a depth-compatible cached
|
|
165
|
+
// result after the uniform barrier, with no cross-workgroup dependency.
|
|
166
|
+
// Four quadrants are kept per half-cell. Each owner pays four receiver
|
|
167
|
+
// center-color reads and at most supportCount (<=15) accepted source-color
|
|
168
|
+
// reads; midpoint anchor recovery reuses an existing slot and adds no read
|
|
169
|
+
// budget. The shared result retains odd/even coverage instead of spreading an
|
|
170
|
+
// odd row across both rows.
|
|
171
|
+
var<workgroup> halfFilteredColor : array<vec4<f32>, 64>;
|
|
172
|
+
var<workgroup> halfFilteredDepth : array<f32, 64>;
|
|
173
|
+
var<workgroup> halfFilteredCoverage : array<f32, 64>;
|
|
174
|
+
var<workgroup> halfFilteredReceiver : array<vec2<i32>, 16>;
|
|
175
|
+
var<workgroup> halfFilteredMotion : array<vec2<f32>, 16>;
|
|
176
|
+
var<workgroup> halfFilteredRadius : array<f32, 16>;
|
|
177
|
+
var<workgroup> halfFilteredValid : array<u32, 64>;
|
|
178
|
+
var<workgroup> longVectorScratch : array<u32, 64>;
|
|
179
|
+
// A reconstruct workgroup covers one 8x8 output footprint. Its first lane
|
|
180
|
+
// derives the complete bounded tile neighbourhood once and publishes the top
|
|
181
|
+
// two source directions for all lanes and all designated half-resolution
|
|
182
|
+
// owners. This keeps summary work O(P/T^2), rather than repeating a tile walk
|
|
183
|
+
// per output pixel.
|
|
184
|
+
var<workgroup> neighbourhoodPrimaryMotion : vec2<f32>;
|
|
185
|
+
var<workgroup> neighbourhoodPrimaryLength : f32;
|
|
186
|
+
var<workgroup> neighbourhoodPrimaryAnchor : vec2<i32>;
|
|
187
|
+
var<workgroup> neighbourhoodSecondaryMotion : vec2<f32>;
|
|
188
|
+
var<workgroup> neighbourhoodSecondaryLength : f32;
|
|
189
|
+
var<workgroup> neighbourhoodSecondaryAnchor : vec2<i32>;
|
|
190
|
+
|
|
191
|
+
// maxRadiusPixels is authored at [0,64], so a 16-pixel tile support reaches
|
|
192
|
+
// at most four tiles. The fixed 9x9 lattice is geometrically derived from its
|
|
193
|
+
// index, then intersected with the circular/AABB support. It is a constant
|
|
194
|
+
// bounded metadata walk: no loop scales with the requested radius and no
|
|
195
|
+
// radius-squared per-pixel storage or write path is introduced.
|
|
196
|
+
const MAX_TILE_REACH : i32 = 4;
|
|
197
|
+
const TILE_NEIGHBORHOOD_SIDE : u32 = 9u;
|
|
198
|
+
const TILE_NEIGHBORHOOD_COUNT : u32 = 81u;
|
|
199
|
+
|
|
200
|
+
fn finite(value : f32) -> bool {
|
|
201
|
+
return value == value && abs(value) < 3.402823e+38;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn pixelInBounds(pixel : vec2<i32>, dimensions : vec2<i32>) -> bool {
|
|
205
|
+
return pixel.x >= 0 && pixel.y >= 0 && pixel.x < dimensions.x && pixel.y < dimensions.y;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn decodeDepth(packed : f32) -> f32 {
|
|
209
|
+
if (packed < 0.0) { return 0.0; }
|
|
210
|
+
return exp2(packed) - 1.0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn temporalAt(pixel : vec2<i32>, dimensions : vec2<i32>) -> TemporalSample {
|
|
214
|
+
let samplePixel = clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1));
|
|
215
|
+
let packed = textureLoad(sceneTemporal, samplePixel, 0);
|
|
216
|
+
let invalidMotion = packed.w >= 2.0;
|
|
217
|
+
return TemporalSample(
|
|
218
|
+
packed.xy,
|
|
219
|
+
decodeDepth(packed.z),
|
|
220
|
+
clamp(packed.w - select(0.0, 2.0, invalidMotion), 0.0, 1.0),
|
|
221
|
+
packed.z >= 0.0,
|
|
222
|
+
!invalidMotion,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn depthReject(center : f32, sample : f32) -> bool {
|
|
227
|
+
return sample > center + max(0.01, center * 0.01);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn distinctDirection(a : vec2<f32>, b : vec2<f32>) -> bool {
|
|
231
|
+
let aLength = length(a);
|
|
232
|
+
let bLength = length(b);
|
|
233
|
+
if (aLength <= 1e-5 || bLength <= 1e-5) { return false; }
|
|
234
|
+
// Keep only genuinely different directions. Opposite signs still carry
|
|
235
|
+
// separate source segments, while nearly parallel candidates do not spend
|
|
236
|
+
// the bounded second-direction slot.
|
|
237
|
+
return abs(dot(a / aLength, b / bLength)) < 0.94;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn sourceVelocitySegmentCovers(
|
|
241
|
+
receiverPixel : vec2<i32>,
|
|
242
|
+
sourcePixel : vec2<i32>,
|
|
243
|
+
motionPixels : vec2<f32>,
|
|
244
|
+
radius : f32,
|
|
245
|
+
) -> bool {
|
|
246
|
+
let delta = vec2<f32>(receiverPixel - sourcePixel);
|
|
247
|
+
let motionLength = length(motionPixels);
|
|
248
|
+
if (motionLength <= 1e-5 || radius <= 1e-5) { return false; }
|
|
249
|
+
let direction = motionPixels / motionLength;
|
|
250
|
+
let along = dot(delta, direction);
|
|
251
|
+
let perpendicular = abs(delta.x * direction.y - delta.y * direction.x);
|
|
252
|
+
// A source segment owns a one-pixel coverage tube. This is a bounded
|
|
253
|
+
// geometric test, not a radius-sized source scan, and lets a sparse mover
|
|
254
|
+
// cover a static receiver without accepting a far/orthogonal source.
|
|
255
|
+
return abs(along) <= radius + 0.75 && perpendicular <= 0.75;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn symmetricOffset(index : u32, count : u32, motion : vec2<f32>) -> vec2<f32> {
|
|
259
|
+
let denominator = max(f32(count), 1.0);
|
|
260
|
+
return motion * (2.0 * (f32(index) + 0.5) / denominator - 1.0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn tileNeighborhoodOffset(index : u32) -> vec2<i32> {
|
|
264
|
+
let x = i32(index % TILE_NEIGHBORHOOD_SIDE) - MAX_TILE_REACH;
|
|
265
|
+
let y = i32(index / TILE_NEIGHBORHOOD_SIDE) - MAX_TILE_REACH;
|
|
266
|
+
return vec2<i32>(x, y);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn tileOffsetWithinSupport(offset : vec2<i32>, tileReach : i32) -> bool {
|
|
270
|
+
if (offset.x == 0 && offset.y == 0) { return false; }
|
|
271
|
+
let boundedReach = min(max(tileReach, 1), MAX_TILE_REACH);
|
|
272
|
+
// The bounded lattice is a square around the receiver tile. The precise
|
|
273
|
+
// AABB/circle intersection below decides whether a diagonal tile really
|
|
274
|
+
// intersects the support; a circular pre-filter here would drop the
|
|
275
|
+
// (±2,±2) and boundary cases before that exact test can run.
|
|
276
|
+
return abs(offset.x) <= boundedReach && abs(offset.y) <= boundedReach;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn tileIntersectsSupport(
|
|
280
|
+
receiverPixel : vec2<i32>,
|
|
281
|
+
candidateTile : vec2<i32>,
|
|
282
|
+
dimensions : vec2<i32>,
|
|
283
|
+
supportRadius : f32,
|
|
284
|
+
) -> bool {
|
|
285
|
+
// Use the receiver-to-tile-AABB lower bound. This includes every tile whose
|
|
286
|
+
// contents can intersect the support, including signs and diagonals, while
|
|
287
|
+
// rejecting an outside-support tile before loading its summary.
|
|
288
|
+
let tileMin = vec2<f32>(candidateTile * 16);
|
|
289
|
+
let tileMax = min(tileMin + vec2<f32>(16.0), vec2<f32>(dimensions));
|
|
290
|
+
let receiver = vec2<f32>(receiverPixel) + vec2<f32>(0.5);
|
|
291
|
+
let dx = max(max(tileMin.x - receiver.x, 0.0), receiver.x - tileMax.x);
|
|
292
|
+
let dy = max(max(tileMin.y - receiver.y, 0.0), receiver.y - tileMax.y);
|
|
293
|
+
let distance = vec2<f32>(dx, dy);
|
|
294
|
+
return dot(distance, distance) <= supportRadius * supportRadius + 1.0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fn tileIntersectsWorkgroupSupport(
|
|
298
|
+
workgroupBase : vec2<i32>,
|
|
299
|
+
candidateTile : vec2<i32>,
|
|
300
|
+
dimensions : vec2<i32>,
|
|
301
|
+
supportRadius : f32,
|
|
302
|
+
) -> bool {
|
|
303
|
+
// A workgroup's 8x8 rectangle is the conservative receiver domain for one
|
|
304
|
+
// shared metadata walk. The AABB lower bound includes every pixel in that
|
|
305
|
+
// rectangle that can reach the candidate tile, including diagonal/sign
|
|
306
|
+
// variants, while rejecting a tile fully outside the support.
|
|
307
|
+
let tileMin = vec2<f32>(candidateTile * 16);
|
|
308
|
+
let tileMax = min(tileMin + vec2<f32>(16.0), vec2<f32>(dimensions));
|
|
309
|
+
let groupMin = vec2<f32>(workgroupBase) + vec2<f32>(0.5);
|
|
310
|
+
let groupMax = min(groupMin + vec2<f32>(8.0), vec2<f32>(dimensions));
|
|
311
|
+
let dx = max(max(tileMin.x - groupMax.x, 0.0), groupMin.x - tileMax.x);
|
|
312
|
+
let dy = max(max(tileMin.y - groupMax.y, 0.0), groupMin.y - tileMax.y);
|
|
313
|
+
let distance = vec2<f32>(dx, dy);
|
|
314
|
+
return dot(distance, distance) <= supportRadius * supportRadius + 1.0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn scanTileNeighbourhood(
|
|
318
|
+
workgroupBase : vec2<i32>,
|
|
319
|
+
dimensions : vec2<i32>,
|
|
320
|
+
) {
|
|
321
|
+
let tiles = (vec2<u32>(dimensions) + vec2<u32>(15)) / vec2<u32>(16);
|
|
322
|
+
let tileCoordinate = workgroupBase / vec2<i32>(16);
|
|
323
|
+
let currentIndex = u32(tileCoordinate.y) * tiles.x + u32(tileCoordinate.x);
|
|
324
|
+
let current = tileSummary[currentIndex];
|
|
325
|
+
var primary = current.maxMotionAndLength.xy * vec2<f32>(dimensions) * params.exposureScale;
|
|
326
|
+
var primaryLength = length(primary);
|
|
327
|
+
var primaryAnchor = vec2<i32>(
|
|
328
|
+
i32(current.maxMotionAndLength.z),
|
|
329
|
+
i32(current.maxMotionAndLength.w),
|
|
330
|
+
);
|
|
331
|
+
var secondary = current.secondaryMotionAndDepth.xy * vec2<f32>(dimensions) * params.exposureScale;
|
|
332
|
+
var secondaryLength = length(secondary);
|
|
333
|
+
var secondaryAnchor = vec2<i32>(
|
|
334
|
+
i32(current.secondaryMotionAndDepth.z),
|
|
335
|
+
i32(current.secondaryMotionAndDepth.w),
|
|
336
|
+
);
|
|
337
|
+
let supportRadius = min(max(params.maxRadiusPixels, 1.0), 64.0);
|
|
338
|
+
let tileReach = min(MAX_TILE_REACH, max(1, i32(ceil(supportRadius / 16.0))));
|
|
339
|
+
for (var tileCandidateIndex = 0u; tileCandidateIndex < TILE_NEIGHBORHOOD_COUNT; tileCandidateIndex += 1u) {
|
|
340
|
+
let offset = tileNeighborhoodOffset(tileCandidateIndex);
|
|
341
|
+
if (!tileOffsetWithinSupport(offset, tileReach)) { continue; }
|
|
342
|
+
let candidateTile = tileCoordinate + offset;
|
|
343
|
+
if (
|
|
344
|
+
candidateTile.x < 0 ||
|
|
345
|
+
candidateTile.y < 0 ||
|
|
346
|
+
candidateTile.x >= i32(tiles.x) ||
|
|
347
|
+
candidateTile.y >= i32(tiles.y) ||
|
|
348
|
+
!tileIntersectsWorkgroupSupport(workgroupBase, candidateTile, dimensions, supportRadius)
|
|
349
|
+
) { continue; }
|
|
350
|
+
let candidate = tileSummary[u32(candidateTile.y) * tiles.x + u32(candidateTile.x)];
|
|
351
|
+
let candidatePrimary = candidate.maxMotionAndLength.xy * vec2<f32>(dimensions) * params.exposureScale;
|
|
352
|
+
let candidatePrimaryLength = length(candidatePrimary);
|
|
353
|
+
let candidatePrimaryAnchor = vec2<i32>(
|
|
354
|
+
i32(candidate.maxMotionAndLength.z),
|
|
355
|
+
i32(candidate.maxMotionAndLength.w),
|
|
356
|
+
);
|
|
357
|
+
if (candidatePrimaryLength > primaryLength) {
|
|
358
|
+
let previousPrimary = primary;
|
|
359
|
+
let previousLength = primaryLength;
|
|
360
|
+
let previousAnchor = primaryAnchor;
|
|
361
|
+
primary = candidatePrimary;
|
|
362
|
+
primaryLength = candidatePrimaryLength;
|
|
363
|
+
primaryAnchor = candidatePrimaryAnchor;
|
|
364
|
+
if (previousLength > 1e-5 && distinctDirection(primary, previousPrimary) && previousLength > secondaryLength) {
|
|
365
|
+
secondary = previousPrimary;
|
|
366
|
+
secondaryLength = previousLength;
|
|
367
|
+
secondaryAnchor = previousAnchor;
|
|
368
|
+
}
|
|
369
|
+
} else if (
|
|
370
|
+
candidatePrimaryLength > secondaryLength &&
|
|
371
|
+
distinctDirection(primary, candidatePrimary)
|
|
372
|
+
) {
|
|
373
|
+
secondary = candidatePrimary;
|
|
374
|
+
secondaryLength = candidatePrimaryLength;
|
|
375
|
+
secondaryAnchor = candidatePrimaryAnchor;
|
|
376
|
+
}
|
|
377
|
+
let candidateSecondary = candidate.secondaryMotionAndDepth.xy * vec2<f32>(dimensions) * params.exposureScale;
|
|
378
|
+
let candidateSecondaryLength = length(candidateSecondary);
|
|
379
|
+
let candidateSecondaryAnchor = vec2<i32>(
|
|
380
|
+
i32(candidate.secondaryMotionAndDepth.z),
|
|
381
|
+
i32(candidate.secondaryMotionAndDepth.w),
|
|
382
|
+
);
|
|
383
|
+
if (candidateSecondaryLength > secondaryLength && distinctDirection(primary, candidateSecondary)) {
|
|
384
|
+
secondary = candidateSecondary;
|
|
385
|
+
secondaryLength = candidateSecondaryLength;
|
|
386
|
+
secondaryAnchor = candidateSecondaryAnchor;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
neighbourhoodPrimaryMotion = primary;
|
|
390
|
+
neighbourhoodPrimaryLength = primaryLength;
|
|
391
|
+
neighbourhoodPrimaryAnchor = primaryAnchor;
|
|
392
|
+
neighbourhoodSecondaryMotion = secondary;
|
|
393
|
+
neighbourhoodSecondaryLength = secondaryLength;
|
|
394
|
+
neighbourhoodSecondaryAnchor = secondaryAnchor;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
fn validMotionParameters() -> bool {
|
|
398
|
+
return
|
|
399
|
+
finite(params.frameDeltaSeconds) &&
|
|
400
|
+
params.frameDeltaSeconds > 0.0 &&
|
|
401
|
+
params.frameDeltaSeconds <= 0.1 &&
|
|
402
|
+
finite(params.exposureScale) &&
|
|
403
|
+
params.exposureScale > 0.0 &&
|
|
404
|
+
finite(params.targetFps) &&
|
|
405
|
+
params.targetFps >= 0.0 &&
|
|
406
|
+
params.targetFps <= 240.0 &&
|
|
407
|
+
params.targetFps == floor(params.targetFps) &&
|
|
408
|
+
params.sampleCount >= 4u &&
|
|
409
|
+
params.maxRadiusPixels > 0.0 &&
|
|
410
|
+
finite(params.maxRadiusPixels) &&
|
|
411
|
+
params.shutterAngle > 0.0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn sourceMotionPixels(sample : TemporalSample, dimensions : vec2<i32>) -> vec2<f32> {
|
|
415
|
+
return sample.motion * vec2<f32>(dimensions) * params.exposureScale;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fn sourceRadius(motionPixels : vec2<f32>, shutter : f32) -> f32 {
|
|
419
|
+
return min(length(motionPixels) * shutter * 0.5, params.maxRadiusPixels);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
fn resolveMotionBlurSupport(
|
|
423
|
+
pixel : vec2<i32>,
|
|
424
|
+
dimensions : vec2<i32>,
|
|
425
|
+
) -> MotionBlurSupport {
|
|
426
|
+
var empty = MotionBlurSupport(
|
|
427
|
+
vec2<f32>(0.0),
|
|
428
|
+
vec2<f32>(0.0),
|
|
429
|
+
vec2<i32>(0),
|
|
430
|
+
vec2<i32>(0),
|
|
431
|
+
0.0,
|
|
432
|
+
0.0,
|
|
433
|
+
false,
|
|
434
|
+
false,
|
|
435
|
+
);
|
|
436
|
+
if (!pixelInBounds(pixel, dimensions) || !validMotionParameters() || params.reset != 0u) {
|
|
437
|
+
return empty;
|
|
438
|
+
}
|
|
439
|
+
let center = temporalAt(pixel, dimensions);
|
|
440
|
+
if (!center.validDepth || !center.motionValid) { return empty; }
|
|
441
|
+
let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
|
|
442
|
+
let centerMotion = sourceMotionPixels(center, dimensions);
|
|
443
|
+
let centerLength = length(centerMotion);
|
|
444
|
+
var primaryMotion = centerMotion;
|
|
445
|
+
var primaryLength = centerLength;
|
|
446
|
+
var primaryAnchor = pixel;
|
|
447
|
+
var secondaryMotion = neighbourhoodSecondaryMotion;
|
|
448
|
+
var secondaryLength = neighbourhoodSecondaryLength;
|
|
449
|
+
var secondaryAnchor = neighbourhoodSecondaryAnchor;
|
|
450
|
+
if (neighbourhoodPrimaryLength > primaryLength) {
|
|
451
|
+
if (distinctDirection(primaryMotion, neighbourhoodPrimaryMotion)) {
|
|
452
|
+
secondaryMotion = primaryMotion;
|
|
453
|
+
secondaryLength = primaryLength;
|
|
454
|
+
secondaryAnchor = pixel;
|
|
455
|
+
}
|
|
456
|
+
primaryMotion = neighbourhoodPrimaryMotion;
|
|
457
|
+
primaryLength = neighbourhoodPrimaryLength;
|
|
458
|
+
primaryAnchor = neighbourhoodPrimaryAnchor;
|
|
459
|
+
} else if (
|
|
460
|
+
neighbourhoodPrimaryLength > secondaryLength &&
|
|
461
|
+
distinctDirection(primaryMotion, neighbourhoodPrimaryMotion)
|
|
462
|
+
) {
|
|
463
|
+
secondaryMotion = neighbourhoodPrimaryMotion;
|
|
464
|
+
secondaryLength = neighbourhoodPrimaryLength;
|
|
465
|
+
secondaryAnchor = neighbourhoodPrimaryAnchor;
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
secondaryLength <= 1e-5 ||
|
|
469
|
+
!distinctDirection(primaryMotion, secondaryMotion)
|
|
470
|
+
) {
|
|
471
|
+
secondaryMotion = vec2<f32>(0.0);
|
|
472
|
+
secondaryLength = 0.0;
|
|
473
|
+
}
|
|
474
|
+
empty.primaryMotion = primaryMotion;
|
|
475
|
+
empty.secondaryMotion = secondaryMotion;
|
|
476
|
+
empty.primaryAnchor = primaryAnchor;
|
|
477
|
+
empty.secondaryAnchor = secondaryAnchor;
|
|
478
|
+
empty.primaryRadius = sourceRadius(primaryMotion, shutter);
|
|
479
|
+
empty.secondaryRadius = sourceRadius(secondaryMotion, shutter);
|
|
480
|
+
empty.valid = empty.primaryRadius > 1e-5 || empty.secondaryRadius > 1e-5;
|
|
481
|
+
empty.longVector = max(empty.primaryRadius, empty.secondaryRadius) > 8.0;
|
|
482
|
+
return empty;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn sourceAnchorForReceiver(
|
|
486
|
+
receiverPixel : vec2<i32>,
|
|
487
|
+
anchor : vec2<i32>,
|
|
488
|
+
motion : vec2<f32>,
|
|
489
|
+
dimensions : vec2<i32>,
|
|
490
|
+
) -> vec2<i32> {
|
|
491
|
+
// A tile summary has one source anchor, while a horizontal source may span
|
|
492
|
+
// the tile's perpendicular axis. Project that anchor onto the receiver's
|
|
493
|
+
// motion line before the midpoint read. The following source footprint
|
|
494
|
+
// validates the projected pixel, so a thin or off-axis source cannot be
|
|
495
|
+
// invented by the projection.
|
|
496
|
+
let motionLength = length(motion);
|
|
497
|
+
if (motionLength <= 1e-5) { return anchor; }
|
|
498
|
+
let direction = motion / motionLength;
|
|
499
|
+
let delta = vec2<f32>(receiverPixel - anchor);
|
|
500
|
+
let perpendicular = delta - direction * dot(delta, direction);
|
|
501
|
+
let projected = vec2<f32>(anchor) + perpendicular;
|
|
502
|
+
let candidate = vec2<i32>(floor(projected + vec2<f32>(0.5)));
|
|
503
|
+
return clamp(candidate, vec2<i32>(0), dimensions - vec2<i32>(1));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn supportTap(
|
|
507
|
+
receiverPixel : vec2<i32>,
|
|
508
|
+
index : u32,
|
|
509
|
+
count : u32,
|
|
510
|
+
motion : vec2<f32>,
|
|
511
|
+
radius : f32,
|
|
512
|
+
anchor : vec2<i32>,
|
|
513
|
+
dimensions : vec2<i32>,
|
|
514
|
+
) -> MotionBlurTap {
|
|
515
|
+
// Preserve the geometric midpoint as the primary sample. The source anchor
|
|
516
|
+
// is a recovery coordinate for a sparse tile summary, not a replacement for
|
|
517
|
+
// the receiver's authored support. It is tried only when this midpoint has
|
|
518
|
+
// no valid source, and therefore never adds a color read or a tap.
|
|
519
|
+
var fallbackPixel = vec2<i32>(0);
|
|
520
|
+
var fallbackValid = false;
|
|
521
|
+
if (index == count / 2u) {
|
|
522
|
+
fallbackPixel = sourceAnchorForReceiver(receiverPixel, anchor, motion, dimensions);
|
|
523
|
+
fallbackValid = pixelInBounds(fallbackPixel, dimensions);
|
|
524
|
+
}
|
|
525
|
+
let direction = motion / max(length(motion), 1e-5);
|
|
526
|
+
let offset = symmetricOffset(index, count, direction * radius);
|
|
527
|
+
let dimensionsF = vec2<f32>(dimensions);
|
|
528
|
+
let uv = (vec2<f32>(receiverPixel) + vec2<f32>(0.5) + offset) / dimensionsF;
|
|
529
|
+
if (uv.x < 0.0 || uv.y < 0.0 || uv.x >= 1.0 || uv.y >= 1.0) {
|
|
530
|
+
return MotionBlurTap(vec2<i32>(0), fallbackPixel, false, fallbackValid);
|
|
531
|
+
}
|
|
532
|
+
let candidate = vec2<i32>(floor(uv * dimensionsF));
|
|
533
|
+
return MotionBlurTap(candidate, fallbackPixel, pixelInBounds(candidate, dimensions), fallbackValid);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fn sourceAt(
|
|
537
|
+
receiverPixel : vec2<i32>,
|
|
538
|
+
sourcePixel : vec2<i32>,
|
|
539
|
+
centerDepth : f32,
|
|
540
|
+
shutter : f32,
|
|
541
|
+
dimensions : vec2<i32>,
|
|
542
|
+
) -> MotionBlurSource {
|
|
543
|
+
let empty = MotionBlurSource(
|
|
544
|
+
vec2<i32>(0),
|
|
545
|
+
vec2<f32>(0.0),
|
|
546
|
+
0.0,
|
|
547
|
+
0.0,
|
|
548
|
+
0.0,
|
|
549
|
+
0.0,
|
|
550
|
+
0.0,
|
|
551
|
+
0.0,
|
|
552
|
+
0u,
|
|
553
|
+
false,
|
|
554
|
+
);
|
|
555
|
+
if (!pixelInBounds(sourcePixel, dimensions)) { return empty; }
|
|
556
|
+
let source = temporalAt(sourcePixel, dimensions);
|
|
557
|
+
if (!source.validDepth || !source.motionValid || depthReject(centerDepth, source.depth)) {
|
|
558
|
+
return empty;
|
|
559
|
+
}
|
|
560
|
+
let motionPixels = sourceMotionPixels(source, dimensions);
|
|
561
|
+
let radius = sourceRadius(motionPixels, shutter);
|
|
562
|
+
if (!sourceVelocitySegmentCovers(receiverPixel, sourcePixel, motionPixels, radius)) {
|
|
563
|
+
return empty;
|
|
564
|
+
}
|
|
565
|
+
return MotionBlurSource(
|
|
566
|
+
sourcePixel,
|
|
567
|
+
motionPixels,
|
|
568
|
+
source.depth,
|
|
569
|
+
radius,
|
|
570
|
+
1.0,
|
|
571
|
+
1.0,
|
|
572
|
+
1.0,
|
|
573
|
+
1.0,
|
|
574
|
+
0u,
|
|
575
|
+
true,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// A half-resolution owner represents a complete 2x2 receiving footprint. The
|
|
580
|
+
// source motion test is evaluated against every receiving pixel in that
|
|
581
|
+
// footprint, so odd/even rows and columns remain visible without a second
|
|
582
|
+
// filter pass or an unbounded neighbourhood walk.
|
|
583
|
+
fn sourceAtFootprint(
|
|
584
|
+
receiverPixel : vec2<i32>,
|
|
585
|
+
sourcePixel : vec2<i32>,
|
|
586
|
+
centerDepth : f32,
|
|
587
|
+
shutter : f32,
|
|
588
|
+
dimensions : vec2<i32>,
|
|
589
|
+
) -> MotionBlurSource {
|
|
590
|
+
var selected = MotionBlurSource(
|
|
591
|
+
vec2<i32>(0),
|
|
592
|
+
vec2<f32>(0.0),
|
|
593
|
+
0.0,
|
|
594
|
+
0.0,
|
|
595
|
+
0.0,
|
|
596
|
+
0.0,
|
|
597
|
+
0.0,
|
|
598
|
+
0.0,
|
|
599
|
+
0u,
|
|
600
|
+
false,
|
|
601
|
+
);
|
|
602
|
+
var selectedDepth = 3.402823e+38;
|
|
603
|
+
var coverage0 = 0.0;
|
|
604
|
+
var coverage1 = 0.0;
|
|
605
|
+
var coverage2 = 0.0;
|
|
606
|
+
var coverage3 = 0.0;
|
|
607
|
+
var selectedReceiverMask = 0u;
|
|
608
|
+
for (var y = 0u; y < 2u; y += 1u) {
|
|
609
|
+
for (var x = 0u; x < 2u; x += 1u) {
|
|
610
|
+
let candidatePixel = sourcePixel + vec2<i32>(i32(x), i32(y));
|
|
611
|
+
if (!pixelInBounds(candidatePixel, dimensions)) { continue; }
|
|
612
|
+
let temporal = temporalAt(candidatePixel, dimensions);
|
|
613
|
+
if (!temporal.validDepth || !temporal.motionValid || depthReject(centerDepth, temporal.depth)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
let motionPixels = sourceMotionPixels(temporal, dimensions);
|
|
617
|
+
let radius = sourceRadius(motionPixels, shutter);
|
|
618
|
+
var receiverMask = 0u;
|
|
619
|
+
// A source half-cell and a receiver half-cell share the same integer
|
|
620
|
+
// displacement. This one-to-one correspondence keeps a diagonal source
|
|
621
|
+
// from being counted for all four receiver quadrants just because its
|
|
622
|
+
// tube crosses the quadrant. The segment test still supplies the motion
|
|
623
|
+
// support and depth/validity checks remain source-owned.
|
|
624
|
+
let receiver = receiverPixel + vec2<i32>(i32(x), i32(y));
|
|
625
|
+
if (
|
|
626
|
+
pixelInBounds(receiver, dimensions) &&
|
|
627
|
+
sourceVelocitySegmentCovers(receiver, candidatePixel, motionPixels, radius)
|
|
628
|
+
) {
|
|
629
|
+
receiverMask = receiverMask | (1u << (y * 2u + x));
|
|
630
|
+
}
|
|
631
|
+
if (receiverMask == 0u) { continue; }
|
|
632
|
+
if (!selected.valid || temporal.depth < selectedDepth) {
|
|
633
|
+
selected = MotionBlurSource(
|
|
634
|
+
candidatePixel,
|
|
635
|
+
motionPixels,
|
|
636
|
+
temporal.depth,
|
|
637
|
+
radius,
|
|
638
|
+
0.0,
|
|
639
|
+
0.0,
|
|
640
|
+
0.0,
|
|
641
|
+
0.0,
|
|
642
|
+
receiverMask,
|
|
643
|
+
true,
|
|
644
|
+
);
|
|
645
|
+
selectedDepth = temporal.depth;
|
|
646
|
+
coverage0 = 0.0;
|
|
647
|
+
coverage1 = 0.0;
|
|
648
|
+
coverage2 = 0.0;
|
|
649
|
+
coverage3 = 0.0;
|
|
650
|
+
selectedReceiverMask = receiverMask;
|
|
651
|
+
}
|
|
652
|
+
if (abs(temporal.depth - selectedDepth) <= 1e-4) {
|
|
653
|
+
// The common integer displacement above gives each source cell one
|
|
654
|
+
// receiver quadrant. The matching footprint therefore contributes its
|
|
655
|
+
// full unit overlap to that one owner.
|
|
656
|
+
if ((receiverMask & 1u) != 0u) { coverage0 += 1.0; }
|
|
657
|
+
if ((receiverMask & 2u) != 0u) { coverage1 += 1.0; }
|
|
658
|
+
if ((receiverMask & 4u) != 0u) { coverage2 += 1.0; }
|
|
659
|
+
if ((receiverMask & 8u) != 0u) { coverage3 += 1.0; }
|
|
660
|
+
selectedReceiverMask = selectedReceiverMask | receiverMask;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (selected.valid) {
|
|
665
|
+
// The owner samples a 2x2 source footprint but pays for one color read.
|
|
666
|
+
// Each receiving quadrant keeps the fraction of that footprint that is
|
|
667
|
+
// covered at the selected depth. This preserves energy at boundaries and
|
|
668
|
+
// carries a one-pixel odd-row feature into its real row only.
|
|
669
|
+
selected.coverage0 = min(coverage0, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 1u) != 0u);
|
|
670
|
+
selected.coverage1 = min(coverage1, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 2u) != 0u);
|
|
671
|
+
selected.coverage2 = min(coverage2, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 4u) != 0u);
|
|
672
|
+
selected.coverage3 = min(coverage3, 1.0) * select(0.0, 1.0, (selectedReceiverMask & 8u) != 0u);
|
|
673
|
+
selected.receiverMask = selectedReceiverMask;
|
|
674
|
+
}
|
|
675
|
+
return selected;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn midpointTileOwnership(
|
|
679
|
+
receiverPixel : vec2<i32>,
|
|
680
|
+
source : MotionBlurSource,
|
|
681
|
+
) -> f32 {
|
|
682
|
+
// A recovered tile anchor represents one two-pixel source footprint. The
|
|
683
|
+
// footprint is split across the finite, geometrically derived tile bands of
|
|
684
|
+
// the support segment. One half-cell owner per band claims the represented
|
|
685
|
+
// midpoint slot, rather than painting every receiver in the interval.
|
|
686
|
+
let sourcePixel = source.pixel;
|
|
687
|
+
let motionLength = length(source.motion);
|
|
688
|
+
if (motionLength <= 1e-5 || source.radius <= 1e-5) { return 0.0; }
|
|
689
|
+
let receiverTile = receiverPixel / vec2<i32>(16);
|
|
690
|
+
let sourceTile = sourcePixel / vec2<i32>(16);
|
|
691
|
+
let tileDelta = vec2<f32>(sourceTile - receiverTile);
|
|
692
|
+
let direction = source.motion / motionLength;
|
|
693
|
+
let along = abs(dot(tileDelta, direction));
|
|
694
|
+
let perpendicular = abs(tileDelta.x * direction.y - tileDelta.y * direction.x);
|
|
695
|
+
let tileReach = min(4, max(1, i32(ceil(source.radius / 16.0))));
|
|
696
|
+
if (along < 1.0 || ceil(along) > f32(tileReach) || perpendicular > 1.0) { return 0.0; }
|
|
697
|
+
let groupBase = (receiverPixel / vec2<i32>(8)) * vec2<i32>(8);
|
|
698
|
+
let local = receiverPixel - groupBase;
|
|
699
|
+
let ownerX = select(
|
|
700
|
+
clamp(((sourcePixel.x - groupBase.x) / 2) * 2, 0, 6),
|
|
701
|
+
select(0, 6, sourcePixel.x > receiverPixel.x),
|
|
702
|
+
sourcePixel.x != receiverPixel.x,
|
|
703
|
+
);
|
|
704
|
+
let ownerY = select(
|
|
705
|
+
clamp(((sourcePixel.y - groupBase.y) / 2) * 2, 0, 6),
|
|
706
|
+
select(0, 6, sourcePixel.y > receiverPixel.y),
|
|
707
|
+
sourcePixel.y != receiverPixel.y,
|
|
708
|
+
);
|
|
709
|
+
if (local.x != ownerX || local.y != ownerY) { return 0.0; }
|
|
710
|
+
let sourceFootprintLength = 2.0;
|
|
711
|
+
let supportLength = max(2.0 * source.radius, sourceFootprintLength);
|
|
712
|
+
// Divide the finite footprint ownership over every reachable tile band.
|
|
713
|
+
// This keeps the total recovery mass bounded when the support grows from
|
|
714
|
+
// 32 to 64 pixels while retaining a nonzero owner at any in-view band.
|
|
715
|
+
return min(1.0, sourceFootprintLength / (supportLength * f32(tileReach)));
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn integrateLongColor(
|
|
719
|
+
filtered : vec4<f32>,
|
|
720
|
+
validCount : f32,
|
|
721
|
+
hasAcceptedSource : bool,
|
|
722
|
+
uncovered : f32,
|
|
723
|
+
count : u32,
|
|
724
|
+
supportCount : u32,
|
|
725
|
+
centerColor : vec4<f32>,
|
|
726
|
+
centerMoving : bool,
|
|
727
|
+
) -> vec4<f32> {
|
|
728
|
+
// Uncovered ownership is decremented at the same time as validCount is incremented
|
|
729
|
+
// for both geometric and midpoint-recovered samples. Keep the derived cap
|
|
730
|
+
// as a guard against floating point accumulation and make the ownership
|
|
731
|
+
// transfer explicit instead of adding an independent brightness term.
|
|
732
|
+
let conservedUncovered = min(
|
|
733
|
+
max(uncovered, 0.0),
|
|
734
|
+
max(f32(supportCount) - validCount, 0.0),
|
|
735
|
+
);
|
|
736
|
+
// A moving receiver can be partially covered at a silhouette edge. Use the
|
|
737
|
+
// accepted foreground only for the receiver's missing current coverage; a
|
|
738
|
+
// fully covered receiver keeps the original center contribution and the
|
|
739
|
+
// Dawn energy contract remains unchanged for uniform opaque surfaces.
|
|
740
|
+
let acceptedColor = select(
|
|
741
|
+
vec3<f32>(0.0),
|
|
742
|
+
filtered.rgb / max(validCount, 1.0),
|
|
743
|
+
hasAcceptedSource && validCount > 0.0,
|
|
744
|
+
);
|
|
745
|
+
// Preserve the color target's authored alpha as the output floor. It is
|
|
746
|
+
// separate from the RGB test below, which identifies an empty background
|
|
747
|
+
// receiver without treating alpha as a scene-coverage attachment.
|
|
748
|
+
let missingForeground = 1.0 - clamp(centerColor.a, 0.0, 1.0);
|
|
749
|
+
let edgeCoverage = select(missingForeground, 1.0, centerMoving);
|
|
750
|
+
let edgeFill = acceptedColor * edgeCoverage * EDGE_FILL_FACTOR;
|
|
751
|
+
let movingFallback = select(centerColor.rgb, edgeFill, hasAcceptedSource && validCount > 0.0);
|
|
752
|
+
let background = select(vec3<f32>(0.0), movingFallback, centerMoving);
|
|
753
|
+
// The accepted colour can come from a transparent receiver's moving
|
|
754
|
+
// foreground. Carry the same bounded support coverage into alpha so that
|
|
755
|
+
// RGB written outside the current silhouette is not composited as black.
|
|
756
|
+
// Preserve the receiver's authored coverage as a floor: blur must not make
|
|
757
|
+
// an already visible pixel disappear.
|
|
758
|
+
let acceptedAlpha = select(
|
|
759
|
+
0.0,
|
|
760
|
+
filtered.a / max(validCount, 1.0),
|
|
761
|
+
validCount > 0.0,
|
|
762
|
+
);
|
|
763
|
+
let acceptedCoverage = validCount + missingForeground * conservedUncovered;
|
|
764
|
+
let reconstructedAlpha = acceptedAlpha * acceptedCoverage / max(f32(count), 1.0);
|
|
765
|
+
let outputAlpha = clamp(max(centerColor.a, reconstructedAlpha), 0.0, 1.0);
|
|
766
|
+
let emptyReceiver =
|
|
767
|
+
!centerMoving &&
|
|
768
|
+
hasAcceptedSource &&
|
|
769
|
+
max(max(abs(centerColor.r), abs(centerColor.g)), abs(centerColor.b)) < 1e-4;
|
|
770
|
+
let sourceScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
|
|
771
|
+
let sourceContribution = filtered.rgb * sourceScale;
|
|
772
|
+
let trailScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
|
|
773
|
+
let trailContribution = background * conservedUncovered * trailScale;
|
|
774
|
+
return vec4<f32>(
|
|
775
|
+
(sourceContribution + centerColor.rgb + trailContribution) / max(f32(count), 1.0),
|
|
776
|
+
outputAlpha,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn longSupportFilter(
|
|
781
|
+
receiverPixel : vec2<i32>,
|
|
782
|
+
support : MotionBlurSupport,
|
|
783
|
+
centerDepth : f32,
|
|
784
|
+
dimensions : vec2<i32>,
|
|
785
|
+
) -> MotionBlurFilter {
|
|
786
|
+
var empty = MotionBlurFilter(
|
|
787
|
+
vec4<f32>(0.0),
|
|
788
|
+
vec4<f32>(0.0),
|
|
789
|
+
vec4<f32>(0.0),
|
|
790
|
+
vec4<f32>(0.0),
|
|
791
|
+
0.0,
|
|
792
|
+
0.0,
|
|
793
|
+
0.0,
|
|
794
|
+
0.0,
|
|
795
|
+
0.0,
|
|
796
|
+
0.0,
|
|
797
|
+
0.0,
|
|
798
|
+
0.0,
|
|
799
|
+
receiverPixel,
|
|
800
|
+
vec2<f32>(0.0),
|
|
801
|
+
0.0,
|
|
802
|
+
0u,
|
|
803
|
+
0u,
|
|
804
|
+
false,
|
|
805
|
+
);
|
|
806
|
+
if (!support.valid) { return empty; }
|
|
807
|
+
let count = min(params.sampleCount, 16u);
|
|
808
|
+
// The authored tier is the total source colour-read budget. Reserve one slot
|
|
809
|
+
// for the receiver/background contribution; the remaining bounded taps
|
|
810
|
+
// (at most 15) are split between the two source directions when needed.
|
|
811
|
+
let supportCount = max(count - 1u, 1u);
|
|
812
|
+
let useSecondDirection = support.secondaryRadius > 1e-5 && distinctDirection(support.primaryMotion, support.secondaryMotion);
|
|
813
|
+
let primaryCount = select(supportCount, max(supportCount / 2u, 1u), useSecondDirection);
|
|
814
|
+
let secondaryCount = max(supportCount - primaryCount, 1u);
|
|
815
|
+
let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
|
|
816
|
+
let center = temporalAt(receiverPixel, dimensions);
|
|
817
|
+
// Keep each receiving quadrant's own center color. Reusing the owner
|
|
818
|
+
// pixel for all four quadrants causes a one-pixel parity shift to duplicate
|
|
819
|
+
// a bright source (or erase a bright receiver) in the half-resolution cache.
|
|
820
|
+
var centerColor0 = colorAt(receiverPixel, dimensions);
|
|
821
|
+
var centerColor1 = colorAt(receiverPixel + vec2<i32>(1, 0), dimensions);
|
|
822
|
+
var centerColor2 = colorAt(receiverPixel + vec2<i32>(0, 1), dimensions);
|
|
823
|
+
var centerColor3 = colorAt(receiverPixel + vec2<i32>(1, 1), dimensions);
|
|
824
|
+
var filtered0 = vec4<f32>(0.0);
|
|
825
|
+
var filtered1 = vec4<f32>(0.0);
|
|
826
|
+
var filtered2 = vec4<f32>(0.0);
|
|
827
|
+
var filtered3 = vec4<f32>(0.0);
|
|
828
|
+
var validCount0 = 0.0;
|
|
829
|
+
var validCount1 = 0.0;
|
|
830
|
+
var validCount2 = 0.0;
|
|
831
|
+
var validCount3 = 0.0;
|
|
832
|
+
var hasAcceptedSource0 = false;
|
|
833
|
+
var hasAcceptedSource1 = false;
|
|
834
|
+
var hasAcceptedSource2 = false;
|
|
835
|
+
var hasAcceptedSource3 = false;
|
|
836
|
+
var uncovered0 = f32(supportCount);
|
|
837
|
+
var uncovered1 = f32(supportCount);
|
|
838
|
+
var uncovered2 = f32(supportCount);
|
|
839
|
+
var uncovered3 = f32(supportCount);
|
|
840
|
+
var midpointFallbackPixel = vec2<i32>(0);
|
|
841
|
+
var midpointFallbackValid = false;
|
|
842
|
+
var midpointFallbackRadius = 0.0;
|
|
843
|
+
var nearestDepth0 = 3.402823e+38;
|
|
844
|
+
var nearestDepth1 = 3.402823e+38;
|
|
845
|
+
var nearestDepth2 = 3.402823e+38;
|
|
846
|
+
var nearestDepth3 = 3.402823e+38;
|
|
847
|
+
for (var index = 0u; index < 16u; index += 1u) {
|
|
848
|
+
if (index >= supportCount) { break; }
|
|
849
|
+
let secondDirection = useSecondDirection && index >= primaryCount;
|
|
850
|
+
let localIndex = select(index, index - primaryCount, secondDirection);
|
|
851
|
+
let localCount = select(primaryCount, secondaryCount, secondDirection);
|
|
852
|
+
let selectedMotion = select(support.primaryMotion, support.secondaryMotion, secondDirection);
|
|
853
|
+
let selectedRadius = select(support.primaryRadius, support.secondaryRadius, secondDirection);
|
|
854
|
+
let tap = supportTap(
|
|
855
|
+
receiverPixel,
|
|
856
|
+
localIndex,
|
|
857
|
+
localCount,
|
|
858
|
+
selectedMotion,
|
|
859
|
+
selectedRadius,
|
|
860
|
+
select(support.primaryAnchor, support.secondaryAnchor, secondDirection),
|
|
861
|
+
dimensions,
|
|
862
|
+
);
|
|
863
|
+
if (!tap.valid) {
|
|
864
|
+
if (tap.fallbackValid && selectedRadius > midpointFallbackRadius) {
|
|
865
|
+
midpointFallbackPixel = tap.fallbackPixel;
|
|
866
|
+
midpointFallbackValid = true;
|
|
867
|
+
midpointFallbackRadius = selectedRadius;
|
|
868
|
+
}
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
let source = sourceAtFootprint(
|
|
872
|
+
receiverPixel,
|
|
873
|
+
tap.pixel,
|
|
874
|
+
centerDepth,
|
|
875
|
+
shutter,
|
|
876
|
+
dimensions,
|
|
877
|
+
);
|
|
878
|
+
if (!source.valid) {
|
|
879
|
+
if (tap.fallbackValid && selectedRadius > midpointFallbackRadius) {
|
|
880
|
+
midpointFallbackPixel = tap.fallbackPixel;
|
|
881
|
+
midpointFallbackValid = true;
|
|
882
|
+
midpointFallbackRadius = selectedRadius;
|
|
883
|
+
}
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
let sampleColor = colorAt(source.pixel, dimensions);
|
|
887
|
+
// Keep the receiver's own color as the center contribution. A source
|
|
888
|
+
// footprint may cover an adjacent receiver quadrant without being the
|
|
889
|
+
// receiver itself; substituting its color here creates odd/even-alignment
|
|
890
|
+
// brightening at thin-object boundaries.
|
|
891
|
+
if ((source.receiverMask & 1u) != 0u) {
|
|
892
|
+
filtered0 += sampleColor * source.coverage0;
|
|
893
|
+
validCount0 += source.coverage0;
|
|
894
|
+
hasAcceptedSource0 = true;
|
|
895
|
+
uncovered0 = max(uncovered0 - source.coverage0, 0.0);
|
|
896
|
+
nearestDepth0 = min(nearestDepth0, source.depth);
|
|
897
|
+
}
|
|
898
|
+
if ((source.receiverMask & 2u) != 0u) {
|
|
899
|
+
filtered1 += sampleColor * source.coverage1;
|
|
900
|
+
validCount1 += source.coverage1;
|
|
901
|
+
hasAcceptedSource1 = true;
|
|
902
|
+
uncovered1 = max(uncovered1 - source.coverage1, 0.0);
|
|
903
|
+
nearestDepth1 = min(nearestDepth1, source.depth);
|
|
904
|
+
}
|
|
905
|
+
if ((source.receiverMask & 4u) != 0u) {
|
|
906
|
+
filtered2 += sampleColor * source.coverage2;
|
|
907
|
+
validCount2 += source.coverage2;
|
|
908
|
+
hasAcceptedSource2 = true;
|
|
909
|
+
uncovered2 = max(uncovered2 - source.coverage2, 0.0);
|
|
910
|
+
nearestDepth2 = min(nearestDepth2, source.depth);
|
|
911
|
+
}
|
|
912
|
+
if ((source.receiverMask & 8u) != 0u) {
|
|
913
|
+
filtered3 += sampleColor * source.coverage3;
|
|
914
|
+
validCount3 += source.coverage3;
|
|
915
|
+
hasAcceptedSource3 = true;
|
|
916
|
+
uncovered3 = max(uncovered3 - source.coverage3, 0.0);
|
|
917
|
+
nearestDepth3 = min(nearestDepth3, source.depth);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// A tile anchor recovers a sparse source only for the widest support tier,
|
|
921
|
+
// where the fixed geometric lattice can leave a source between taps. It
|
|
922
|
+
// spends the represented midpoint ownership of the finite support segment;
|
|
923
|
+
// every accepted colour, coverage and depth contribution uses that same
|
|
924
|
+
// bounded ownership, so the fallback cannot create an opaque stripe. The
|
|
925
|
+
// shorter tiers keep their exact geometric energy and do not need recovery.
|
|
926
|
+
if (midpointFallbackValid && max(support.primaryRadius, support.secondaryRadius) > 32.0) {
|
|
927
|
+
let fallbackSource = sourceAtFootprint(
|
|
928
|
+
receiverPixel,
|
|
929
|
+
midpointFallbackPixel,
|
|
930
|
+
centerDepth,
|
|
931
|
+
shutter,
|
|
932
|
+
dimensions,
|
|
933
|
+
);
|
|
934
|
+
if (fallbackSource.valid) {
|
|
935
|
+
let midpointWeight = midpointTileOwnership(receiverPixel, fallbackSource);
|
|
936
|
+
if (
|
|
937
|
+
midpointWeight > 0.0
|
|
938
|
+
) {
|
|
939
|
+
let fallbackColor = colorAt(fallbackSource.pixel, dimensions);
|
|
940
|
+
let fallbackWeight0 = fallbackSource.coverage0 * midpointWeight;
|
|
941
|
+
let fallbackWeight1 = fallbackSource.coverage1 * midpointWeight;
|
|
942
|
+
let fallbackWeight2 = fallbackSource.coverage2 * midpointWeight;
|
|
943
|
+
let fallbackWeight3 = fallbackSource.coverage3 * midpointWeight;
|
|
944
|
+
if (validCount0 <= 0.0 && (fallbackSource.receiverMask & 1u) != 0u) {
|
|
945
|
+
let ownedWeight0 = min(fallbackWeight0, uncovered0);
|
|
946
|
+
if (ownedWeight0 > 0.0) {
|
|
947
|
+
filtered0 += fallbackColor * ownedWeight0;
|
|
948
|
+
validCount0 += ownedWeight0;
|
|
949
|
+
hasAcceptedSource0 = true;
|
|
950
|
+
uncovered0 = max(uncovered0 - ownedWeight0, 0.0);
|
|
951
|
+
nearestDepth0 = min(nearestDepth0, fallbackSource.depth);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (validCount1 <= 0.0 && (fallbackSource.receiverMask & 2u) != 0u) {
|
|
955
|
+
let ownedWeight1 = min(fallbackWeight1, uncovered1);
|
|
956
|
+
if (ownedWeight1 > 0.0) {
|
|
957
|
+
filtered1 += fallbackColor * ownedWeight1;
|
|
958
|
+
validCount1 += ownedWeight1;
|
|
959
|
+
hasAcceptedSource1 = true;
|
|
960
|
+
uncovered1 = max(uncovered1 - ownedWeight1, 0.0);
|
|
961
|
+
nearestDepth1 = min(nearestDepth1, fallbackSource.depth);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (validCount2 <= 0.0 && (fallbackSource.receiverMask & 4u) != 0u) {
|
|
965
|
+
let ownedWeight2 = min(fallbackWeight2, uncovered2);
|
|
966
|
+
if (ownedWeight2 > 0.0) {
|
|
967
|
+
filtered2 += fallbackColor * ownedWeight2;
|
|
968
|
+
validCount2 += ownedWeight2;
|
|
969
|
+
hasAcceptedSource2 = true;
|
|
970
|
+
uncovered2 = max(uncovered2 - ownedWeight2, 0.0);
|
|
971
|
+
nearestDepth2 = min(nearestDepth2, fallbackSource.depth);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (validCount3 <= 0.0 && (fallbackSource.receiverMask & 8u) != 0u) {
|
|
975
|
+
let ownedWeight3 = min(fallbackWeight3, uncovered3);
|
|
976
|
+
if (ownedWeight3 > 0.0) {
|
|
977
|
+
filtered3 += fallbackColor * ownedWeight3;
|
|
978
|
+
validCount3 += ownedWeight3;
|
|
979
|
+
hasAcceptedSource3 = true;
|
|
980
|
+
uncovered3 = max(uncovered3 - ownedWeight3, 0.0);
|
|
981
|
+
nearestDepth3 = min(nearestDepth3, fallbackSource.depth);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
let moving0 = temporalAt(receiverPixel, dimensions);
|
|
988
|
+
let moving1 = temporalAt(receiverPixel + vec2<i32>(1, 0), dimensions);
|
|
989
|
+
let moving2 = temporalAt(receiverPixel + vec2<i32>(0, 1), dimensions);
|
|
990
|
+
let moving3 = temporalAt(receiverPixel + vec2<i32>(1, 1), dimensions);
|
|
991
|
+
let motion0 = sourceMotionPixels(moving0, dimensions);
|
|
992
|
+
let motion1 = sourceMotionPixels(moving1, dimensions);
|
|
993
|
+
let motion2 = sourceMotionPixels(moving2, dimensions);
|
|
994
|
+
let motion3 = sourceMotionPixels(moving3, dimensions);
|
|
995
|
+
let centerMoving0 = moving0.validDepth && moving0.motionValid && sourceRadius(motion0, shutter) > 1e-5;
|
|
996
|
+
let centerMoving1 = moving1.validDepth && moving1.motionValid && sourceRadius(motion1, shutter) > 1e-5;
|
|
997
|
+
let centerMoving2 = moving2.validDepth && moving2.motionValid && sourceRadius(motion2, shutter) > 1e-5;
|
|
998
|
+
let centerMoving3 = moving3.validDepth && moving3.motionValid && sourceRadius(motion3, shutter) > 1e-5;
|
|
999
|
+
empty.color0 = integrateLongColor(filtered0, validCount0, hasAcceptedSource0, uncovered0, count, supportCount, centerColor0, centerMoving0);
|
|
1000
|
+
empty.color1 = integrateLongColor(filtered1, validCount1, hasAcceptedSource1, uncovered1, count, supportCount, centerColor1, centerMoving1);
|
|
1001
|
+
empty.color2 = integrateLongColor(filtered2, validCount2, hasAcceptedSource2, uncovered2, count, supportCount, centerColor2, centerMoving2);
|
|
1002
|
+
empty.color3 = integrateLongColor(filtered3, validCount3, hasAcceptedSource3, uncovered3, count, supportCount, centerColor3, centerMoving3);
|
|
1003
|
+
empty.depth0 = select(centerDepth, nearestDepth0, validCount0 > 0.0);
|
|
1004
|
+
empty.depth1 = select(centerDepth, nearestDepth1, validCount1 > 0.0);
|
|
1005
|
+
empty.depth2 = select(centerDepth, nearestDepth2, validCount2 > 0.0);
|
|
1006
|
+
empty.depth3 = select(centerDepth, nearestDepth3, validCount3 > 0.0);
|
|
1007
|
+
empty.coverage0 = validCount0 / max(f32(supportCount), 1.0);
|
|
1008
|
+
empty.coverage1 = validCount1 / max(f32(supportCount), 1.0);
|
|
1009
|
+
empty.coverage2 = validCount2 / max(f32(supportCount), 1.0);
|
|
1010
|
+
empty.coverage3 = validCount3 / max(f32(supportCount), 1.0);
|
|
1011
|
+
empty.motion = select(support.primaryMotion, support.secondaryMotion, support.primaryRadius <= 1e-5);
|
|
1012
|
+
empty.radius = max(support.primaryRadius, support.secondaryRadius);
|
|
1013
|
+
empty.receiverMask =
|
|
1014
|
+
select(0u, 1u, validCount0 > 0.0 || centerMoving0) |
|
|
1015
|
+
select(0u, 2u, validCount1 > 0.0 || centerMoving1) |
|
|
1016
|
+
select(0u, 4u, validCount2 > 0.0 || centerMoving2) |
|
|
1017
|
+
select(0u, 8u, validCount3 > 0.0 || centerMoving3);
|
|
1018
|
+
empty.validMask =
|
|
1019
|
+
select(0u, 1u, validCount0 > 0.0 || centerMoving0) |
|
|
1020
|
+
select(0u, 2u, validCount1 > 0.0 || centerMoving1) |
|
|
1021
|
+
select(0u, 4u, validCount2 > 0.0 || centerMoving2) |
|
|
1022
|
+
select(0u, 8u, validCount3 > 0.0 || centerMoving3);
|
|
1023
|
+
empty.valid = empty.validMask != 0u;
|
|
1024
|
+
return empty;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
fn colorAt(pixel : vec2<i32>, dimensions : vec2<i32>) -> vec4<f32> {
|
|
1028
|
+
return textureLoad(currentColor, clamp(pixel, vec2<i32>(0), dimensions - vec2<i32>(1)), 0);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
fn cacheFootprintContains(receiverPixel : vec2<i32>, ownerPixel : vec2<i32>) -> bool {
|
|
1032
|
+
return
|
|
1033
|
+
receiverPixel.x >= ownerPixel.x && receiverPixel.x <= ownerPixel.x + 1 &&
|
|
1034
|
+
receiverPixel.y >= ownerPixel.y && receiverPixel.y <= ownerPixel.y + 1;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
fn cachedSupportCovers(
|
|
1038
|
+
receiverPixel : vec2<i32>,
|
|
1039
|
+
ownerPixel : vec2<i32>,
|
|
1040
|
+
motion : vec2<f32>,
|
|
1041
|
+
radius : f32,
|
|
1042
|
+
) -> bool {
|
|
1043
|
+
if (!cacheFootprintContains(receiverPixel, ownerPixel)) { return false; }
|
|
1044
|
+
let motionLength = length(motion);
|
|
1045
|
+
if (motionLength <= 1e-5 || radius <= 1e-5) { return false; }
|
|
1046
|
+
let direction = motion / motionLength;
|
|
1047
|
+
let delta = vec2<f32>(receiverPixel - ownerPixel);
|
|
1048
|
+
let along = dot(delta, direction);
|
|
1049
|
+
let perpendicular = abs(delta.x * direction.y - delta.y * direction.x);
|
|
1050
|
+
return abs(along) <= radius + 1.0 && perpendicular <= 1.0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
fn cachedDirectionMatches(support : MotionBlurSupport, motion : vec2<f32>) -> bool {
|
|
1054
|
+
let motionLength = length(motion);
|
|
1055
|
+
if (motionLength <= 1e-5) { return false; }
|
|
1056
|
+
let direction = motion / motionLength;
|
|
1057
|
+
if (support.primaryRadius > 1e-5 && abs(dot(normalize(support.primaryMotion), direction)) >= 0.94) {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
return support.secondaryRadius > 1e-5 && abs(dot(normalize(support.secondaryMotion), direction)) >= 0.94;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
fn depthGuidedHalfResolutionColor(
|
|
1064
|
+
localId : vec3<u32>,
|
|
1065
|
+
receiverPixel : vec2<i32>,
|
|
1066
|
+
support : MotionBlurSupport,
|
|
1067
|
+
centerDepth : f32,
|
|
1068
|
+
) -> MotionBlurCacheSample {
|
|
1069
|
+
let cell = vec2<i32>(localId.xy / vec2<u32>(2));
|
|
1070
|
+
let quadrant = (localId.y % 2u) * 2u + (localId.x % 2u);
|
|
1071
|
+
var selected = MotionBlurCacheSample(vec4<f32>(0.0), 0.0, false);
|
|
1072
|
+
var selectedDelta = 3.402823e+38;
|
|
1073
|
+
// Search the bounded adjacent cache cells, but accept only a cell whose
|
|
1074
|
+
// authored 2x2 footprint and direction actually cover this receiver.
|
|
1075
|
+
for (var y = -1; y <= 1; y += 1) {
|
|
1076
|
+
for (var x = -1; x <= 1; x += 1) {
|
|
1077
|
+
let candidateCell = cell + vec2<i32>(x, y);
|
|
1078
|
+
if (candidateCell.x < 0 || candidateCell.y < 0 || candidateCell.x >= 4 || candidateCell.y >= 4) { continue; }
|
|
1079
|
+
let cacheCellIndex = u32(candidateCell.y * 4 + candidateCell.x);
|
|
1080
|
+
let index = cacheCellIndex * 4u + quadrant;
|
|
1081
|
+
if (
|
|
1082
|
+
halfFilteredValid[index] == 0u ||
|
|
1083
|
+
depthReject(centerDepth, halfFilteredDepth[index]) ||
|
|
1084
|
+
!cachedDirectionMatches(support, halfFilteredMotion[cacheCellIndex]) ||
|
|
1085
|
+
!cachedSupportCovers(
|
|
1086
|
+
receiverPixel,
|
|
1087
|
+
halfFilteredReceiver[cacheCellIndex],
|
|
1088
|
+
halfFilteredMotion[cacheCellIndex],
|
|
1089
|
+
halfFilteredRadius[cacheCellIndex],
|
|
1090
|
+
)
|
|
1091
|
+
) { continue; }
|
|
1092
|
+
let delta = abs(halfFilteredDepth[index] - centerDepth);
|
|
1093
|
+
if (!selected.valid || delta < selectedDelta) {
|
|
1094
|
+
selected.color = halfFilteredColor[index];
|
|
1095
|
+
selected.coverage = halfFilteredCoverage[index];
|
|
1096
|
+
selectedDelta = delta;
|
|
1097
|
+
selected.valid = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return selected;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
@compute @workgroup_size(8, 8)
|
|
1105
|
+
fn tile_summary(
|
|
1106
|
+
@builtin(global_invocation_id) id : vec3<u32>,
|
|
1107
|
+
@builtin(local_invocation_id) localId : vec3<u32>,
|
|
1108
|
+
@builtin(local_invocation_index) localIndex : u32,
|
|
1109
|
+
@builtin(workgroup_id) workgroupId : vec3<u32>,
|
|
1110
|
+
) {
|
|
1111
|
+
let dimensions = textureDimensions(sceneTemporal);
|
|
1112
|
+
let tiles = (dimensions + vec2<u32>(15)) / vec2<u32>(16);
|
|
1113
|
+
let tile = workgroupId.xy;
|
|
1114
|
+
let inTile = tile.x < tiles.x && tile.y < tiles.y;
|
|
1115
|
+
var maxMotion = vec2<f32>(0.0);
|
|
1116
|
+
var maxLength = 0.0;
|
|
1117
|
+
var maxSource = vec2<i32>(0);
|
|
1118
|
+
var secondaryMotion = vec2<f32>(0.0);
|
|
1119
|
+
var secondaryLength = 0.0;
|
|
1120
|
+
var secondarySource = vec2<i32>(0);
|
|
1121
|
+
for (var localY = 0u; localY < 2u; localY += 1u) {
|
|
1122
|
+
for (var localX = 0u; localX < 2u; localX += 1u) {
|
|
1123
|
+
let pixel = tile * vec2<u32>(16) + localId.xy * vec2<u32>(2) + vec2<u32>(localX, localY);
|
|
1124
|
+
if (!inTile || pixel.x >= dimensions.x || pixel.y >= dimensions.y) { continue; }
|
|
1125
|
+
let sample = temporalAt(vec2<i32>(pixel), vec2<i32>(dimensions));
|
|
1126
|
+
if (!sample.validDepth || !sample.motionValid) { continue; }
|
|
1127
|
+
let motionLength = length(sample.motion * vec2<f32>(dimensions));
|
|
1128
|
+
if (motionLength > maxLength) {
|
|
1129
|
+
if (distinctDirection(maxMotion, sample.motion)) {
|
|
1130
|
+
secondaryMotion = maxMotion;
|
|
1131
|
+
secondaryLength = maxLength;
|
|
1132
|
+
secondarySource = maxSource;
|
|
1133
|
+
}
|
|
1134
|
+
maxLength = motionLength;
|
|
1135
|
+
maxMotion = sample.motion;
|
|
1136
|
+
maxSource = vec2<i32>(pixel);
|
|
1137
|
+
} else if (
|
|
1138
|
+
motionLength > secondaryLength &&
|
|
1139
|
+
distinctDirection(maxMotion, sample.motion)
|
|
1140
|
+
) {
|
|
1141
|
+
secondaryLength = motionLength;
|
|
1142
|
+
secondaryMotion = sample.motion;
|
|
1143
|
+
secondarySource = vec2<i32>(pixel);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
tileMotionScratch[localIndex] = maxMotion;
|
|
1148
|
+
tileLengthScratch[localIndex] = maxLength;
|
|
1149
|
+
tileSourceScratch[localIndex] = maxSource;
|
|
1150
|
+
tileSecondaryMotionScratch[localIndex] = secondaryMotion;
|
|
1151
|
+
tileSecondaryLengthScratch[localIndex] = secondaryLength;
|
|
1152
|
+
tileSecondarySourceScratch[localIndex] = secondarySource;
|
|
1153
|
+
workgroupBarrier();
|
|
1154
|
+
for (var stride = 32u; stride > 0u; stride = stride / 2u) {
|
|
1155
|
+
if (localIndex < stride) {
|
|
1156
|
+
let otherIndex = localIndex + stride;
|
|
1157
|
+
let otherLength = tileLengthScratch[otherIndex];
|
|
1158
|
+
if (otherLength > tileLengthScratch[localIndex]) {
|
|
1159
|
+
let oldMotion = tileMotionScratch[localIndex];
|
|
1160
|
+
let oldLength = tileLengthScratch[localIndex];
|
|
1161
|
+
let oldSource = tileSourceScratch[localIndex];
|
|
1162
|
+
tileLengthScratch[localIndex] = otherLength;
|
|
1163
|
+
tileMotionScratch[localIndex] = tileMotionScratch[otherIndex];
|
|
1164
|
+
tileSourceScratch[localIndex] = tileSourceScratch[otherIndex];
|
|
1165
|
+
if (
|
|
1166
|
+
oldLength > 1e-5 &&
|
|
1167
|
+
distinctDirection(tileMotionScratch[localIndex], oldMotion) &&
|
|
1168
|
+
oldLength > tileSecondaryLengthScratch[localIndex]
|
|
1169
|
+
) {
|
|
1170
|
+
tileSecondaryLengthScratch[localIndex] = oldLength;
|
|
1171
|
+
tileSecondaryMotionScratch[localIndex] = oldMotion;
|
|
1172
|
+
tileSecondarySourceScratch[localIndex] = oldSource;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (
|
|
1176
|
+
tileSecondaryLengthScratch[otherIndex] > tileSecondaryLengthScratch[localIndex] &&
|
|
1177
|
+
distinctDirection(
|
|
1178
|
+
tileMotionScratch[localIndex],
|
|
1179
|
+
tileSecondaryMotionScratch[otherIndex],
|
|
1180
|
+
)
|
|
1181
|
+
) {
|
|
1182
|
+
tileSecondaryLengthScratch[localIndex] = tileSecondaryLengthScratch[otherIndex];
|
|
1183
|
+
tileSecondaryMotionScratch[localIndex] = tileSecondaryMotionScratch[otherIndex];
|
|
1184
|
+
tileSecondarySourceScratch[localIndex] = tileSecondarySourceScratch[otherIndex];
|
|
1185
|
+
}
|
|
1186
|
+
if (
|
|
1187
|
+
tileLengthScratch[otherIndex] > tileSecondaryLengthScratch[localIndex] &&
|
|
1188
|
+
distinctDirection(tileMotionScratch[localIndex], tileMotionScratch[otherIndex])
|
|
1189
|
+
) {
|
|
1190
|
+
tileSecondaryLengthScratch[localIndex] = tileLengthScratch[otherIndex];
|
|
1191
|
+
tileSecondaryMotionScratch[localIndex] = tileMotionScratch[otherIndex];
|
|
1192
|
+
tileSecondarySourceScratch[localIndex] = tileSourceScratch[otherIndex];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
workgroupBarrier();
|
|
1196
|
+
}
|
|
1197
|
+
if (localIndex == 0u && inTile) {
|
|
1198
|
+
let tileIndex = tile.y * tiles.x + tile.x;
|
|
1199
|
+
tileSummary[tileIndex] = MotionBlurTileSummary(
|
|
1200
|
+
vec4<f32>(
|
|
1201
|
+
tileMotionScratch[0],
|
|
1202
|
+
f32(tileSourceScratch[0].x),
|
|
1203
|
+
f32(tileSourceScratch[0].y),
|
|
1204
|
+
),
|
|
1205
|
+
vec4<f32>(
|
|
1206
|
+
tileSecondaryMotionScratch[0],
|
|
1207
|
+
f32(tileSecondarySourceScratch[0].x),
|
|
1208
|
+
f32(tileSecondarySourceScratch[0].y),
|
|
1209
|
+
),
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
@compute @workgroup_size(8, 8)
|
|
1215
|
+
fn reconstruct(
|
|
1216
|
+
@builtin(global_invocation_id) id : vec3<u32>,
|
|
1217
|
+
@builtin(local_invocation_id) localId : vec3<u32>,
|
|
1218
|
+
@builtin(workgroup_id) workgroupId : vec3<u32>,
|
|
1219
|
+
) {
|
|
1220
|
+
let dimensions = textureDimensions(currentColor);
|
|
1221
|
+
let colorDimensions = vec2<i32>(dimensions);
|
|
1222
|
+
let pixel = vec2<i32>(id.xy);
|
|
1223
|
+
let inBounds = id.x < dimensions.x && id.y < dimensions.y;
|
|
1224
|
+
let workgroupBase = vec2<i32>(workgroupId.xy * vec2<u32>(8));
|
|
1225
|
+
let ownsHalfCell = (localId.x % 2u == 0u) && (localId.y % 2u == 0u);
|
|
1226
|
+
let halfIndex = (localId.y / 2u) * 4u + (localId.x / 2u);
|
|
1227
|
+
let localIndex = localId.y * 8u + localId.x;
|
|
1228
|
+
if (localIndex == 0u) {
|
|
1229
|
+
scanTileNeighbourhood(workgroupBase, colorDimensions);
|
|
1230
|
+
}
|
|
1231
|
+
workgroupBarrier();
|
|
1232
|
+
let support = resolveMotionBlurSupport(pixel, colorDimensions);
|
|
1233
|
+
let longVector = support.valid && support.longVector;
|
|
1234
|
+
|
|
1235
|
+
longVectorScratch[localId.y * 8u + localId.x] = select(0u, 1u, longVector);
|
|
1236
|
+
workgroupBarrier();
|
|
1237
|
+
for (var longStride = 32u; longStride > 0u; longStride = longStride / 2u) {
|
|
1238
|
+
let localIndex = localId.y * 8u + localId.x;
|
|
1239
|
+
if (localIndex < longStride) {
|
|
1240
|
+
longVectorScratch[localIndex] = max(
|
|
1241
|
+
longVectorScratch[localIndex],
|
|
1242
|
+
longVectorScratch[localIndex + longStride],
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
workgroupBarrier();
|
|
1246
|
+
}
|
|
1247
|
+
let anyLongVector = longVectorScratch[0] != 0u;
|
|
1248
|
+
if (anyLongVector && ownsHalfCell) {
|
|
1249
|
+
// Every designated owner evaluates the full support exactly once. The
|
|
1250
|
+
// cached result includes filtered color, nearest accepted depth, and
|
|
1251
|
+
// coverage; no 2x2 prefilter is substituted for the long gather.
|
|
1252
|
+
let sourcePixel = workgroupBase + vec2<i32>(localId.xy);
|
|
1253
|
+
let sourceSupport = resolveMotionBlurSupport(sourcePixel, colorDimensions);
|
|
1254
|
+
let filtered = longSupportFilter(
|
|
1255
|
+
sourcePixel,
|
|
1256
|
+
sourceSupport,
|
|
1257
|
+
temporalAt(sourcePixel, colorDimensions).depth,
|
|
1258
|
+
colorDimensions,
|
|
1259
|
+
);
|
|
1260
|
+
halfFilteredReceiver[halfIndex] = sourcePixel;
|
|
1261
|
+
halfFilteredMotion[halfIndex] = filtered.motion;
|
|
1262
|
+
halfFilteredRadius[halfIndex] = filtered.radius;
|
|
1263
|
+
let cacheBase = halfIndex * 4u;
|
|
1264
|
+
halfFilteredColor[cacheBase] = filtered.color0;
|
|
1265
|
+
halfFilteredColor[cacheBase + 1u] = filtered.color1;
|
|
1266
|
+
halfFilteredColor[cacheBase + 2u] = filtered.color2;
|
|
1267
|
+
halfFilteredColor[cacheBase + 3u] = filtered.color3;
|
|
1268
|
+
halfFilteredDepth[cacheBase] = filtered.depth0;
|
|
1269
|
+
halfFilteredDepth[cacheBase + 1u] = filtered.depth1;
|
|
1270
|
+
halfFilteredDepth[cacheBase + 2u] = filtered.depth2;
|
|
1271
|
+
halfFilteredDepth[cacheBase + 3u] = filtered.depth3;
|
|
1272
|
+
halfFilteredCoverage[cacheBase] = filtered.coverage0;
|
|
1273
|
+
halfFilteredCoverage[cacheBase + 1u] = filtered.coverage1;
|
|
1274
|
+
halfFilteredCoverage[cacheBase + 2u] = filtered.coverage2;
|
|
1275
|
+
halfFilteredCoverage[cacheBase + 3u] = filtered.coverage3;
|
|
1276
|
+
halfFilteredValid[cacheBase] = select(0u, 1u, (filtered.validMask & 1u) != 0u);
|
|
1277
|
+
halfFilteredValid[cacheBase + 1u] = select(0u, 1u, (filtered.validMask & 2u) != 0u);
|
|
1278
|
+
halfFilteredValid[cacheBase + 2u] = select(0u, 1u, (filtered.validMask & 4u) != 0u);
|
|
1279
|
+
halfFilteredValid[cacheBase + 3u] = select(0u, 1u, (filtered.validMask & 8u) != 0u);
|
|
1280
|
+
}
|
|
1281
|
+
workgroupBarrier();
|
|
1282
|
+
// No invocation can return before both uniform barriers above. This keeps
|
|
1283
|
+
// odd extents and out-of-bounds lanes from allowing a long source to read a
|
|
1284
|
+
// cache entry that another invocation has not published yet.
|
|
1285
|
+
if (!inBounds) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (!support.valid) {
|
|
1289
|
+
textureStore(outputColor, pixel, colorAt(pixel, colorDimensions));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if (longVector) {
|
|
1293
|
+
let cached = depthGuidedHalfResolutionColor(
|
|
1294
|
+
localId,
|
|
1295
|
+
pixel,
|
|
1296
|
+
support,
|
|
1297
|
+
temporalAt(pixel, colorDimensions).depth,
|
|
1298
|
+
);
|
|
1299
|
+
if (cached.valid) {
|
|
1300
|
+
textureStore(outputColor, pixel, cached.color);
|
|
1301
|
+
} else {
|
|
1302
|
+
textureStore(outputColor, pixel, colorAt(pixel, colorDimensions));
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
let useSecondDirection = support.secondaryRadius > 1e-5 && distinctDirection(support.primaryMotion, support.secondaryMotion);
|
|
1307
|
+
let count = min(params.sampleCount, 16u);
|
|
1308
|
+
let supportCount = max(count - 1u, 1u);
|
|
1309
|
+
let primaryCount = select(supportCount, max(supportCount / 2u, 1u), useSecondDirection);
|
|
1310
|
+
let secondaryCount = max(supportCount - primaryCount, 1u);
|
|
1311
|
+
let shutter = clamp(params.shutterAngle / 360.0, 0.0, 1.0);
|
|
1312
|
+
let center = temporalAt(pixel, colorDimensions);
|
|
1313
|
+
let centerMotion = sourceMotionPixels(center, colorDimensions);
|
|
1314
|
+
let centerMoving = center.validDepth && center.motionValid && sourceRadius(centerMotion, shutter) > 1e-5;
|
|
1315
|
+
let centerColor = colorAt(pixel, colorDimensions);
|
|
1316
|
+
var accum = vec4<f32>(0.0);
|
|
1317
|
+
var weight = 0.0;
|
|
1318
|
+
for (var index = 0u; index < 16u; index += 1u) {
|
|
1319
|
+
if (index >= supportCount) { break; }
|
|
1320
|
+
let secondDirection = useSecondDirection && index >= primaryCount;
|
|
1321
|
+
let localIndex = select(index, index - primaryCount, secondDirection);
|
|
1322
|
+
let localCount = select(primaryCount, secondaryCount, secondDirection);
|
|
1323
|
+
let selectedMotion = select(support.primaryMotion, support.secondaryMotion, secondDirection);
|
|
1324
|
+
let selectedRadius = select(support.primaryRadius, support.secondaryRadius, secondDirection);
|
|
1325
|
+
let tap = supportTap(
|
|
1326
|
+
pixel,
|
|
1327
|
+
localIndex,
|
|
1328
|
+
localCount,
|
|
1329
|
+
selectedMotion,
|
|
1330
|
+
selectedRadius,
|
|
1331
|
+
select(support.primaryAnchor, support.secondaryAnchor, secondDirection),
|
|
1332
|
+
colorDimensions,
|
|
1333
|
+
);
|
|
1334
|
+
if (!tap.valid) { continue; }
|
|
1335
|
+
let source = sourceAt(pixel, tap.pixel, center.depth, shutter, colorDimensions);
|
|
1336
|
+
if (!source.valid) { continue; }
|
|
1337
|
+
let sampleColor = colorAt(source.pixel, colorDimensions);
|
|
1338
|
+
accum += sampleColor;
|
|
1339
|
+
weight += 1.0;
|
|
1340
|
+
}
|
|
1341
|
+
let uncovered = max(f32(supportCount) - weight, 0.0);
|
|
1342
|
+
let acceptedColor = select(vec3<f32>(0.0), accum.rgb / max(weight, 1.0), weight > 0.0);
|
|
1343
|
+
let missingForeground = 1.0 - clamp(centerColor.a, 0.0, 1.0);
|
|
1344
|
+
let edgeCoverage = select(missingForeground, 1.0, centerMoving);
|
|
1345
|
+
let edgeFill = acceptedColor * edgeCoverage * EDGE_FILL_FACTOR;
|
|
1346
|
+
let movingFallback = select(centerColor.rgb, edgeFill, weight > 0.0);
|
|
1347
|
+
let uncoveredColor = select(vec3<f32>(0.0), movingFallback, centerMoving);
|
|
1348
|
+
let acceptedAlpha = select(0.0, accum.a / max(weight, 1.0), weight > 0.0);
|
|
1349
|
+
let acceptedCoverage = weight + missingForeground * uncovered;
|
|
1350
|
+
let reconstructedAlpha = acceptedAlpha * acceptedCoverage / max(f32(count), 1.0);
|
|
1351
|
+
let outputAlpha = clamp(max(centerColor.a, reconstructedAlpha), 0.0, 1.0);
|
|
1352
|
+
let emptyReceiver =
|
|
1353
|
+
!centerMoving &&
|
|
1354
|
+
weight > 0.0 &&
|
|
1355
|
+
max(max(abs(centerColor.r), abs(centerColor.g)), abs(centerColor.b)) < 1e-4;
|
|
1356
|
+
let sourceScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
|
|
1357
|
+
let sourceContribution = accum.rgb * sourceScale;
|
|
1358
|
+
let trailScale = select(1.0, 1.0 - EDGE_FILL_FACTOR, emptyReceiver);
|
|
1359
|
+
let trailContribution = uncoveredColor * uncovered * trailScale;
|
|
1360
|
+
textureStore(
|
|
1361
|
+
outputColor,
|
|
1362
|
+
pixel,
|
|
1363
|
+
vec4<f32>((sourceContribution + centerColor.rgb + trailContribution) / max(f32(count), 1.0), outputAlpha),
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
`;
|
|
1367
|
+
|
|
1368
|
+
export function encodeMotionBlurComputeParams(
|
|
1369
|
+
params: MotionBlurParams,
|
|
1370
|
+
frameDeltaSeconds = 1 / 60,
|
|
1371
|
+
reset = false,
|
|
1372
|
+
): Uint8Array {
|
|
1373
|
+
const bytes = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
|
|
1374
|
+
const view = new DataView(bytes.buffer);
|
|
1375
|
+
// Keep invalid timing out of the GPU parameter block. The reset bit carries
|
|
1376
|
+
// the discontinuity; a finite zero makes the bypass deterministic and
|
|
1377
|
+
// avoids propagating NaN payloads through backend validation.
|
|
1378
|
+
const uniformDeltaSeconds = Number.isFinite(frameDeltaSeconds) ? frameDeltaSeconds : 0;
|
|
1379
|
+
view.setFloat32(0, params.shutterAngle, true);
|
|
1380
|
+
view.setFloat32(4, params.maxRadiusPixels, true);
|
|
1381
|
+
view.setUint32(8, effectiveMotionBlurSampleCount(params.sampleCount), true);
|
|
1382
|
+
view.setUint32(12, reset ? 1 : 0, true);
|
|
1383
|
+
view.setFloat32(16, params.targetFps, true);
|
|
1384
|
+
view.setFloat32(20, motionBlurExposureScale(uniformDeltaSeconds, params.targetFps), true);
|
|
1385
|
+
view.setFloat32(24, uniformDeltaSeconds, true);
|
|
1386
|
+
view.setUint32(28, 0, true);
|
|
1387
|
+
return bytes;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function positiveDimension(value: number | undefined): number {
|
|
1391
|
+
return Number.isFinite(value) && (value as number) > 0 ? Math.ceil(value as number) : 1;
|
|
35
1392
|
}
|
|
36
1393
|
|
|
37
1394
|
export function planMotionBlur(
|
|
38
1395
|
params: MotionBlurParams,
|
|
39
1396
|
context: RenderFeaturePlanContext,
|
|
1397
|
+
temporal: Pick<MotionBlurFeatureFrame, 'frameDeltaSeconds' | 'reset'> = {
|
|
1398
|
+
frameDeltaSeconds: 1 / 60,
|
|
1399
|
+
reset: false,
|
|
1400
|
+
},
|
|
40
1401
|
): Result<RenderFeaturePlan, RenderError> {
|
|
41
1402
|
if (!motionBlurTemporalDemand(params)) return ok({ resources: [], passes: [] });
|
|
42
1403
|
try {
|
|
43
|
-
const
|
|
1404
|
+
const sceneTemporal = context.sceneData.require(SCENE_DATA_TEMPORAL_V1_SCHEMA);
|
|
44
1405
|
const outputFormat =
|
|
45
1406
|
context.targets.find((target) => target.name === 'motion-output')?.format ?? 'rgba16float';
|
|
1407
|
+
const fullscreen = {
|
|
1408
|
+
kind: 'fullscreen-program' as const,
|
|
1409
|
+
name: 'motion-blur',
|
|
1410
|
+
source: 'forgeax::shader::motion-blur',
|
|
1411
|
+
reads: ['scene-color', 'scene-temporal'] as const,
|
|
1412
|
+
params: {
|
|
1413
|
+
byteSize: MOTION_BLUR_PARAMS_BYTE_SIZE,
|
|
1414
|
+
defaultValue: new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE),
|
|
1415
|
+
},
|
|
1416
|
+
};
|
|
1417
|
+
const computeCapable =
|
|
1418
|
+
context.caps.compute === true &&
|
|
1419
|
+
context.caps.storageBuffer === true &&
|
|
1420
|
+
context.caps.storageTexture === true &&
|
|
1421
|
+
context.caps.rgba16floatRenderable === true &&
|
|
1422
|
+
outputFormat === 'rgba16float';
|
|
1423
|
+
if (computeCapable) {
|
|
1424
|
+
const width = positiveDimension(context.frame.width);
|
|
1425
|
+
const height = positiveDimension(context.frame.height);
|
|
1426
|
+
const tileWidth = Math.max(1, Math.ceil(width / 16));
|
|
1427
|
+
const tileHeight = Math.max(1, Math.ceil(height / 16));
|
|
1428
|
+
return ok({
|
|
1429
|
+
resources: [
|
|
1430
|
+
{
|
|
1431
|
+
kind: 'compute-program',
|
|
1432
|
+
name: 'motion-blur-compute',
|
|
1433
|
+
program: {
|
|
1434
|
+
wgsl: MOTION_BLUR_COMPUTE_WGSL,
|
|
1435
|
+
entryPoints: ['tile_summary', 'reconstruct'],
|
|
1436
|
+
bindings: [
|
|
1437
|
+
{
|
|
1438
|
+
entries: [
|
|
1439
|
+
{
|
|
1440
|
+
binding: 0,
|
|
1441
|
+
visibility: 4,
|
|
1442
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
binding: 1,
|
|
1446
|
+
visibility: 4,
|
|
1447
|
+
texture: { sampleType: 'float', viewDimension: '2d' },
|
|
1448
|
+
},
|
|
1449
|
+
{ binding: 2, visibility: 4, buffer: { type: 'storage' } },
|
|
1450
|
+
{
|
|
1451
|
+
binding: 3,
|
|
1452
|
+
visibility: 4,
|
|
1453
|
+
storageTexture: {
|
|
1454
|
+
access: 'write-only',
|
|
1455
|
+
format: 'rgba16float',
|
|
1456
|
+
viewDimension: '2d',
|
|
1457
|
+
},
|
|
1458
|
+
},
|
|
1459
|
+
{ binding: 4, visibility: 4, buffer: { type: 'uniform' } },
|
|
1460
|
+
],
|
|
1461
|
+
},
|
|
1462
|
+
],
|
|
1463
|
+
},
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
kind: 'buffer',
|
|
1467
|
+
name: 'motion-blur-tile-summary',
|
|
1468
|
+
size: tileWidth * tileHeight * 32,
|
|
1469
|
+
usage: ['storage'] as const,
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
kind: 'buffer',
|
|
1473
|
+
name: 'motion-blur-compute-params',
|
|
1474
|
+
size: MOTION_BLUR_PARAMS_BYTE_SIZE,
|
|
1475
|
+
usage: ['uniform'] as const,
|
|
1476
|
+
data: encodeMotionBlurComputeParams(params, temporal.frameDeltaSeconds, temporal.reset),
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
kind: 'prepared-gpu-resource',
|
|
1480
|
+
name: 'motion-blur-input',
|
|
1481
|
+
resource: { kind: 'texture-view' as const, value: undefined },
|
|
1482
|
+
logicalTarget: 'motion-input',
|
|
1483
|
+
},
|
|
1484
|
+
{
|
|
1485
|
+
kind: 'prepared-gpu-resource',
|
|
1486
|
+
name: 'motion-blur-temporal',
|
|
1487
|
+
resource: { kind: 'texture-view' as const, value: undefined },
|
|
1488
|
+
logicalTarget: sceneTemporal,
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
kind: 'prepared-gpu-resource',
|
|
1492
|
+
name: 'motion-blur-output',
|
|
1493
|
+
resource: { kind: 'texture-view' as const, value: undefined },
|
|
1494
|
+
logicalTarget: 'motion-output',
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
kind: 'compute-bindings',
|
|
1498
|
+
name: 'motion-blur-compute-bindings',
|
|
1499
|
+
program: 'motion-blur-compute',
|
|
1500
|
+
entries: [
|
|
1501
|
+
{ binding: 0, resource: 'motion-blur-input' },
|
|
1502
|
+
{ binding: 1, resource: 'motion-blur-temporal' },
|
|
1503
|
+
{ binding: 2, resource: 'motion-blur-tile-summary' },
|
|
1504
|
+
{ binding: 3, resource: 'motion-blur-output' },
|
|
1505
|
+
{ binding: 4, resource: 'motion-blur-compute-params' },
|
|
1506
|
+
],
|
|
1507
|
+
},
|
|
1508
|
+
],
|
|
1509
|
+
passes: [
|
|
1510
|
+
{
|
|
1511
|
+
kind: 'compute',
|
|
1512
|
+
name: 'motion-blur-tile-summary',
|
|
1513
|
+
program: 'motion-blur-compute',
|
|
1514
|
+
bindings: 'motion-blur-compute-bindings',
|
|
1515
|
+
dispatches: [
|
|
1516
|
+
{
|
|
1517
|
+
kind: 'direct',
|
|
1518
|
+
entryPoint: 'tile_summary',
|
|
1519
|
+
workgroups: [tileWidth, tileHeight, 1],
|
|
1520
|
+
},
|
|
1521
|
+
],
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
kind: 'compute',
|
|
1525
|
+
// Keep the public post-process identity stable while the compute
|
|
1526
|
+
// lane adds its private tile-summary producer before it.
|
|
1527
|
+
name: 'motion-blur',
|
|
1528
|
+
program: 'motion-blur-compute',
|
|
1529
|
+
bindings: 'motion-blur-compute-bindings',
|
|
1530
|
+
dispatches: [
|
|
1531
|
+
{
|
|
1532
|
+
kind: 'direct',
|
|
1533
|
+
entryPoint: 'reconstruct',
|
|
1534
|
+
workgroups: [
|
|
1535
|
+
Math.max(1, Math.ceil(width / 8)),
|
|
1536
|
+
Math.max(1, Math.ceil(height / 8)),
|
|
1537
|
+
1,
|
|
1538
|
+
],
|
|
1539
|
+
},
|
|
1540
|
+
],
|
|
1541
|
+
},
|
|
1542
|
+
],
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
46
1545
|
return ok({
|
|
47
1546
|
resources: [
|
|
48
|
-
|
|
49
|
-
kind: 'fullscreen-program',
|
|
50
|
-
name: 'motion-blur',
|
|
51
|
-
source: 'forgeax::shader::motion-blur',
|
|
52
|
-
reads: ['scene-color', 'scene-temporal'],
|
|
53
|
-
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
54
|
-
},
|
|
1547
|
+
fullscreen,
|
|
55
1548
|
{
|
|
56
1549
|
kind: 'graphics-program',
|
|
57
1550
|
name: 'motion-blur-pipeline',
|
|
@@ -71,7 +1564,7 @@ export function planMotionBlur(
|
|
|
71
1564
|
fullscreen: true,
|
|
72
1565
|
shader: MOTION_BLUR_FEATURE_IDENTITY,
|
|
73
1566
|
input: 'motion-input',
|
|
74
|
-
temporal,
|
|
1567
|
+
temporal: sceneTemporal,
|
|
75
1568
|
},
|
|
76
1569
|
logicalTargets: { input: 'motion-input' },
|
|
77
1570
|
},
|
|
@@ -81,7 +1574,7 @@ export function planMotionBlur(
|
|
|
81
1574
|
kind: 'raster',
|
|
82
1575
|
name: 'motion-blur',
|
|
83
1576
|
colorAttachments: [{ target: 'motion-output', loadOp: 'clear', storeOp: 'store' }],
|
|
84
|
-
sampledTargets: ['motion-input',
|
|
1577
|
+
sampledTargets: ['motion-input', sceneTemporal],
|
|
85
1578
|
draws: [
|
|
86
1579
|
{
|
|
87
1580
|
program: 'motion-blur-pipeline',
|
|
@@ -100,20 +1593,31 @@ export function planMotionBlur(
|
|
|
100
1593
|
}
|
|
101
1594
|
|
|
102
1595
|
export function createMotionBlurFeature(
|
|
103
|
-
read: () =>
|
|
1596
|
+
read: () => MotionBlurFeatureInput | undefined,
|
|
104
1597
|
): RenderFeature<MotionBlurFeatureFrame> {
|
|
105
1598
|
return Object.freeze({
|
|
106
1599
|
identity: MOTION_BLUR_FEATURE_IDENTITY,
|
|
1600
|
+
// The program is an Engine-owned static WGSL source. Immediate creation
|
|
1601
|
+
// lets a newly admitted frame use the same graph/dispatch receipt instead
|
|
1602
|
+
// of silently falling back to the raster post effect while the validated
|
|
1603
|
+
// manifest adapter is still warming.
|
|
1604
|
+
shaderModuleMode: 'immediate' as const,
|
|
107
1605
|
extract: (_context: RenderFeatureExtractContext) => {
|
|
108
1606
|
const input = read();
|
|
109
|
-
const validated = validateMotionBlurParams(input);
|
|
1607
|
+
const validated = validateMotionBlurParams(input?.params);
|
|
110
1608
|
if (!validated.ok) return err(validated.error as unknown as RenderError);
|
|
111
|
-
const params = input === undefined ? undefined : validated.value;
|
|
112
|
-
return ok({
|
|
1609
|
+
const params = input?.params === undefined ? undefined : validated.value;
|
|
1610
|
+
return ok({
|
|
1611
|
+
params,
|
|
1612
|
+
demanded: motionBlurTemporalDemand(params),
|
|
1613
|
+
frameDeltaSeconds:
|
|
1614
|
+
input?.frameDeltaSeconds === undefined ? 1 / 60 : input.frameDeltaSeconds,
|
|
1615
|
+
reset: input?.reset === true,
|
|
1616
|
+
});
|
|
113
1617
|
},
|
|
114
1618
|
plan: (frame: MotionBlurFeatureFrame, context: RenderFeaturePlanContext) =>
|
|
115
1619
|
frame.params === undefined
|
|
116
1620
|
? ok({ resources: [], passes: [] })
|
|
117
|
-
: planMotionBlur(frame.params, context),
|
|
1621
|
+
: planMotionBlur(frame.params, context, frame),
|
|
118
1622
|
});
|
|
119
1623
|
}
|