@framefields/node-mask-math 0.0.0-stage → 2.0.3
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/.turbo/turbo-build.log +21 -0
- package/.turbo/turbo-test.log +12 -0
- package/CHANGELOG.md +25 -0
- package/LICENCE +202 -0
- package/dist/index-DaeM7Wai.d.mts +11 -0
- package/dist/index-DaeM7Wai.d.mts.map +1 -0
- package/dist/index.d.mts +152 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/renderer.d.mts +2 -0
- package/dist/renderer.mjs +3 -0
- package/dist/renderers-B_QRX8uG.mjs +784 -0
- package/dist/renderers-B_QRX8uG.mjs.map +1 -0
- package/package.json +45 -3
- package/src/index.ts +2 -0
- package/src/renderers/index.ts +8 -0
- package/src/renderers/webgpu-renderer.ts +832 -0
- package/src/shared/config.ts +133 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +24 -0
- package/tsconfig.json +12 -0
- package/tsdown.config.ts +13 -0
- package/README.md +0 -3
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/// <reference types="webgpu" />
|
|
2
|
+
import type { VirtualMediaData } from "@framefields/core";
|
|
3
|
+
import type { WebGPUNodeRenderer } from "@framefields/node-sdk";
|
|
4
|
+
import { signalRegistry } from "@framefields/webgpu-renderers";
|
|
5
|
+
import {
|
|
6
|
+
MAX_RADIUS,
|
|
7
|
+
type MaskChannel,
|
|
8
|
+
type MaskMathOp,
|
|
9
|
+
type MaskMathSignalData,
|
|
10
|
+
type MaskOperation,
|
|
11
|
+
type MaskOutputFormat,
|
|
12
|
+
} from "../shared/index.js";
|
|
13
|
+
|
|
14
|
+
const OPERATION_MAP: Record<MaskOperation, number> = {
|
|
15
|
+
Union: 0,
|
|
16
|
+
Intersect: 1,
|
|
17
|
+
Subtract: 2,
|
|
18
|
+
Difference: 3,
|
|
19
|
+
Invert: 4,
|
|
20
|
+
Dilate: 5,
|
|
21
|
+
Erode: 6,
|
|
22
|
+
Choke: 7,
|
|
23
|
+
Feather: 8,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const CHANNEL_MAP: Record<MaskChannel, number> = {
|
|
27
|
+
Alpha: 0,
|
|
28
|
+
Luminance: 1,
|
|
29
|
+
Red: 2,
|
|
30
|
+
Green: 3,
|
|
31
|
+
Blue: 4,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const OUTPUT_FORMAT_MAP: Record<MaskOutputFormat, number> = {
|
|
35
|
+
WhiteWithAlpha: 0,
|
|
36
|
+
GrayscaleRGB: 1,
|
|
37
|
+
AlphaOnly: 2,
|
|
38
|
+
PassthroughRGB: 3,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const WGSL_MASKMATH = `
|
|
42
|
+
struct MaskMathUniforms {
|
|
43
|
+
dirX : f32,
|
|
44
|
+
dirY : f32,
|
|
45
|
+
radius : f32,
|
|
46
|
+
threshold : f32,
|
|
47
|
+
|
|
48
|
+
clampMin : f32,
|
|
49
|
+
clampMax : f32,
|
|
50
|
+
operation : f32,
|
|
51
|
+
channelA : f32,
|
|
52
|
+
|
|
53
|
+
channelB : f32,
|
|
54
|
+
binarize : f32,
|
|
55
|
+
invertResult : f32,
|
|
56
|
+
outputFormat : f32,
|
|
57
|
+
|
|
58
|
+
hasMaskB : f32,
|
|
59
|
+
hasRadiusSig : f32,
|
|
60
|
+
hasThreshSig : f32,
|
|
61
|
+
hasClampMinSig : f32,
|
|
62
|
+
|
|
63
|
+
hasClampMaxSig : f32,
|
|
64
|
+
isSecondPass : f32,
|
|
65
|
+
pad0 : f32,
|
|
66
|
+
pad1 : f32,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
@group(0) @binding(0) var<uniform> u : MaskMathUniforms;
|
|
70
|
+
|
|
71
|
+
@group(1) @binding(0) var texA : texture_2d<f32>;
|
|
72
|
+
@group(1) @binding(1) var texB : texture_2d<f32>;
|
|
73
|
+
@group(1) @binding(2) var origTexA : texture_2d<f32>;
|
|
74
|
+
@group(1) @binding(3) var samp : sampler;
|
|
75
|
+
|
|
76
|
+
@group(2) @binding(0) var radiusSigTex : texture_2d<f32>;
|
|
77
|
+
@group(2) @binding(1) var threshSigTex : texture_2d<f32>;
|
|
78
|
+
@group(2) @binding(2) var clampMinSigTex : texture_2d<f32>;
|
|
79
|
+
@group(2) @binding(3) var clampMaxSigTex : texture_2d<f32>;
|
|
80
|
+
@group(2) @binding(4) var signalSamp : sampler;
|
|
81
|
+
|
|
82
|
+
struct VSOut {
|
|
83
|
+
@builtin(position) pos : vec4<f32>,
|
|
84
|
+
@location(0) uv : vec2<f32>,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> VSOut {
|
|
88
|
+
var pos = array<vec2<f32>, 4>(
|
|
89
|
+
vec2<f32>(-1.0, 1.0),
|
|
90
|
+
vec2<f32>(1.0, 1.0),
|
|
91
|
+
vec2<f32>(-1.0, -1.0),
|
|
92
|
+
vec2<f32>(1.0, -1.0)
|
|
93
|
+
);
|
|
94
|
+
var uv = array<vec2<f32>, 4>(
|
|
95
|
+
vec2<f32>(0.0, 0.0),
|
|
96
|
+
vec2<f32>(1.0, 0.0),
|
|
97
|
+
vec2<f32>(0.0, 1.0),
|
|
98
|
+
vec2<f32>(1.0, 1.0)
|
|
99
|
+
);
|
|
100
|
+
return VSOut(vec4<f32>(pos[vi], 0.0, 1.0), uv[vi]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn extractChannel(color : vec4<f32>, chCode : f32) -> f32 {
|
|
104
|
+
if (chCode < 0.5) {
|
|
105
|
+
// Alpha
|
|
106
|
+
return color.a;
|
|
107
|
+
} else if (chCode < 1.5) {
|
|
108
|
+
// Luminance
|
|
109
|
+
let luma = dot(color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
110
|
+
if (color.a > 1e-4) {
|
|
111
|
+
return min(1.0, luma / color.a);
|
|
112
|
+
}
|
|
113
|
+
return luma;
|
|
114
|
+
} else if (chCode < 2.5) {
|
|
115
|
+
// Red
|
|
116
|
+
if (color.a > 1e-4) {
|
|
117
|
+
return min(1.0, color.r / color.a);
|
|
118
|
+
}
|
|
119
|
+
return color.r;
|
|
120
|
+
} else if (chCode < 3.5) {
|
|
121
|
+
// Green
|
|
122
|
+
if (color.a > 1e-4) {
|
|
123
|
+
return min(1.0, color.g / color.a);
|
|
124
|
+
}
|
|
125
|
+
return color.g;
|
|
126
|
+
} else {
|
|
127
|
+
// Blue
|
|
128
|
+
if (color.a > 1e-4) {
|
|
129
|
+
return min(1.0, color.b / color.a);
|
|
130
|
+
}
|
|
131
|
+
return color.b;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
|
|
136
|
+
let dimensions = vec2<f32>(textureDimensions(texA));
|
|
137
|
+
let texelSize = 1.0 / max(dimensions, vec2<f32>(1.0, 1.0));
|
|
138
|
+
|
|
139
|
+
var radius = u.radius;
|
|
140
|
+
if (u.hasRadiusSig > 0.5) {
|
|
141
|
+
radius = clamp(textureSampleLevel(radiusSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r, 0.0, 200.0);
|
|
142
|
+
}
|
|
143
|
+
var threshold = u.threshold;
|
|
144
|
+
if (u.hasThreshSig > 0.5) {
|
|
145
|
+
threshold = clamp(textureSampleLevel(threshSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r, 0.0, 1.0);
|
|
146
|
+
}
|
|
147
|
+
var clampMin = u.clampMin;
|
|
148
|
+
if (u.hasClampMinSig > 0.5) {
|
|
149
|
+
clampMin = clamp(textureSampleLevel(clampMinSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r, 0.0, 1.0);
|
|
150
|
+
}
|
|
151
|
+
var clampMax = u.clampMax;
|
|
152
|
+
if (u.hasClampMaxSig > 0.5) {
|
|
153
|
+
clampMax = clamp(textureSampleLevel(clampMaxSigTex, signalSamp, vec2<f32>(0.5, 0.5), 0.0).r, 0.0, 1.0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let op = u.operation;
|
|
157
|
+
let direction = vec2<f32>(u.dirX, u.dirY);
|
|
158
|
+
|
|
159
|
+
var val = 0.0;
|
|
160
|
+
|
|
161
|
+
if (u.isSecondPass <= 0.5) {
|
|
162
|
+
// -------------------------------------------------------------
|
|
163
|
+
// PASS 1: Horizontal pass / Boolean operation
|
|
164
|
+
// -------------------------------------------------------------
|
|
165
|
+
let sampleA = textureSampleLevel(texA, samp, in.uv, 0.0);
|
|
166
|
+
let sampleB = textureSampleLevel(texB, samp, in.uv, 0.0);
|
|
167
|
+
|
|
168
|
+
let a = extractChannel(sampleA, u.channelA);
|
|
169
|
+
let b = select(0.0, extractChannel(sampleB, u.channelB), u.hasMaskB > 0.5);
|
|
170
|
+
|
|
171
|
+
if (op < 0.5) {
|
|
172
|
+
// Union (A + B)
|
|
173
|
+
val = select(a, clamp(a + b, 0.0, 1.0), u.hasMaskB > 0.5);
|
|
174
|
+
} else if (op < 1.5) {
|
|
175
|
+
// Intersect (min(A, B))
|
|
176
|
+
val = select(a, min(a, b), u.hasMaskB > 0.5);
|
|
177
|
+
} else if (op < 2.5) {
|
|
178
|
+
// Subtract (clamp(A - B, 0, 1))
|
|
179
|
+
val = select(a, clamp(a - b, 0.0, 1.0), u.hasMaskB > 0.5);
|
|
180
|
+
} else if (op < 3.5) {
|
|
181
|
+
// Difference (|A - B|)
|
|
182
|
+
val = select(a, abs(a - b), u.hasMaskB > 0.5);
|
|
183
|
+
} else if (op < 4.5) {
|
|
184
|
+
// Invert (1 - A)
|
|
185
|
+
val = 1.0 - a;
|
|
186
|
+
} else if (op < 5.5) {
|
|
187
|
+
// Dilate (Horizontal Max)
|
|
188
|
+
if (radius <= 0.0) {
|
|
189
|
+
val = a;
|
|
190
|
+
} else {
|
|
191
|
+
let stepScale = max(1.0, radius / 20.0);
|
|
192
|
+
let sampleRadius = min(i32(ceil(radius / stepScale)), 48);
|
|
193
|
+
var maxVal = a;
|
|
194
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
195
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
196
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
197
|
+
maxVal = max(maxVal, extractChannel(s, u.channelA));
|
|
198
|
+
}
|
|
199
|
+
val = maxVal;
|
|
200
|
+
}
|
|
201
|
+
} else if (op < 7.5) {
|
|
202
|
+
// Erode & Choke (Horizontal Min)
|
|
203
|
+
if (radius <= 0.0) {
|
|
204
|
+
val = a;
|
|
205
|
+
} else {
|
|
206
|
+
let stepScale = max(1.0, radius / 20.0);
|
|
207
|
+
let sampleRadius = min(i32(ceil(radius / stepScale)), 48);
|
|
208
|
+
var minVal = a;
|
|
209
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
210
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
211
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
212
|
+
minVal = min(minVal, extractChannel(s, u.channelA));
|
|
213
|
+
}
|
|
214
|
+
val = minVal;
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
// Feather (Horizontal Gaussian Blur)
|
|
218
|
+
if (radius <= 0.0) {
|
|
219
|
+
val = a;
|
|
220
|
+
} else {
|
|
221
|
+
let spatial_sigma = max(0.1, radius / 2.0);
|
|
222
|
+
let stepScale = max(1.0, spatial_sigma / 20.0);
|
|
223
|
+
let effectiveSigma = spatial_sigma / stepScale;
|
|
224
|
+
let sampleRadius = min(i32(ceil(3.0 * effectiveSigma)), 48);
|
|
225
|
+
var accum = 0.0;
|
|
226
|
+
var totalWeight = 0.0;
|
|
227
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
228
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
229
|
+
let x = f32(i) * stepScale;
|
|
230
|
+
let weight = exp(-0.5 * (x / spatial_sigma) * (x / spatial_sigma));
|
|
231
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
232
|
+
accum += extractChannel(s, u.channelA) * weight;
|
|
233
|
+
totalWeight += weight;
|
|
234
|
+
}
|
|
235
|
+
val = accum / max(totalWeight, 1e-5);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return vec4<f32>(val, val, val, val);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// -------------------------------------------------------------
|
|
243
|
+
// PASS 2: Vertical pass & Post-processing (clamp, binarize, format)
|
|
244
|
+
// -------------------------------------------------------------
|
|
245
|
+
let samplePass1 = textureSampleLevel(texA, samp, in.uv, 0.0);
|
|
246
|
+
var matte = samplePass1.r;
|
|
247
|
+
|
|
248
|
+
// 1. Spatial kernel processing (Dilate, Erode, Choke, Feather) when radius > 0
|
|
249
|
+
if (radius > 0.0) {
|
|
250
|
+
if (op >= 4.5 && op < 5.5) {
|
|
251
|
+
// Dilate (Vertical Max)
|
|
252
|
+
let stepScale = max(1.0, radius / 20.0);
|
|
253
|
+
let sampleRadius = min(i32(ceil(radius / stepScale)), 48);
|
|
254
|
+
var maxVal = matte;
|
|
255
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
256
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
257
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
258
|
+
maxVal = max(maxVal, s.r);
|
|
259
|
+
}
|
|
260
|
+
matte = maxVal;
|
|
261
|
+
} else if (op >= 5.5 && op < 7.5) {
|
|
262
|
+
// Erode & Choke (Vertical Min)
|
|
263
|
+
let stepScale = max(1.0, radius / 20.0);
|
|
264
|
+
let sampleRadius = min(i32(ceil(radius / stepScale)), 48);
|
|
265
|
+
var minVal = matte;
|
|
266
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
267
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
268
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
269
|
+
minVal = min(minVal, s.r);
|
|
270
|
+
}
|
|
271
|
+
matte = minVal;
|
|
272
|
+
} else if (op >= 7.5) {
|
|
273
|
+
// Feather (Vertical Gaussian Blur)
|
|
274
|
+
let spatial_sigma = max(0.1, radius / 2.0);
|
|
275
|
+
let stepScale = max(1.0, spatial_sigma / 20.0);
|
|
276
|
+
let effectiveSigma = spatial_sigma / stepScale;
|
|
277
|
+
let sampleRadius = min(i32(ceil(3.0 * effectiveSigma)), 48);
|
|
278
|
+
var accum = 0.0;
|
|
279
|
+
var totalWeight = 0.0;
|
|
280
|
+
for (var i = -sampleRadius; i <= sampleRadius; i++) {
|
|
281
|
+
let offset = vec2<f32>(f32(i) * stepScale) * direction * texelSize;
|
|
282
|
+
let x = f32(i) * stepScale;
|
|
283
|
+
let weight = exp(-0.5 * (x / spatial_sigma) * (x / spatial_sigma));
|
|
284
|
+
let s = textureSampleLevel(texA, samp, in.uv + offset, 0.0);
|
|
285
|
+
accum += s.r * weight;
|
|
286
|
+
totalWeight += weight;
|
|
287
|
+
}
|
|
288
|
+
matte = accum / max(totalWeight, 1e-5);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 2. Choke non-linear threshold contraction curve (applies even if radius == 0)
|
|
293
|
+
if (op >= 6.5 && op < 7.5) {
|
|
294
|
+
let threshRange = max(0.01, 1.0 - threshold);
|
|
295
|
+
matte = clamp((matte - threshold) / threshRange, 0.0, 1.0);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Range clamping
|
|
299
|
+
let lowerLimit = min(clampMin, clampMax);
|
|
300
|
+
let upperLimit = max(clampMin, clampMax);
|
|
301
|
+
matte = clamp(matte, lowerLimit, upperLimit);
|
|
302
|
+
|
|
303
|
+
// Optional Binarization
|
|
304
|
+
if (u.binarize > 0.5) {
|
|
305
|
+
matte = select(0.0, 1.0, matte >= threshold);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Optional Inversion
|
|
309
|
+
if (u.invertResult > 0.5) {
|
|
310
|
+
matte = 1.0 - matte;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Output format conversion
|
|
314
|
+
let fmt = u.outputFormat;
|
|
315
|
+
if (fmt < 0.5) {
|
|
316
|
+
// WhiteWithAlpha: vec4(matte, matte, matte, matte) (premultiplied standard mask)
|
|
317
|
+
return vec4<f32>(matte, matte, matte, matte);
|
|
318
|
+
} else if (fmt < 1.5) {
|
|
319
|
+
// GrayscaleRGB: vec4(matte, matte, matte, 1.0)
|
|
320
|
+
return vec4<f32>(matte, matte, matte, 1.0);
|
|
321
|
+
} else if (fmt < 2.5) {
|
|
322
|
+
// AlphaOnly: vec4(0.0, 0.0, 0.0, matte)
|
|
323
|
+
return vec4<f32>(0.0, 0.0, 0.0, matte);
|
|
324
|
+
} else {
|
|
325
|
+
// PassthroughRGB: Mask A color with new alpha
|
|
326
|
+
let origCol = textureSampleLevel(origTexA, samp, in.uv, 0.0);
|
|
327
|
+
if (origCol.a < 1e-4) {
|
|
328
|
+
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
329
|
+
}
|
|
330
|
+
let unpremult = origCol.rgb / origCol.a;
|
|
331
|
+
return vec4<f32>(unpremult * matte, matte);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
interface DeviceMaskMathResources {
|
|
337
|
+
uniformLayout: GPUBindGroupLayout;
|
|
338
|
+
textureLayout: GPUBindGroupLayout;
|
|
339
|
+
signalTextureLayout: GPUBindGroupLayout;
|
|
340
|
+
pipeline: GPURenderPipeline;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const deviceResourceCache = new WeakMap<GPUDevice, DeviceMaskMathResources>();
|
|
344
|
+
|
|
345
|
+
function getMaskMathResources(
|
|
346
|
+
device: GPUDevice,
|
|
347
|
+
targetFormat: GPUTextureFormat,
|
|
348
|
+
): DeviceMaskMathResources {
|
|
349
|
+
const cached = deviceResourceCache.get(device);
|
|
350
|
+
if (cached) return cached;
|
|
351
|
+
|
|
352
|
+
const uniformLayout = device.createBindGroupLayout({
|
|
353
|
+
entries: [
|
|
354
|
+
{
|
|
355
|
+
binding: 0,
|
|
356
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
357
|
+
buffer: { type: "uniform" },
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const textureLayout = device.createBindGroupLayout({
|
|
363
|
+
entries: [
|
|
364
|
+
{
|
|
365
|
+
binding: 0,
|
|
366
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
367
|
+
texture: { sampleType: "float" },
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
binding: 1,
|
|
371
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
372
|
+
texture: { sampleType: "float" },
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
binding: 2,
|
|
376
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
377
|
+
texture: { sampleType: "float" },
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
binding: 3,
|
|
381
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
382
|
+
sampler: { type: "filtering" },
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const signalTextureLayout = device.createBindGroupLayout({
|
|
388
|
+
entries: [
|
|
389
|
+
{
|
|
390
|
+
binding: 0,
|
|
391
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
392
|
+
texture: { sampleType: "float" },
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
binding: 1,
|
|
396
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
397
|
+
texture: { sampleType: "float" },
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
binding: 2,
|
|
401
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
402
|
+
texture: { sampleType: "float" },
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
binding: 3,
|
|
406
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
407
|
+
texture: { sampleType: "float" },
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
binding: 4,
|
|
411
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
412
|
+
sampler: { type: "filtering" },
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const shaderModule = device.createShaderModule({
|
|
418
|
+
code: WGSL_MASKMATH,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
422
|
+
bindGroupLayouts: [uniformLayout, textureLayout, signalTextureLayout],
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const pipeline = device.createRenderPipeline({
|
|
426
|
+
layout: pipelineLayout,
|
|
427
|
+
vertex: {
|
|
428
|
+
module: shaderModule,
|
|
429
|
+
entryPoint: "vs",
|
|
430
|
+
},
|
|
431
|
+
fragment: {
|
|
432
|
+
module: shaderModule,
|
|
433
|
+
entryPoint: "fs",
|
|
434
|
+
targets: [{ format: targetFormat }],
|
|
435
|
+
},
|
|
436
|
+
primitive: {
|
|
437
|
+
topology: "triangle-strip",
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const res: DeviceMaskMathResources = {
|
|
442
|
+
uniformLayout,
|
|
443
|
+
textureLayout,
|
|
444
|
+
signalTextureLayout,
|
|
445
|
+
pipeline,
|
|
446
|
+
};
|
|
447
|
+
deviceResourceCache.set(device, res);
|
|
448
|
+
return res;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export const WebGPURenderer: WebGPUNodeRenderer = async (args) => {
|
|
452
|
+
const {
|
|
453
|
+
ctx,
|
|
454
|
+
encoder,
|
|
455
|
+
pass,
|
|
456
|
+
targetView,
|
|
457
|
+
targetTexture,
|
|
458
|
+
targetWidth,
|
|
459
|
+
targetHeight,
|
|
460
|
+
props,
|
|
461
|
+
drawChild,
|
|
462
|
+
} = args;
|
|
463
|
+
|
|
464
|
+
const op = props.virtualMedia?.operation as MaskMathOp | undefined;
|
|
465
|
+
if (op?.op !== "MaskMath" || !op) return;
|
|
466
|
+
|
|
467
|
+
const childMedia = props.virtualMedia?.children?.[0];
|
|
468
|
+
if (!childMedia) return;
|
|
469
|
+
|
|
470
|
+
// End incoming pass
|
|
471
|
+
pass.end();
|
|
472
|
+
|
|
473
|
+
const frame = props.frame ?? 0;
|
|
474
|
+
const fps = props.fps || 30;
|
|
475
|
+
const width = targetWidth;
|
|
476
|
+
const height = targetHeight;
|
|
477
|
+
|
|
478
|
+
const resolveField = (
|
|
479
|
+
handleId: string | null | undefined,
|
|
480
|
+
fallbackVal: number,
|
|
481
|
+
clampMin = 0,
|
|
482
|
+
clampMax = 1000,
|
|
483
|
+
): {
|
|
484
|
+
val: number;
|
|
485
|
+
hasStaticSig: boolean;
|
|
486
|
+
sd: MaskMathSignalData | null;
|
|
487
|
+
handleId?: string | null;
|
|
488
|
+
} => {
|
|
489
|
+
if (handleId && op.inputs?.[handleId]?.connectionValid) {
|
|
490
|
+
const outputItem = op.inputs[handleId].outputItem;
|
|
491
|
+
if (outputItem?.type === "Number") {
|
|
492
|
+
const numVal =
|
|
493
|
+
typeof outputItem.data === "number"
|
|
494
|
+
? outputItem.data
|
|
495
|
+
: ((outputItem.data as { value?: number })?.value ?? fallbackVal);
|
|
496
|
+
return {
|
|
497
|
+
val: Math.max(clampMin, Math.min(clampMax, numVal)),
|
|
498
|
+
hasStaticSig: false,
|
|
499
|
+
sd: null,
|
|
500
|
+
handleId,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (outputItem?.type === "Signal" && outputItem.data) {
|
|
504
|
+
return {
|
|
505
|
+
val: fallbackVal,
|
|
506
|
+
hasStaticSig: true,
|
|
507
|
+
sd: outputItem.data as MaskMathSignalData,
|
|
508
|
+
handleId,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
val: Math.max(clampMin, Math.min(clampMax, fallbackVal)),
|
|
514
|
+
hasStaticSig: false,
|
|
515
|
+
sd: null,
|
|
516
|
+
handleId,
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const radiusRes = resolveField(
|
|
521
|
+
op.radiusHandleId,
|
|
522
|
+
Number(op.radius ?? 0),
|
|
523
|
+
0,
|
|
524
|
+
MAX_RADIUS,
|
|
525
|
+
);
|
|
526
|
+
const thresholdRes = resolveField(
|
|
527
|
+
op.thresholdHandleId,
|
|
528
|
+
Number(op.threshold ?? 0.5),
|
|
529
|
+
0.0,
|
|
530
|
+
1.0,
|
|
531
|
+
);
|
|
532
|
+
const clampMinRes = resolveField(
|
|
533
|
+
op.clampMinHandleId,
|
|
534
|
+
Number(op.clampMin ?? 0.0),
|
|
535
|
+
0.0,
|
|
536
|
+
1.0,
|
|
537
|
+
);
|
|
538
|
+
const clampMaxRes = resolveField(
|
|
539
|
+
op.clampMaxHandleId,
|
|
540
|
+
Number(op.clampMax ?? 1.0),
|
|
541
|
+
0.0,
|
|
542
|
+
1.0,
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// 1. Render primary Mask A child media
|
|
546
|
+
const tmpATex = ctx.renderer.getTemporaryTexture(width, height, [
|
|
547
|
+
...(props.excludeTextures || []),
|
|
548
|
+
targetTexture,
|
|
549
|
+
]);
|
|
550
|
+
const tmpAView = tmpATex.createView();
|
|
551
|
+
|
|
552
|
+
const childAPass = ctx.renderer.beginFrame(
|
|
553
|
+
encoder,
|
|
554
|
+
tmpAView,
|
|
555
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
556
|
+
width,
|
|
557
|
+
height,
|
|
558
|
+
"clear",
|
|
559
|
+
);
|
|
560
|
+
childAPass.end();
|
|
561
|
+
|
|
562
|
+
ctx.renderer.pushScissor({ x: 0, y: 0, width, height });
|
|
563
|
+
ctx.renderer.pushIdentity();
|
|
564
|
+
await drawChild(
|
|
565
|
+
childMedia,
|
|
566
|
+
{
|
|
567
|
+
...props,
|
|
568
|
+
virtualMedia: childMedia,
|
|
569
|
+
renderId: `${props.renderId}-maskA`,
|
|
570
|
+
excludeTextures: [...(props.excludeTextures || []), tmpATex],
|
|
571
|
+
},
|
|
572
|
+
tmpAView,
|
|
573
|
+
tmpATex,
|
|
574
|
+
width,
|
|
575
|
+
height,
|
|
576
|
+
);
|
|
577
|
+
ctx.renderer.popTransform();
|
|
578
|
+
ctx.renderer.popScissor();
|
|
579
|
+
|
|
580
|
+
// 2. Render optional Mask B child media if connected
|
|
581
|
+
let tmpBTex: GPUTexture | null = null;
|
|
582
|
+
if (op.maskBMedia) {
|
|
583
|
+
tmpBTex = ctx.renderer.getTemporaryTexture(width, height, [
|
|
584
|
+
...(props.excludeTextures || []),
|
|
585
|
+
targetTexture,
|
|
586
|
+
tmpATex,
|
|
587
|
+
]);
|
|
588
|
+
const tmpBView = tmpBTex.createView();
|
|
589
|
+
|
|
590
|
+
const childBPass = ctx.renderer.beginFrame(
|
|
591
|
+
encoder,
|
|
592
|
+
tmpBView,
|
|
593
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
594
|
+
width,
|
|
595
|
+
height,
|
|
596
|
+
"clear",
|
|
597
|
+
);
|
|
598
|
+
childBPass.end();
|
|
599
|
+
|
|
600
|
+
ctx.renderer.pushScissor({ x: 0, y: 0, width, height });
|
|
601
|
+
ctx.renderer.pushIdentity();
|
|
602
|
+
await drawChild(
|
|
603
|
+
op.maskBMedia as VirtualMediaData,
|
|
604
|
+
{
|
|
605
|
+
...props,
|
|
606
|
+
virtualMedia: op.maskBMedia as VirtualMediaData,
|
|
607
|
+
renderId: `${props.renderId}-maskB`,
|
|
608
|
+
excludeTextures: [...(props.excludeTextures || []), tmpATex, tmpBTex],
|
|
609
|
+
},
|
|
610
|
+
tmpBView,
|
|
611
|
+
tmpBTex,
|
|
612
|
+
width,
|
|
613
|
+
height,
|
|
614
|
+
);
|
|
615
|
+
ctx.renderer.popTransform();
|
|
616
|
+
ctx.renderer.popScissor();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// 3. Compile pipeline resources
|
|
620
|
+
const { pipeline, uniformLayout, textureLayout, signalTextureLayout } =
|
|
621
|
+
getMaskMathResources(ctx.device, ctx.renderer.format);
|
|
622
|
+
const sampler = ctx.renderer.samplerCache.getSampler(ctx.device);
|
|
623
|
+
|
|
624
|
+
const elapsedSeconds =
|
|
625
|
+
props.elapsedMs !== undefined ? props.elapsedMs / 1000 : frame / fps;
|
|
626
|
+
const durationSeconds = props.virtualMedia?.metadata?.durationMs
|
|
627
|
+
? props.virtualMedia.metadata.durationMs / 1000
|
|
628
|
+
: props.durationMs !== undefined
|
|
629
|
+
? props.durationMs / 1000
|
|
630
|
+
: 0;
|
|
631
|
+
|
|
632
|
+
const getSignalView = (
|
|
633
|
+
res: {
|
|
634
|
+
val: number;
|
|
635
|
+
hasStaticSig: boolean;
|
|
636
|
+
sd: MaskMathSignalData | null;
|
|
637
|
+
handleId?: string | null;
|
|
638
|
+
},
|
|
639
|
+
suffix: string,
|
|
640
|
+
) => {
|
|
641
|
+
if (res.hasStaticSig && res.sd) {
|
|
642
|
+
const cacheNodeId =
|
|
643
|
+
res.sd.nodeId ??
|
|
644
|
+
res.handleId ??
|
|
645
|
+
`${props.renderId ?? "maskmath"}_${suffix}`;
|
|
646
|
+
return signalRegistry.getOrCreate2DTextureView(
|
|
647
|
+
ctx.device,
|
|
648
|
+
encoder,
|
|
649
|
+
cacheNodeId,
|
|
650
|
+
elapsedSeconds,
|
|
651
|
+
durationSeconds,
|
|
652
|
+
res.sd,
|
|
653
|
+
width,
|
|
654
|
+
height,
|
|
655
|
+
props.renderId,
|
|
656
|
+
frame,
|
|
657
|
+
fps,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
return signalRegistry.getDummy1x1TextureView(ctx.device);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const radiusView = getSignalView(radiusRes, "radius_sig");
|
|
664
|
+
const threshView = getSignalView(thresholdRes, "threshold_sig");
|
|
665
|
+
const clampMinView = getSignalView(clampMinRes, "clampMin_sig");
|
|
666
|
+
const clampMaxView = getSignalView(clampMaxRes, "clampMax_sig");
|
|
667
|
+
|
|
668
|
+
const sigBindGroup = ctx.device.createBindGroup({
|
|
669
|
+
layout: signalTextureLayout,
|
|
670
|
+
entries: [
|
|
671
|
+
{ binding: 0, resource: radiusView },
|
|
672
|
+
{ binding: 1, resource: threshView },
|
|
673
|
+
{ binding: 2, resource: clampMinView },
|
|
674
|
+
{ binding: 3, resource: clampMaxView },
|
|
675
|
+
{ binding: 4, resource: sampler },
|
|
676
|
+
],
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const operationCode = OPERATION_MAP[op.operation ?? "Union"] ?? 0;
|
|
680
|
+
const channelACode = CHANNEL_MAP[op.channelA ?? "Alpha"] ?? 0;
|
|
681
|
+
const channelBCode = CHANNEL_MAP[op.channelB ?? "Alpha"] ?? 0;
|
|
682
|
+
const outputFormatCode =
|
|
683
|
+
OUTPUT_FORMAT_MAP[op.outputFormat ?? "WhiteWithAlpha"] ?? 0;
|
|
684
|
+
|
|
685
|
+
const dummyBView = tmpBTex ? tmpBTex.createView() : tmpAView;
|
|
686
|
+
|
|
687
|
+
// Populate separate uniform data buffers for each pass
|
|
688
|
+
const hUniformData = new Float32Array(20);
|
|
689
|
+
const vUniformData = new Float32Array(20);
|
|
690
|
+
|
|
691
|
+
const baseUniforms = [
|
|
692
|
+
0.0, // [0] dirX (set per pass)
|
|
693
|
+
0.0, // [1] dirY (set per pass)
|
|
694
|
+
radiusRes.val, // [2] radius
|
|
695
|
+
thresholdRes.val, // [3] threshold
|
|
696
|
+
clampMinRes.val, // [4] clampMin
|
|
697
|
+
clampMaxRes.val, // [5] clampMax
|
|
698
|
+
operationCode, // [6] operation
|
|
699
|
+
channelACode, // [7] channelA
|
|
700
|
+
channelBCode, // [8] channelB
|
|
701
|
+
op.binarize ? 1.0 : 0.0, // [9] binarize
|
|
702
|
+
op.invertResult ? 1.0 : 0.0, // [10] invertResult
|
|
703
|
+
outputFormatCode, // [11] outputFormat
|
|
704
|
+
tmpBTex ? 1.0 : 0.0, // [12] hasMaskB
|
|
705
|
+
radiusRes.hasStaticSig ? 1.0 : 0.0, // [13] hasRadiusSig
|
|
706
|
+
thresholdRes.hasStaticSig ? 1.0 : 0.0, // [14] hasThreshSig
|
|
707
|
+
clampMinRes.hasStaticSig ? 1.0 : 0.0, // [15] hasClampMinSig
|
|
708
|
+
clampMaxRes.hasStaticSig ? 1.0 : 0.0, // [16] hasClampMaxSig
|
|
709
|
+
0.0, // [17] isSecondPass (set per pass)
|
|
710
|
+
0.0, // [18] pad0
|
|
711
|
+
0.0, // [19] pad1
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
hUniformData.set(baseUniforms);
|
|
715
|
+
hUniformData[0] = 1.0; // dirX
|
|
716
|
+
hUniformData[1] = 0.0; // dirY
|
|
717
|
+
hUniformData[17] = 0.0; // isSecondPass = false
|
|
718
|
+
|
|
719
|
+
vUniformData.set(baseUniforms);
|
|
720
|
+
vUniformData[0] = 0.0; // dirX
|
|
721
|
+
vUniformData[1] = 1.0; // dirY
|
|
722
|
+
vUniformData[17] = 1.0; // isSecondPass = true
|
|
723
|
+
|
|
724
|
+
const outTex1 = ctx.renderer.getTemporaryTexture(width, height, [
|
|
725
|
+
tmpATex,
|
|
726
|
+
...(tmpBTex ? [tmpBTex] : []),
|
|
727
|
+
targetTexture,
|
|
728
|
+
...(props.excludeTextures || []),
|
|
729
|
+
]);
|
|
730
|
+
const outTex2 = ctx.renderer.getTemporaryTexture(width, height, [
|
|
731
|
+
tmpATex,
|
|
732
|
+
...(tmpBTex ? [tmpBTex] : []),
|
|
733
|
+
outTex1,
|
|
734
|
+
targetTexture,
|
|
735
|
+
...(props.excludeTextures || []),
|
|
736
|
+
]);
|
|
737
|
+
|
|
738
|
+
// Pass 1: Horizontal Pass (or direct combination)
|
|
739
|
+
const hBuffer = ctx.renderer.getTemporaryBuffer(hUniformData);
|
|
740
|
+
|
|
741
|
+
const hPass = encoder.beginRenderPass({
|
|
742
|
+
colorAttachments: [
|
|
743
|
+
{
|
|
744
|
+
view: outTex1.createView(),
|
|
745
|
+
loadOp: "clear",
|
|
746
|
+
storeOp: "store",
|
|
747
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
});
|
|
751
|
+
hPass.setPipeline(pipeline);
|
|
752
|
+
hPass.setBindGroup(
|
|
753
|
+
0,
|
|
754
|
+
ctx.device.createBindGroup({
|
|
755
|
+
layout: uniformLayout,
|
|
756
|
+
entries: [{ binding: 0, resource: { buffer: hBuffer } }],
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
759
|
+
hPass.setBindGroup(
|
|
760
|
+
1,
|
|
761
|
+
ctx.device.createBindGroup({
|
|
762
|
+
layout: textureLayout,
|
|
763
|
+
entries: [
|
|
764
|
+
{ binding: 0, resource: tmpAView },
|
|
765
|
+
{ binding: 1, resource: dummyBView },
|
|
766
|
+
{ binding: 2, resource: tmpAView },
|
|
767
|
+
{ binding: 3, resource: sampler },
|
|
768
|
+
],
|
|
769
|
+
}),
|
|
770
|
+
);
|
|
771
|
+
hPass.setBindGroup(2, sigBindGroup);
|
|
772
|
+
hPass.draw(4);
|
|
773
|
+
hPass.end();
|
|
774
|
+
|
|
775
|
+
// Pass 2: Vertical Pass + post-processing
|
|
776
|
+
const vBuffer = ctx.renderer.getTemporaryBuffer(vUniformData);
|
|
777
|
+
|
|
778
|
+
const vPass = encoder.beginRenderPass({
|
|
779
|
+
colorAttachments: [
|
|
780
|
+
{
|
|
781
|
+
view: outTex2.createView(),
|
|
782
|
+
loadOp: "clear",
|
|
783
|
+
storeOp: "store",
|
|
784
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
785
|
+
},
|
|
786
|
+
],
|
|
787
|
+
});
|
|
788
|
+
vPass.setPipeline(pipeline);
|
|
789
|
+
vPass.setBindGroup(
|
|
790
|
+
0,
|
|
791
|
+
ctx.device.createBindGroup({
|
|
792
|
+
layout: uniformLayout,
|
|
793
|
+
entries: [{ binding: 0, resource: { buffer: vBuffer } }],
|
|
794
|
+
}),
|
|
795
|
+
);
|
|
796
|
+
vPass.setBindGroup(
|
|
797
|
+
1,
|
|
798
|
+
ctx.device.createBindGroup({
|
|
799
|
+
layout: textureLayout,
|
|
800
|
+
entries: [
|
|
801
|
+
{ binding: 0, resource: outTex1.createView() },
|
|
802
|
+
{ binding: 1, resource: dummyBView },
|
|
803
|
+
{ binding: 2, resource: tmpAView },
|
|
804
|
+
{ binding: 3, resource: sampler },
|
|
805
|
+
],
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
vPass.setBindGroup(2, sigBindGroup);
|
|
809
|
+
vPass.draw(4);
|
|
810
|
+
vPass.end();
|
|
811
|
+
|
|
812
|
+
// 4. Final Pass: draw output into targetView
|
|
813
|
+
const finalPass = ctx.renderer.beginFrame(
|
|
814
|
+
encoder,
|
|
815
|
+
targetView,
|
|
816
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
817
|
+
targetWidth,
|
|
818
|
+
targetHeight,
|
|
819
|
+
"load",
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const finalOpacity = props.opacity ?? op.opacity ?? 1.0;
|
|
823
|
+
|
|
824
|
+
ctx.renderer.drawTexture(
|
|
825
|
+
finalPass,
|
|
826
|
+
outTex2,
|
|
827
|
+
{ x: 0, y: 0, width: targetWidth, height: targetHeight },
|
|
828
|
+
{ opacity: finalOpacity },
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
args.pass = finalPass;
|
|
832
|
+
};
|