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