@forgeax/engine-render 0.1.31 → 0.1.32
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 +68 -1
- package/dist/assembly/webgpu-ready.d.ts +2 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/{chunk-C3DDSXYK.mjs → chunk-4NNZQCL3.mjs} +110 -35
- package/dist/chunk-4NNZQCL3.mjs.map +1 -0
- package/dist/{chunk-JRBDO2KB.mjs → chunk-6TFBXLRJ.mjs} +19 -7
- package/dist/chunk-6TFBXLRJ.mjs.map +1 -0
- package/dist/{chunk-FUUO4UND.mjs → chunk-DRR7D3KX.mjs} +3 -3
- package/dist/chunk-DRR7D3KX.mjs.map +1 -0
- package/dist/{chunk-S5UE2T5P.mjs → chunk-GWCEMDVR.mjs} +380 -4
- package/dist/chunk-GWCEMDVR.mjs.map +1 -0
- package/dist/{chunk-MDQJPY3T.mjs → chunk-JCIRYQ4V.mjs} +3 -3
- package/dist/{chunk-MDQJPY3T.mjs.map → chunk-JCIRYQ4V.mjs.map} +1 -1
- package/dist/{chunk-PSEQ6QPH.mjs → chunk-JRIPCMYC.mjs} +17 -10
- package/dist/chunk-JRIPCMYC.mjs.map +1 -0
- package/dist/{chunk-7MQ3XBSP.mjs → chunk-LULCA3OF.mjs} +4 -4
- package/dist/{chunk-7MQ3XBSP.mjs.map → chunk-LULCA3OF.mjs.map} +1 -1
- package/dist/{chunk-NO4WZCAH.mjs → chunk-S665NJNF.mjs} +3 -3
- package/dist/{chunk-NO4WZCAH.mjs.map → chunk-S665NJNF.mjs.map} +1 -1
- package/dist/components/depth-of-field.d.ts +28 -0
- package/dist/components/depth-of-field.d.ts.map +1 -0
- package/dist/components/directional-light.d.ts +2 -1
- package/dist/components/directional-light.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +898 -328
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts +19 -0
- package/dist/features/depth-of-field/depth-of-field-assembly.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts +43 -0
- package/dist/features/depth-of-field/depth-of-field-feature.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts +34 -0
- package/dist/features/depth-of-field/depth-of-field-inspection.d.ts.map +1 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts +83 -0
- package/dist/features/depth-of-field/depth-of-field-params.d.ts.map +1 -0
- package/dist/features/types.d.ts +19 -1
- package/dist/features/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +35 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances.d.ts +7 -0
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +6 -6
- package/dist/mesh-material-bindings.d.ts +3 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +21 -1
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +7 -0
- 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 +8 -1
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +2 -0
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +13 -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 +18 -4
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +2 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/package.json +21 -21
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +12 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +27 -1
- package/src/__tests__/instances-store.unit.test.ts +15 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +28 -0
- package/src/assembly/webgpu-ready.ts +5 -5
- package/src/assembly/webgpu-renderer.ts +14 -14
- package/src/components/depth-of-field.ts +45 -0
- package/src/components/directional-light.ts +2 -1
- package/src/components/index.ts +1 -0
- package/src/extract/camera.ts +22 -2
- package/src/features/depth-of-field/__tests__/depth-of-field-params.unit.test.ts +943 -0
- package/src/features/depth-of-field/depth-of-field-assembly.ts +100 -0
- package/src/features/depth-of-field/depth-of-field-feature.ts +295 -0
- package/src/features/depth-of-field/depth-of-field-inspection.ts +237 -0
- package/src/features/depth-of-field/depth-of-field-params.ts +556 -0
- package/src/features/host.ts +1 -1
- package/src/features/types.ts +18 -1
- package/src/index.ts +32 -0
- package/src/inspection-types.ts +38 -0
- package/src/instances.ts +19 -5
- package/src/mesh-material-bindings.ts +4 -0
- package/src/pipeline/standard-output/graph.ts +3 -0
- package/src/pipeline/standard-output/types.ts +4 -1
- package/src/pipeline/standard-post.ts +21 -2
- package/src/plugin.ts +2 -0
- package/src/record/frame-snapshot.ts +26 -1
- package/src/record/frame.ts +39 -6
- package/src/record/main-pass.ts +1 -5
- package/src/record/typed-frame-graph.ts +106 -6
- package/src/recovery/render-system-candidate.ts +6 -0
- package/src/render-contract.ts +11 -0
- package/src/render-graph-primitives.ts +8 -1
- package/src/render-pipeline.ts +17 -1
- package/src/render-system-extract-tail.ts +87 -13
- package/src/render-system-extract.ts +103 -25
- package/src/render-system.ts +78 -0
- package/src/typed-render-graph-primitives.ts +3 -0
- package/dist/chunk-C3DDSXYK.mjs.map +0 -1
- package/dist/chunk-FUUO4UND.mjs.map +0 -1
- package/dist/chunk-JRBDO2KB.mjs.map +0 -1
- package/dist/chunk-PSEQ6QPH.mjs.map +0 -1
- package/dist/chunk-S5UE2T5P.mjs.map +0 -1
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import {
|
|
3
|
+
type DepthOfFieldData,
|
|
4
|
+
type DepthOfFieldQuality,
|
|
5
|
+
DepthOfFieldQualityValue,
|
|
6
|
+
type DepthOfFieldSide,
|
|
7
|
+
DepthOfFieldSideValue,
|
|
8
|
+
depthOfFieldQualityFromF32,
|
|
9
|
+
depthOfFieldSideFromF32,
|
|
10
|
+
} from '../../components/depth-of-field';
|
|
11
|
+
|
|
12
|
+
export const DEPTH_OF_FIELD_PARAMS_BYTE_SIZE = 64;
|
|
13
|
+
|
|
14
|
+
// Camera component values cross the UBO boundary as f32. Keep bounds in that
|
|
15
|
+
// same representation so the authored decimal boundary 0.7 remains valid
|
|
16
|
+
// after quantization.
|
|
17
|
+
const MIN_F_STOP = Math.fround(0.7);
|
|
18
|
+
const MAX_F_STOP = Math.fround(32);
|
|
19
|
+
const MAX_LINEAR_DEPTH_RAW = Math.fround(1 - 2 ** -24);
|
|
20
|
+
|
|
21
|
+
export interface DepthOfFieldParams {
|
|
22
|
+
readonly focusDistance: number;
|
|
23
|
+
readonly fStop: number;
|
|
24
|
+
readonly sensorHeight: number;
|
|
25
|
+
readonly maxRadiusPixels: number;
|
|
26
|
+
readonly quality: DepthOfFieldQuality;
|
|
27
|
+
readonly blurSide: DepthOfFieldSide;
|
|
28
|
+
readonly focalLength: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_DEPTH_OF_FIELD_PARAMS: DepthOfFieldParams = Object.freeze({
|
|
32
|
+
focusDistance: 8,
|
|
33
|
+
fStop: 2.8,
|
|
34
|
+
sensorHeight: 0.024,
|
|
35
|
+
maxRadiusPixels: 16,
|
|
36
|
+
quality: 'medium',
|
|
37
|
+
blurSide: 'both',
|
|
38
|
+
focalLength: 0,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function quantizeF32(value: number): number {
|
|
42
|
+
return Math.fround(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DepthOfFieldErrorCode =
|
|
46
|
+
| 'depth-of-field-invalid-params'
|
|
47
|
+
| 'depth-of-field-orthographic-unsupported';
|
|
48
|
+
|
|
49
|
+
export interface DepthOfFieldInvalidParamsDetail {
|
|
50
|
+
readonly field: string;
|
|
51
|
+
readonly value: unknown;
|
|
52
|
+
readonly expected: string;
|
|
53
|
+
readonly bound?: readonly [number, number] | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DepthOfFieldOrthographicDetail {
|
|
57
|
+
readonly projection: 'orthographic';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type DepthOfFieldErrorDetail =
|
|
61
|
+
| DepthOfFieldInvalidParamsDetail
|
|
62
|
+
| DepthOfFieldOrthographicDetail;
|
|
63
|
+
|
|
64
|
+
/** Detached validation facts carried from extract to renderer inspection. */
|
|
65
|
+
export interface DepthOfFieldRequestFailure {
|
|
66
|
+
readonly code: DepthOfFieldErrorCode;
|
|
67
|
+
readonly detail: DepthOfFieldErrorDetail;
|
|
68
|
+
readonly expected: string;
|
|
69
|
+
readonly hint: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class DepthOfFieldValidationError extends Error {
|
|
73
|
+
readonly code: DepthOfFieldErrorCode;
|
|
74
|
+
readonly expected: string;
|
|
75
|
+
readonly hint: string;
|
|
76
|
+
readonly detail: DepthOfFieldErrorDetail;
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
code: DepthOfFieldErrorCode,
|
|
80
|
+
detail: DepthOfFieldErrorDetail,
|
|
81
|
+
expected: string,
|
|
82
|
+
hint: string,
|
|
83
|
+
) {
|
|
84
|
+
super(`${code}: ${expected}`);
|
|
85
|
+
this.name = 'DepthOfFieldValidationError';
|
|
86
|
+
this.code = code;
|
|
87
|
+
this.expected = expected;
|
|
88
|
+
this.hint = hint;
|
|
89
|
+
this.detail = detail;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function depthOfFieldRequestFailure(
|
|
94
|
+
error: DepthOfFieldValidationError,
|
|
95
|
+
): DepthOfFieldRequestFailure {
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
code: error.code,
|
|
98
|
+
detail: error.detail,
|
|
99
|
+
expected: error.expected,
|
|
100
|
+
hint: error.hint,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the params used by a frame when authoring validation failed.
|
|
106
|
+
* Invalid/unsupported requests stay visible as errors, but an already
|
|
107
|
+
* accepted DoF graph must keep receiving its last-known-good UBO until the
|
|
108
|
+
* author submits a valid replacement or explicitly removes the component.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveDepthOfFieldFrameParams(
|
|
111
|
+
requested: DepthOfFieldParams | undefined,
|
|
112
|
+
requestFailure: DepthOfFieldRequestFailure | undefined,
|
|
113
|
+
accepted: DepthOfFieldParams | undefined,
|
|
114
|
+
): DepthOfFieldParams | undefined {
|
|
115
|
+
if (requested !== undefined) return requested;
|
|
116
|
+
if (
|
|
117
|
+
requestFailure === undefined ||
|
|
118
|
+
requestFailure.code === 'depth-of-field-orthographic-unsupported'
|
|
119
|
+
) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return accepted;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface DepthOfFieldCameraInput {
|
|
126
|
+
readonly projection: 'perspective' | 'orthographic';
|
|
127
|
+
readonly fov: number;
|
|
128
|
+
readonly near: number;
|
|
129
|
+
readonly far: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function invalid(
|
|
133
|
+
field: string,
|
|
134
|
+
value: unknown,
|
|
135
|
+
expected: string,
|
|
136
|
+
bound?: readonly [number, number],
|
|
137
|
+
): DepthOfFieldValidationError {
|
|
138
|
+
return new DepthOfFieldValidationError(
|
|
139
|
+
'depth-of-field-invalid-params',
|
|
140
|
+
{ field, value, expected, ...(bound === undefined ? {} : { bound }) },
|
|
141
|
+
expected,
|
|
142
|
+
`set ${field} to ${expected}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function focalLength(sensorHeight: number, fov: number): number {
|
|
147
|
+
return sensorHeight / (2 * Math.tan(fov / 2));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function opticalCocScale(
|
|
151
|
+
params: Pick<DepthOfFieldParams, 'focalLength' | 'sensorHeight' | 'fStop' | 'focusDistance'>,
|
|
152
|
+
): number {
|
|
153
|
+
// This is evaluated on the CPU once per frame. Keep the ratios separate so
|
|
154
|
+
// the final f32 coefficient does not inherit an avoidable focalLength^2
|
|
155
|
+
// overflow.
|
|
156
|
+
return (
|
|
157
|
+
(params.focalLength / params.sensorHeight / (2 * params.fStop)) *
|
|
158
|
+
(params.focalLength / (params.focusDistance - params.focalLength))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function frameCocCoefficient(
|
|
163
|
+
params: Pick<DepthOfFieldParams, 'focalLength' | 'sensorHeight' | 'fStop' | 'focusDistance'>,
|
|
164
|
+
outputHeight: number,
|
|
165
|
+
): number {
|
|
166
|
+
return quantizeF32(quantizeF32(outputHeight) * opticalCocScale(params));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface DepthOfFieldFrameValues {
|
|
170
|
+
readonly focusDistance: number;
|
|
171
|
+
readonly fStop: number;
|
|
172
|
+
readonly sensorHeight: number;
|
|
173
|
+
readonly focalLength: number;
|
|
174
|
+
readonly maxRadiusPixels: number;
|
|
175
|
+
readonly outputHeight: number;
|
|
176
|
+
readonly near: number;
|
|
177
|
+
readonly far: number;
|
|
178
|
+
readonly cocCoefficient: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveDepthOfFieldFrameValues(
|
|
182
|
+
params: DepthOfFieldParams,
|
|
183
|
+
input: {
|
|
184
|
+
readonly outputHeight: number;
|
|
185
|
+
readonly near: number;
|
|
186
|
+
readonly far: number;
|
|
187
|
+
readonly useTemporalDepth: boolean;
|
|
188
|
+
},
|
|
189
|
+
): Result<DepthOfFieldFrameValues, DepthOfFieldValidationError> {
|
|
190
|
+
const focusDistance = quantizeF32(params.focusDistance);
|
|
191
|
+
const fStop = quantizeF32(params.fStop);
|
|
192
|
+
const sensorHeight = quantizeF32(params.sensorHeight);
|
|
193
|
+
const focal = quantizeF32(params.focalLength);
|
|
194
|
+
const maxRadiusPixels = quantizeF32(params.maxRadiusPixels);
|
|
195
|
+
const outputHeight = quantizeF32(input.outputHeight);
|
|
196
|
+
const near = quantizeF32(input.near);
|
|
197
|
+
const far = quantizeF32(input.far);
|
|
198
|
+
const finitePositiveFields: readonly [string, number, number][] = [
|
|
199
|
+
['focusDistance', focusDistance, params.focusDistance],
|
|
200
|
+
['sensorHeight', sensorHeight, params.sensorHeight],
|
|
201
|
+
['focalLength', focal, params.focalLength],
|
|
202
|
+
['near', near, input.near],
|
|
203
|
+
['far', far, input.far],
|
|
204
|
+
];
|
|
205
|
+
for (const [field, value, raw] of finitePositiveFields) {
|
|
206
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
207
|
+
return err(invalid(field, raw, 'a finite positive f32 value'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!Number.isFinite(fStop) || fStop < MIN_F_STOP || fStop > MAX_F_STOP) {
|
|
211
|
+
return err(
|
|
212
|
+
invalid('fStop', params.fStop, 'a finite f32 value in [0.7, 32]', [MIN_F_STOP, MAX_F_STOP]),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!Number.isFinite(maxRadiusPixels) || maxRadiusPixels < 0 || maxRadiusPixels > 32) {
|
|
216
|
+
return err(
|
|
217
|
+
invalid('maxRadiusPixels', params.maxRadiusPixels, 'a finite f32 value in [0, 32]', [0, 32]),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!Number.isFinite(outputHeight) || outputHeight <= 0) {
|
|
221
|
+
return err(invalid('outputHeight', input.outputHeight, 'a finite positive f32 value'));
|
|
222
|
+
}
|
|
223
|
+
if (far <= near) {
|
|
224
|
+
return err(invalid('near/far', [input.near, input.far], 'finite f32 near > 0 and far > near'));
|
|
225
|
+
}
|
|
226
|
+
if (focusDistance <= focal) {
|
|
227
|
+
return err(invalid('focusDistance', params.focusDistance, 'greater than focalLength in f32'));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// linearDepth() uses this ratio and denominator order in both WGSL
|
|
231
|
+
// variants. Reject a ratio that disappears at f32 precision, then check the
|
|
232
|
+
// near, mid and largest-valid raw-depth samples for finite denominators and
|
|
233
|
+
// reconstructed distances. The denominator is strictly positive for valid
|
|
234
|
+
// raw depth, so no world-unit epsilon belongs in the shader.
|
|
235
|
+
const depthRatio = quantizeF32(near / far);
|
|
236
|
+
if (!Number.isFinite(depthRatio) || depthRatio <= 0) {
|
|
237
|
+
return err(invalid('near/far ratio', depthRatio, 'a finite positive f32 value'));
|
|
238
|
+
}
|
|
239
|
+
for (const raw of [quantizeF32(2 ** -149), 0.5, MAX_LINEAR_DEPTH_RAW]) {
|
|
240
|
+
const oneMinusRaw = quantizeF32(1 - raw);
|
|
241
|
+
const weightedRatio = quantizeF32(raw * depthRatio);
|
|
242
|
+
const denominator = quantizeF32(oneMinusRaw + weightedRatio);
|
|
243
|
+
const distance = quantizeF32(near / denominator);
|
|
244
|
+
if (
|
|
245
|
+
!Number.isFinite(oneMinusRaw) ||
|
|
246
|
+
oneMinusRaw <= 0 ||
|
|
247
|
+
!Number.isFinite(weightedRatio) ||
|
|
248
|
+
!Number.isFinite(denominator) ||
|
|
249
|
+
denominator <= 0 ||
|
|
250
|
+
!Number.isFinite(distance) ||
|
|
251
|
+
distance <= 0
|
|
252
|
+
) {
|
|
253
|
+
return err(invalid('linearDepth', distance, 'finite positive f32 reconstruction'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const cocCoefficient = frameCocCoefficient(
|
|
258
|
+
{
|
|
259
|
+
focusDistance,
|
|
260
|
+
fStop,
|
|
261
|
+
sensorHeight,
|
|
262
|
+
focalLength: focal,
|
|
263
|
+
},
|
|
264
|
+
outputHeight,
|
|
265
|
+
);
|
|
266
|
+
if (!Number.isFinite(cocCoefficient) || cocCoefficient <= 0) {
|
|
267
|
+
return err(
|
|
268
|
+
invalid('cocCoefficient', cocCoefficient, 'a finite positive value representable as f32'),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
// The shader evaluates C * (1 - focus / depth). Check each f32 operation at
|
|
272
|
+
// the projection endpoints before the UBO is written.
|
|
273
|
+
for (const [field, depth] of [
|
|
274
|
+
['near', near],
|
|
275
|
+
['far', far],
|
|
276
|
+
] as const) {
|
|
277
|
+
const focusOverDepth = quantizeF32(focusDistance / depth);
|
|
278
|
+
const depthFactor = quantizeF32(1 - focusOverDepth);
|
|
279
|
+
const radius = quantizeF32(cocCoefficient * depthFactor);
|
|
280
|
+
if (
|
|
281
|
+
!Number.isFinite(focusOverDepth) ||
|
|
282
|
+
!Number.isFinite(depthFactor) ||
|
|
283
|
+
!Number.isFinite(radius)
|
|
284
|
+
) {
|
|
285
|
+
return err(invalid(`coc.${field}`, radius, 'a finite value representable as f32'));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return ok({
|
|
290
|
+
focusDistance,
|
|
291
|
+
fStop,
|
|
292
|
+
sensorHeight,
|
|
293
|
+
focalLength: focal,
|
|
294
|
+
maxRadiusPixels,
|
|
295
|
+
outputHeight,
|
|
296
|
+
near,
|
|
297
|
+
far,
|
|
298
|
+
cocCoefficient,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Validate the values that cross the f32 camera UBO boundary for one frame.
|
|
304
|
+
* Camera and DoF schemas already use f32 columns, but direct callers and
|
|
305
|
+
* synthetic frame tests can still supply wider JS numbers. Quantize first so
|
|
306
|
+
* admission and the WGSL consumer make the same decision.
|
|
307
|
+
*/
|
|
308
|
+
export function validateDepthOfFieldFrameParams(
|
|
309
|
+
params: DepthOfFieldParams,
|
|
310
|
+
input: {
|
|
311
|
+
readonly outputHeight: number;
|
|
312
|
+
readonly near: number;
|
|
313
|
+
readonly far: number;
|
|
314
|
+
readonly useTemporalDepth: boolean;
|
|
315
|
+
},
|
|
316
|
+
): Result<void, DepthOfFieldValidationError> {
|
|
317
|
+
const resolved = resolveDepthOfFieldFrameValues(params, input);
|
|
318
|
+
return resolved.ok ? ok(undefined) : resolved;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Validate and derive one camera DoF declaration at the frame admission seam.
|
|
323
|
+
* The returned focal length is a derived fact; callers never author it.
|
|
324
|
+
*/
|
|
325
|
+
export function validateDepthOfFieldParams(
|
|
326
|
+
input: Partial<DepthOfFieldData> | undefined,
|
|
327
|
+
camera?: DepthOfFieldCameraInput,
|
|
328
|
+
): Result<DepthOfFieldParams, DepthOfFieldValidationError> {
|
|
329
|
+
if (camera?.projection === 'orthographic') {
|
|
330
|
+
return err(
|
|
331
|
+
new DepthOfFieldValidationError(
|
|
332
|
+
'depth-of-field-orthographic-unsupported',
|
|
333
|
+
{ projection: 'orthographic' },
|
|
334
|
+
'DepthOfField requires a perspective Camera',
|
|
335
|
+
'use a perspective Camera or remove the DepthOfField component',
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const value = input ?? {};
|
|
340
|
+
const focusDistance = quantizeF32(
|
|
341
|
+
value.focusDistance ?? DEFAULT_DEPTH_OF_FIELD_PARAMS.focusDistance,
|
|
342
|
+
);
|
|
343
|
+
const fStop = quantizeF32(value.fStop ?? DEFAULT_DEPTH_OF_FIELD_PARAMS.fStop);
|
|
344
|
+
const sensorHeight = quantizeF32(
|
|
345
|
+
value.sensorHeight ?? DEFAULT_DEPTH_OF_FIELD_PARAMS.sensorHeight,
|
|
346
|
+
);
|
|
347
|
+
const maxRadiusPixels = quantizeF32(
|
|
348
|
+
value.maxRadiusPixels ?? DEFAULT_DEPTH_OF_FIELD_PARAMS.maxRadiusPixels,
|
|
349
|
+
);
|
|
350
|
+
const qualityValue = quantizeF32(value.quality ?? DepthOfFieldQualityValue.medium);
|
|
351
|
+
const blurSideValue = quantizeF32(value.blurSide ?? DepthOfFieldSideValue.both);
|
|
352
|
+
const quality = depthOfFieldQualityFromF32(qualityValue);
|
|
353
|
+
const blurSide = depthOfFieldSideFromF32(blurSideValue);
|
|
354
|
+
if (!Number.isFinite(focusDistance) || focusDistance <= 0) {
|
|
355
|
+
return err(
|
|
356
|
+
invalid('focusDistance', value.focusDistance ?? focusDistance, 'a finite positive f32 value'),
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (!Number.isFinite(fStop) || fStop < MIN_F_STOP || fStop > MAX_F_STOP) {
|
|
360
|
+
return err(
|
|
361
|
+
invalid('fStop', value.fStop ?? fStop, 'a finite f32 value in [0.7, 32]', [
|
|
362
|
+
MIN_F_STOP,
|
|
363
|
+
MAX_F_STOP,
|
|
364
|
+
]),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
if (!Number.isFinite(sensorHeight) || sensorHeight <= 0) {
|
|
368
|
+
return err(
|
|
369
|
+
invalid('sensorHeight', value.sensorHeight ?? sensorHeight, 'a finite positive f32 value'),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (!Number.isFinite(maxRadiusPixels) || maxRadiusPixels < 0 || maxRadiusPixels > 32) {
|
|
373
|
+
return err(
|
|
374
|
+
invalid(
|
|
375
|
+
'maxRadiusPixels',
|
|
376
|
+
value.maxRadiusPixels ?? maxRadiusPixels,
|
|
377
|
+
'a finite f32 value in [0, 32]',
|
|
378
|
+
[0, 32],
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (quality === undefined) {
|
|
383
|
+
return err(invalid('quality', qualityValue, 'DepthOfFieldQualityValue.low, medium, or high'));
|
|
384
|
+
}
|
|
385
|
+
if (blurSide === undefined) {
|
|
386
|
+
return err(invalid('blurSide', blurSideValue, 'DepthOfFieldSideValue.both, near, or far'));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let focal = DEFAULT_DEPTH_OF_FIELD_PARAMS.focalLength;
|
|
390
|
+
if (camera !== undefined) {
|
|
391
|
+
const fov = quantizeF32(camera.fov);
|
|
392
|
+
const near = quantizeF32(camera.near);
|
|
393
|
+
const far = quantizeF32(camera.far);
|
|
394
|
+
if (!Number.isFinite(fov) || fov <= 0 || fov >= Math.PI) {
|
|
395
|
+
return err(invalid('fov', camera.fov, 'a finite f32 value in (0, PI)'));
|
|
396
|
+
}
|
|
397
|
+
if (!Number.isFinite(near) || !Number.isFinite(far) || near <= 0 || far <= near) {
|
|
398
|
+
return err(
|
|
399
|
+
invalid('near/far', [camera.near, camera.far], 'finite f32 near > 0 and far > near'),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
focal = quantizeF32(focalLength(sensorHeight, fov));
|
|
403
|
+
if (!Number.isFinite(focal) || focal <= 0) {
|
|
404
|
+
return err(invalid('focalLength', focal, 'a finite positive f32 value'));
|
|
405
|
+
}
|
|
406
|
+
if (focusDistance < near || focusDistance > far) {
|
|
407
|
+
return err(
|
|
408
|
+
invalid('focusDistance', focusDistance, `a value in [${near}, ${far}]`, [near, far]),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (focusDistance <= focal) {
|
|
412
|
+
return err(
|
|
413
|
+
invalid('focusDistance', focusDistance, `greater than focalLength in f32 (${focal})`),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return ok(
|
|
418
|
+
Object.freeze({
|
|
419
|
+
focusDistance,
|
|
420
|
+
fStop,
|
|
421
|
+
sensorHeight,
|
|
422
|
+
maxRadiusPixels,
|
|
423
|
+
quality,
|
|
424
|
+
blurSide,
|
|
425
|
+
focalLength: focal,
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function resolveDepthOfFieldParams(
|
|
431
|
+
input: Partial<DepthOfFieldData> | undefined,
|
|
432
|
+
camera?: DepthOfFieldCameraInput,
|
|
433
|
+
): Result<DepthOfFieldParams | undefined, DepthOfFieldValidationError> {
|
|
434
|
+
if (input === undefined) return ok(undefined);
|
|
435
|
+
return validateDepthOfFieldParams(input, camera);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function depthOfFieldTapCount(quality: DepthOfFieldQuality): 16 | 32 | 64 {
|
|
439
|
+
switch (quality) {
|
|
440
|
+
case 'low':
|
|
441
|
+
return 16;
|
|
442
|
+
case 'medium':
|
|
443
|
+
return 32;
|
|
444
|
+
case 'high':
|
|
445
|
+
return 64;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function depthOfFieldSideCode(side: DepthOfFieldSide): 0 | 1 | 2 {
|
|
450
|
+
switch (side) {
|
|
451
|
+
case 'both':
|
|
452
|
+
return DepthOfFieldSideValue.both;
|
|
453
|
+
case 'near':
|
|
454
|
+
return DepthOfFieldSideValue.near;
|
|
455
|
+
case 'far':
|
|
456
|
+
return DepthOfFieldSideValue.far;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function depthOfFieldQualityCode(quality: DepthOfFieldQuality): 0 | 1 | 2 {
|
|
461
|
+
switch (quality) {
|
|
462
|
+
case 'low':
|
|
463
|
+
return DepthOfFieldQualityValue.low;
|
|
464
|
+
case 'medium':
|
|
465
|
+
return DepthOfFieldQualityValue.medium;
|
|
466
|
+
case 'high':
|
|
467
|
+
return DepthOfFieldQualityValue.high;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Signed thin-lens circle-of-confusion radius in final output pixels. */
|
|
472
|
+
export function signedDepthOfFieldCoC(
|
|
473
|
+
params: Pick<DepthOfFieldParams, 'focalLength' | 'sensorHeight' | 'fStop' | 'focusDistance'> &
|
|
474
|
+
Partial<Pick<DepthOfFieldParams, 'maxRadiusPixels'>>,
|
|
475
|
+
viewDepth: number,
|
|
476
|
+
outputHeight: number,
|
|
477
|
+
): number {
|
|
478
|
+
if (
|
|
479
|
+
!Number.isFinite(viewDepth) ||
|
|
480
|
+
viewDepth <= 0 ||
|
|
481
|
+
!Number.isFinite(outputHeight) ||
|
|
482
|
+
outputHeight <= 0 ||
|
|
483
|
+
!Number.isFinite(params.focalLength) ||
|
|
484
|
+
params.focalLength <= 0 ||
|
|
485
|
+
!Number.isFinite(params.sensorHeight) ||
|
|
486
|
+
params.sensorHeight <= 0 ||
|
|
487
|
+
!Number.isFinite(params.fStop) ||
|
|
488
|
+
params.fStop <= 0 ||
|
|
489
|
+
!Number.isFinite(params.focusDistance) ||
|
|
490
|
+
params.focusDistance <= params.focalLength
|
|
491
|
+
) {
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
const focusDistance = quantizeF32(params.focusDistance);
|
|
495
|
+
const depth = quantizeF32(viewDepth);
|
|
496
|
+
const focalLength = quantizeF32(params.focalLength);
|
|
497
|
+
const sensorHeight = quantizeF32(params.sensorHeight);
|
|
498
|
+
const fStop = quantizeF32(params.fStop);
|
|
499
|
+
const coefficient = frameCocCoefficient(
|
|
500
|
+
{ focusDistance, focalLength, sensorHeight, fStop },
|
|
501
|
+
outputHeight,
|
|
502
|
+
);
|
|
503
|
+
const depthFactor = quantizeF32(1 - quantizeF32(focusDistance / depth));
|
|
504
|
+
const radius = quantizeF32(coefficient * depthFactor);
|
|
505
|
+
const limit = Number.isFinite(params.maxRadiusPixels)
|
|
506
|
+
? quantizeF32(params.maxRadiusPixels as number)
|
|
507
|
+
: Number.POSITIVE_INFINITY;
|
|
508
|
+
return Math.max(-limit, Math.min(limit, Number.isFinite(radius) ? radius : 0));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Pack the one renderer-owned per-frame DoF UBO (4 x vec4, 64 bytes). */
|
|
512
|
+
export function packDepthOfFieldParams(
|
|
513
|
+
params: DepthOfFieldParams,
|
|
514
|
+
input: {
|
|
515
|
+
readonly outputHeight: number;
|
|
516
|
+
readonly near: number;
|
|
517
|
+
readonly far: number;
|
|
518
|
+
readonly useTemporalDepth: boolean;
|
|
519
|
+
},
|
|
520
|
+
): Uint8Array {
|
|
521
|
+
const frameValues = resolveDepthOfFieldFrameValues(params, input);
|
|
522
|
+
if (!frameValues.ok) throw frameValues.error;
|
|
523
|
+
const {
|
|
524
|
+
focusDistance,
|
|
525
|
+
fStop,
|
|
526
|
+
sensorHeight,
|
|
527
|
+
focalLength: focal,
|
|
528
|
+
outputHeight,
|
|
529
|
+
maxRadiusPixels,
|
|
530
|
+
near,
|
|
531
|
+
far,
|
|
532
|
+
cocCoefficient,
|
|
533
|
+
} = frameValues.value;
|
|
534
|
+
const payload = new Float32Array(DEPTH_OF_FIELD_PARAMS_BYTE_SIZE / 4);
|
|
535
|
+
// row3.x is the frame CoC coefficient; the remaining reserved lanes stay
|
|
536
|
+
// zero so the existing 64-byte binding contract remains unchanged.
|
|
537
|
+
payload.set([
|
|
538
|
+
focusDistance,
|
|
539
|
+
fStop,
|
|
540
|
+
sensorHeight,
|
|
541
|
+
focal,
|
|
542
|
+
outputHeight,
|
|
543
|
+
maxRadiusPixels,
|
|
544
|
+
depthOfFieldSideCode(params.blurSide),
|
|
545
|
+
depthOfFieldQualityCode(params.quality),
|
|
546
|
+
near,
|
|
547
|
+
far,
|
|
548
|
+
input.useTemporalDepth ? 1 : 0,
|
|
549
|
+
0, // row2.w reserved
|
|
550
|
+
cocCoefficient, // row3.x / WGSL reserved.x
|
|
551
|
+
0,
|
|
552
|
+
0,
|
|
553
|
+
0,
|
|
554
|
+
]);
|
|
555
|
+
return new Uint8Array(payload.buffer);
|
|
556
|
+
}
|
package/src/features/host.ts
CHANGED
|
@@ -1545,7 +1545,7 @@ export function runRenderFeatureFrame(
|
|
|
1545
1545
|
if (feature.onFrameSubmitted !== undefined) {
|
|
1546
1546
|
submissionSensitivePlans.add(plannedFrame);
|
|
1547
1547
|
submissionAcknowledgments.set(plannedFrame, () =>
|
|
1548
|
-
feature.onFrameSubmitted?.(extracted.value),
|
|
1548
|
+
feature.onFrameSubmitted?.(extracted.value, projected.value),
|
|
1549
1549
|
);
|
|
1550
1550
|
}
|
|
1551
1551
|
plans.push(plannedFrame);
|
package/src/features/types.ts
CHANGED
|
@@ -79,6 +79,23 @@ export interface RenderFeatureExtractContext {
|
|
|
79
79
|
readonly reportHiddenEntity?: (report: RenderFeatureHiddenEntityReport) => void;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Renderer-owned projection that reached graph admission for one feature.
|
|
84
|
+
* Producers may use this bounded receipt to distinguish declared plan work
|
|
85
|
+
* from passes omitted during prepared-resource resolution.
|
|
86
|
+
*/
|
|
87
|
+
export interface RenderFeatureSubmission {
|
|
88
|
+
readonly passes: readonly {
|
|
89
|
+
readonly shadowCaster?: true;
|
|
90
|
+
readonly graphics?: {
|
|
91
|
+
readonly draws: readonly { readonly kind: string }[];
|
|
92
|
+
};
|
|
93
|
+
readonly gpuCompute?: {
|
|
94
|
+
readonly dispatches: readonly unknown[];
|
|
95
|
+
};
|
|
96
|
+
}[];
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
/**
|
|
83
100
|
* Producer-owned extension seam for one render frame.
|
|
84
101
|
*
|
|
@@ -115,7 +132,7 @@ export interface RenderFeature<FrameData> {
|
|
|
115
132
|
* queue submission. The host invokes this callback at most once for the
|
|
116
133
|
* extracted frame and never during declarative planning.
|
|
117
134
|
*/
|
|
118
|
-
onFrameSubmitted?(data: FrameData): void;
|
|
135
|
+
onFrameSubmitted?(data: FrameData, submission?: RenderFeatureSubmission): void;
|
|
119
136
|
/**
|
|
120
137
|
* Discard frame-owned producer state when graph admission or submission is
|
|
121
138
|
* rejected. This is the symmetric recovery path for onFrameSubmitted.
|
package/src/index.ts
CHANGED
|
@@ -74,6 +74,16 @@ export {
|
|
|
74
74
|
cubeCameraUpdateIntentFromF32,
|
|
75
75
|
cubeCameraUpdateIntentToF32,
|
|
76
76
|
} from './components/cube-camera.js';
|
|
77
|
+
export {
|
|
78
|
+
DepthOfField,
|
|
79
|
+
type DepthOfFieldData,
|
|
80
|
+
type DepthOfFieldQuality,
|
|
81
|
+
DepthOfFieldQualityValue,
|
|
82
|
+
type DepthOfFieldSide,
|
|
83
|
+
DepthOfFieldSideValue,
|
|
84
|
+
depthOfFieldQualityFromF32,
|
|
85
|
+
depthOfFieldSideFromF32,
|
|
86
|
+
} from './components/depth-of-field.js';
|
|
77
87
|
export * from './components/directional-light.js';
|
|
78
88
|
export {
|
|
79
89
|
DynamicResolution,
|
|
@@ -196,6 +206,25 @@ export {
|
|
|
196
206
|
type VisibilityResolution,
|
|
197
207
|
type VisibilitySnapshot,
|
|
198
208
|
} from './extract/visibility.js';
|
|
209
|
+
export {
|
|
210
|
+
DEFAULT_DEPTH_OF_FIELD_PARAMS,
|
|
211
|
+
DEPTH_OF_FIELD_PARAMS_BYTE_SIZE,
|
|
212
|
+
type DepthOfFieldCameraInput,
|
|
213
|
+
type DepthOfFieldErrorCode,
|
|
214
|
+
type DepthOfFieldErrorDetail,
|
|
215
|
+
type DepthOfFieldParams,
|
|
216
|
+
type DepthOfFieldRequestFailure,
|
|
217
|
+
DepthOfFieldValidationError,
|
|
218
|
+
depthOfFieldQualityCode,
|
|
219
|
+
depthOfFieldRequestFailure,
|
|
220
|
+
depthOfFieldSideCode,
|
|
221
|
+
depthOfFieldTapCount,
|
|
222
|
+
packDepthOfFieldParams,
|
|
223
|
+
resolveDepthOfFieldParams,
|
|
224
|
+
signedDepthOfFieldCoC,
|
|
225
|
+
validateDepthOfFieldFrameParams,
|
|
226
|
+
validateDepthOfFieldParams,
|
|
227
|
+
} from './features/depth-of-field/depth-of-field-params.js';
|
|
199
228
|
export type {
|
|
200
229
|
RenderFeatureLogicalTarget,
|
|
201
230
|
RenderFeatureMaterialShaderBindingContract,
|
|
@@ -216,6 +245,7 @@ export type {
|
|
|
216
245
|
RenderFeatureHiddenEntityReport,
|
|
217
246
|
RenderFeatureShaderModuleMode,
|
|
218
247
|
RenderFeatureStatus,
|
|
248
|
+
RenderFeatureSubmission,
|
|
219
249
|
RenderFeatureWorldVisibilitySnapshot,
|
|
220
250
|
} from './features/types.js';
|
|
221
251
|
export {
|
|
@@ -232,6 +262,8 @@ export {
|
|
|
232
262
|
} from './ibl/kernel-cache.js';
|
|
233
263
|
export type {
|
|
234
264
|
BloomInspection,
|
|
265
|
+
DepthOfFieldInspection,
|
|
266
|
+
DepthOfFieldInspectionStatus,
|
|
235
267
|
GpuDrivenLane,
|
|
236
268
|
GpuDrivenLaneReason,
|
|
237
269
|
GpuDrivenLaneSummary,
|
package/src/inspection-types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
GpuDrivenPreparationErrorCode,
|
|
10
10
|
GpuDrivenPreparationErrorDetail,
|
|
11
11
|
} from './errors/gpu-driven';
|
|
12
|
+
import type { DepthOfFieldRequestFailure } from './features/depth-of-field/depth-of-field-params';
|
|
12
13
|
import type { PointsLinesInspection } from './points-lines/inspection';
|
|
13
14
|
import type { LodOcclusionInspection } from './scene/visibility/inspection';
|
|
14
15
|
import type { SsrAdmissionIdentity } from './ssr/identity';
|
|
@@ -556,6 +557,43 @@ export interface TemporalTargetInspection {
|
|
|
556
557
|
/** Detached, bounded Motion Blur facts; no graph, device, target, or history handle. */
|
|
557
558
|
export type MotionBlurInspectionStatus = 'off' | 'active' | 'reset' | 'invalid';
|
|
558
559
|
|
|
560
|
+
/** Bounded renderer-owned Depth of Field facts; no graph or GPU handles. */
|
|
561
|
+
export type DepthOfFieldInspectionStatus = 'off' | 'active' | 'reset' | 'invalid' | 'unsupported';
|
|
562
|
+
|
|
563
|
+
export interface DepthOfFieldInspection {
|
|
564
|
+
readonly enabled: boolean;
|
|
565
|
+
readonly status: DepthOfFieldInspectionStatus;
|
|
566
|
+
readonly requested: {
|
|
567
|
+
readonly focusDistance: number;
|
|
568
|
+
readonly fStop: number;
|
|
569
|
+
readonly sensorHeight: number;
|
|
570
|
+
readonly maxRadiusPixels: number;
|
|
571
|
+
readonly quality: 'low' | 'medium' | 'high';
|
|
572
|
+
readonly blurSide: 'both' | 'near' | 'far';
|
|
573
|
+
};
|
|
574
|
+
readonly effective?: DepthOfFieldInspection['requested'];
|
|
575
|
+
readonly focalLength: number;
|
|
576
|
+
readonly outputExtent: { readonly width: number; readonly height: number };
|
|
577
|
+
readonly workExtent: { readonly width: number; readonly height: number };
|
|
578
|
+
readonly tapCount: 0 | 16 | 32 | 64;
|
|
579
|
+
readonly passCount: number;
|
|
580
|
+
readonly textureBytes: number;
|
|
581
|
+
readonly transparentPolicy: 'opaque-depth-approximation';
|
|
582
|
+
readonly graphGeneration: number;
|
|
583
|
+
readonly deviceGeneration: number;
|
|
584
|
+
readonly lastKnownGood: boolean;
|
|
585
|
+
/** Structured authoring validation facts, when admission rejected the request. */
|
|
586
|
+
readonly error?: DepthOfFieldRequestFailure;
|
|
587
|
+
readonly fallbackReason?:
|
|
588
|
+
| 'zero-radius'
|
|
589
|
+
| 'orthographic-unsupported'
|
|
590
|
+
| 'invalid-params'
|
|
591
|
+
| 'shader-not-ready'
|
|
592
|
+
| 'candidate-failed'
|
|
593
|
+
| 'capability-unavailable'
|
|
594
|
+
| undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
559
597
|
export interface MotionBlurInspection {
|
|
560
598
|
readonly enabled: boolean;
|
|
561
599
|
readonly status: MotionBlurInspectionStatus;
|