@framefields/node-lut 2.0.2
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 +20 -0
- package/CHANGELOG.md +23 -0
- package/LICENCE +202 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +23 -0
- package/dist/index.mjs.map +1 -0
- package/dist/renderer.d.mts +7 -0
- package/dist/renderer.d.mts.map +1 -0
- package/dist/renderer.mjs +3 -0
- package/dist/renderers-kmnMKizc.mjs +372 -0
- package/dist/renderers-kmnMKizc.mjs.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +2 -0
- package/src/renderers/index.ts +6 -0
- package/src/renderers/webgpu-renderer.ts +536 -0
- package/src/shared/config.ts +30 -0
- package/src/shared/index.ts +1 -0
- package/tsconfig.json +12 -0
- package/tsdown.config.ts +13 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/// <reference types="webgpu" />
|
|
2
|
+
import type { WebGPUNodeRenderer } from "@framefields/node-sdk";
|
|
3
|
+
import { lutStore, signalRegistry } from "@framefields/webgpu-renderers";
|
|
4
|
+
|
|
5
|
+
export const LUT_3D_SHADER = `
|
|
6
|
+
struct LutUniforms {
|
|
7
|
+
intensity : f32,
|
|
8
|
+
lutSize : f32,
|
|
9
|
+
hasSignal : f32,
|
|
10
|
+
pad : f32,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
@group(0) @binding(0) var<uniform> u : LutUniforms;
|
|
14
|
+
@group(1) @binding(0) var tex : texture_2d<f32>;
|
|
15
|
+
@group(1) @binding(1) var samp : sampler;
|
|
16
|
+
@group(2) @binding(0) var lutTex : texture_3d<f32>;
|
|
17
|
+
@group(2) @binding(1) var lutSamp : sampler;
|
|
18
|
+
@group(3) @binding(0) var signalTex : texture_2d<f32>;
|
|
19
|
+
@group(3) @binding(1) var signalSamp : sampler;
|
|
20
|
+
|
|
21
|
+
struct VSOut {
|
|
22
|
+
@builtin(position) pos : vec4<f32>,
|
|
23
|
+
@location(0) uv : vec2<f32>,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> VSOut {
|
|
27
|
+
var pos = array<vec2<f32>, 4>(
|
|
28
|
+
vec2<f32>(-1.0, 1.0),
|
|
29
|
+
vec2<f32>(1.0, 1.0),
|
|
30
|
+
vec2<f32>(-1.0, -1.0),
|
|
31
|
+
vec2<f32>(1.0, -1.0)
|
|
32
|
+
);
|
|
33
|
+
var uv = array<vec2<f32>, 4>(
|
|
34
|
+
vec2<f32>(0.0, 0.0),
|
|
35
|
+
vec2<f32>(1.0, 0.0),
|
|
36
|
+
vec2<f32>(0.0, 1.0),
|
|
37
|
+
vec2<f32>(1.0, 1.0)
|
|
38
|
+
);
|
|
39
|
+
return VSOut(vec4<f32>(pos[vi], 0.0, 1.0), uv[vi]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
|
|
43
|
+
let inputColor = textureSampleLevel(tex, samp, in.uv, 0.0);
|
|
44
|
+
|
|
45
|
+
// Coordinate adjustment to sample from texel centers
|
|
46
|
+
let uvw = inputColor.rgb * ((u.lutSize - 1.0) / u.lutSize) + vec3<f32>(0.5 / u.lutSize);
|
|
47
|
+
let mappedColor = textureSampleLevel(lutTex, lutSamp, uvw, 0.0);
|
|
48
|
+
|
|
49
|
+
var activeIntensity = u.intensity;
|
|
50
|
+
if (u.hasSignal > 0.5) {
|
|
51
|
+
activeIntensity = textureSampleLevel(signalTex, signalSamp, in.uv, 0.0).r;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let finalRgb = inputColor.rgb + activeIntensity * (mappedColor.rgb - inputColor.rgb);
|
|
55
|
+
return vec4<f32>(finalRgb * inputColor.a, inputColor.a);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
export const LUT_1D_SHADER = `
|
|
60
|
+
struct LutUniforms {
|
|
61
|
+
intensity : f32,
|
|
62
|
+
lutSize : f32,
|
|
63
|
+
hasSignal : f32,
|
|
64
|
+
pad : f32,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
@group(0) @binding(0) var<uniform> u : LutUniforms;
|
|
68
|
+
@group(1) @binding(0) var tex : texture_2d<f32>;
|
|
69
|
+
@group(1) @binding(1) var samp : sampler;
|
|
70
|
+
@group(2) @binding(0) var lutTex : texture_1d<f32>;
|
|
71
|
+
@group(2) @binding(1) var lutSamp : sampler;
|
|
72
|
+
@group(3) @binding(0) var signalTex : texture_2d<f32>;
|
|
73
|
+
@group(3) @binding(1) var signalSamp : sampler;
|
|
74
|
+
|
|
75
|
+
struct VSOut {
|
|
76
|
+
@builtin(position) pos : vec4<f32>,
|
|
77
|
+
@location(0) uv : vec2<f32>,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> VSOut {
|
|
81
|
+
var pos = array<vec2<f32>, 4>(
|
|
82
|
+
vec2<f32>(-1.0, 1.0),
|
|
83
|
+
vec2<f32>(1.0, 1.0),
|
|
84
|
+
vec2<f32>(-1.0, -1.0),
|
|
85
|
+
vec2<f32>(1.0, -1.0)
|
|
86
|
+
);
|
|
87
|
+
var uv = array<vec2<f32>, 4>(
|
|
88
|
+
vec2<f32>(0.0, 0.0),
|
|
89
|
+
vec2<f32>(1.0, 0.0),
|
|
90
|
+
vec2<f32>(0.0, 1.0),
|
|
91
|
+
vec2<f32>(1.0, 1.0)
|
|
92
|
+
);
|
|
93
|
+
return VSOut(vec4<f32>(pos[vi], 0.0, 1.0), uv[vi]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
|
|
97
|
+
let inputColor = textureSampleLevel(tex, samp, in.uv, 0.0);
|
|
98
|
+
|
|
99
|
+
let rCoord = inputColor.r * ((u.lutSize - 1.0) / u.lutSize) + (0.5 / u.lutSize);
|
|
100
|
+
let gCoord = inputColor.g * ((u.lutSize - 1.0) / u.lutSize) + (0.5 / u.lutSize);
|
|
101
|
+
let bCoord = inputColor.b * ((u.lutSize - 1.0) / u.lutSize) + (0.5 / u.lutSize);
|
|
102
|
+
|
|
103
|
+
let mappedR = textureSampleLevel(lutTex, lutSamp, rCoord, 0.0).r;
|
|
104
|
+
let mappedG = textureSampleLevel(lutTex, lutSamp, gCoord, 0.0).g;
|
|
105
|
+
let mappedB = textureSampleLevel(lutTex, lutSamp, bCoord, 0.0).b;
|
|
106
|
+
|
|
107
|
+
var activeIntensity = u.intensity;
|
|
108
|
+
if (u.hasSignal > 0.5) {
|
|
109
|
+
activeIntensity = textureSampleLevel(signalTex, signalSamp, in.uv, 0.0).r;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let finalRgb = inputColor.rgb + activeIntensity * (vec3<f32>(mappedR, mappedG, mappedB) - inputColor.rgb);
|
|
113
|
+
return vec4<f32>(finalRgb * inputColor.a, inputColor.a);
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
interface DeviceLutLayouts {
|
|
118
|
+
lutUniformLayout: GPUBindGroupLayout;
|
|
119
|
+
singleTextureLayout: GPUBindGroupLayout;
|
|
120
|
+
lutTextureLayout3D: GPUBindGroupLayout;
|
|
121
|
+
lutTextureLayout1D: GPUBindGroupLayout;
|
|
122
|
+
signalTextureLayout: GPUBindGroupLayout;
|
|
123
|
+
pipelineCache: Map<string, GPURenderPipeline>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const deviceResourceCache = new WeakMap<GPUDevice, DeviceLutLayouts>();
|
|
127
|
+
const lutUniformData = new Float32Array(4);
|
|
128
|
+
|
|
129
|
+
function getDeviceLayouts(device: GPUDevice): DeviceLutLayouts {
|
|
130
|
+
let res = deviceResourceCache.get(device);
|
|
131
|
+
if (res) return res;
|
|
132
|
+
|
|
133
|
+
const lutUniformLayout = device.createBindGroupLayout({
|
|
134
|
+
entries: [
|
|
135
|
+
{
|
|
136
|
+
binding: 0,
|
|
137
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
138
|
+
buffer: { type: "uniform" },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
const singleTextureLayout = device.createBindGroupLayout({
|
|
143
|
+
entries: [
|
|
144
|
+
{
|
|
145
|
+
binding: 0,
|
|
146
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
147
|
+
texture: { sampleType: "float" },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
binding: 1,
|
|
151
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
152
|
+
sampler: { type: "filtering" },
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
const lutTextureLayout3D = device.createBindGroupLayout({
|
|
157
|
+
entries: [
|
|
158
|
+
{
|
|
159
|
+
binding: 0,
|
|
160
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
161
|
+
texture: { viewDimension: "3d", sampleType: "float" },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
binding: 1,
|
|
165
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
166
|
+
sampler: { type: "filtering" },
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
const lutTextureLayout1D = device.createBindGroupLayout({
|
|
171
|
+
entries: [
|
|
172
|
+
{
|
|
173
|
+
binding: 0,
|
|
174
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
175
|
+
texture: { viewDimension: "1d", sampleType: "float" },
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
binding: 1,
|
|
179
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
180
|
+
sampler: { type: "filtering" },
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
const signalTextureLayout = device.createBindGroupLayout({
|
|
185
|
+
entries: [
|
|
186
|
+
{
|
|
187
|
+
binding: 0,
|
|
188
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
189
|
+
texture: { sampleType: "float" },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
binding: 1,
|
|
193
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
194
|
+
sampler: { type: "filtering" },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
res = {
|
|
200
|
+
lutUniformLayout,
|
|
201
|
+
singleTextureLayout,
|
|
202
|
+
lutTextureLayout3D,
|
|
203
|
+
lutTextureLayout1D,
|
|
204
|
+
signalTextureLayout,
|
|
205
|
+
pipelineCache: new Map(),
|
|
206
|
+
};
|
|
207
|
+
deviceResourceCache.set(device, res);
|
|
208
|
+
return res;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getLutResources(
|
|
212
|
+
device: GPUDevice,
|
|
213
|
+
format: GPUTextureFormat,
|
|
214
|
+
type: "1D" | "3D",
|
|
215
|
+
) {
|
|
216
|
+
const layouts = getDeviceLayouts(device);
|
|
217
|
+
const cacheKey = `lut_${type}_${format}`;
|
|
218
|
+
let pipeline = layouts.pipelineCache.get(cacheKey);
|
|
219
|
+
if (!pipeline) {
|
|
220
|
+
const shaderCode = type === "3D" ? LUT_3D_SHADER : LUT_1D_SHADER;
|
|
221
|
+
const lutModule = device.createShaderModule({
|
|
222
|
+
label: `lut_${type.toLowerCase()}_${format}.wgsl`,
|
|
223
|
+
code: shaderCode,
|
|
224
|
+
});
|
|
225
|
+
const currentLutLayout =
|
|
226
|
+
type === "3D" ? layouts.lutTextureLayout3D : layouts.lutTextureLayout1D;
|
|
227
|
+
pipeline = device.createRenderPipeline({
|
|
228
|
+
label: `LutPipeline_${type}_${format}`,
|
|
229
|
+
layout: device.createPipelineLayout({
|
|
230
|
+
bindGroupLayouts: [
|
|
231
|
+
layouts.lutUniformLayout,
|
|
232
|
+
layouts.singleTextureLayout,
|
|
233
|
+
currentLutLayout,
|
|
234
|
+
layouts.signalTextureLayout,
|
|
235
|
+
],
|
|
236
|
+
}),
|
|
237
|
+
vertex: { module: lutModule, entryPoint: "vs" },
|
|
238
|
+
fragment: {
|
|
239
|
+
module: lutModule,
|
|
240
|
+
entryPoint: "fs",
|
|
241
|
+
targets: [{ format }],
|
|
242
|
+
},
|
|
243
|
+
primitive: { topology: "triangle-strip" },
|
|
244
|
+
});
|
|
245
|
+
layouts.pipelineCache.set(cacheKey, pipeline);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
lutPipeline: pipeline,
|
|
250
|
+
lutUniformLayout: layouts.lutUniformLayout,
|
|
251
|
+
singleTextureLayout: layouts.singleTextureLayout,
|
|
252
|
+
lutTextureLayout:
|
|
253
|
+
type === "3D" ? layouts.lutTextureLayout3D : layouts.lutTextureLayout1D,
|
|
254
|
+
signalTextureLayout: layouts.signalTextureLayout,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const LutWebGPURenderer: WebGPUNodeRenderer = async (args) => {
|
|
259
|
+
const {
|
|
260
|
+
ctx,
|
|
261
|
+
encoder,
|
|
262
|
+
pass,
|
|
263
|
+
targetView,
|
|
264
|
+
targetTexture,
|
|
265
|
+
targetWidth,
|
|
266
|
+
targetHeight,
|
|
267
|
+
props,
|
|
268
|
+
drawChild,
|
|
269
|
+
} = args;
|
|
270
|
+
const frame = props.frame ?? 0;
|
|
271
|
+
const fps = props.fps || 30;
|
|
272
|
+
const op = props.virtualMedia?.operation as any;
|
|
273
|
+
if (op?.op !== "ApplyLUT" || !op) return;
|
|
274
|
+
const childMedia = props.virtualMedia.children?.[0];
|
|
275
|
+
if (!childMedia) return;
|
|
276
|
+
|
|
277
|
+
const lutUrl = op.lutUrl;
|
|
278
|
+
if (!lutUrl) return;
|
|
279
|
+
|
|
280
|
+
// End the active pass
|
|
281
|
+
pass.end();
|
|
282
|
+
|
|
283
|
+
const width = targetWidth;
|
|
284
|
+
const height = targetHeight;
|
|
285
|
+
|
|
286
|
+
// Resolve the intensity signal
|
|
287
|
+
const intensityInput = op.intensityHandleId
|
|
288
|
+
? op.inputs?.[op.intensityHandleId]
|
|
289
|
+
: null;
|
|
290
|
+
const hasSignal = !!(
|
|
291
|
+
intensityInput?.connectionValid &&
|
|
292
|
+
intensityInput.outputItem?.type === "Signal"
|
|
293
|
+
);
|
|
294
|
+
const sd = hasSignal ? intensityInput?.outputItem?.data : null;
|
|
295
|
+
|
|
296
|
+
let intensity = 1.0;
|
|
297
|
+
let hasStaticSignal = false;
|
|
298
|
+
|
|
299
|
+
if (!hasSignal) {
|
|
300
|
+
if (
|
|
301
|
+
intensityInput?.connectionValid &&
|
|
302
|
+
intensityInput.outputItem?.type === "Number"
|
|
303
|
+
) {
|
|
304
|
+
intensity = Number(intensityInput.outputItem.data ?? 1.0);
|
|
305
|
+
} else {
|
|
306
|
+
intensity = Number(op.intensity ?? 1.0);
|
|
307
|
+
}
|
|
308
|
+
} else if (sd && typeof sd === "object") {
|
|
309
|
+
intensity = Number((sd as any).offset ?? 1.0);
|
|
310
|
+
hasStaticSignal = true;
|
|
311
|
+
}
|
|
312
|
+
intensity = Math.max(0, Math.min(10, intensity));
|
|
313
|
+
|
|
314
|
+
// Load the GPU LUT resource without blocking the rendering pipeline.
|
|
315
|
+
let lut = lutStore.get(lutUrl, ctx.device);
|
|
316
|
+
if (!lut) {
|
|
317
|
+
const isHeadless =
|
|
318
|
+
props.isHeadless ??
|
|
319
|
+
(typeof window === "undefined" ||
|
|
320
|
+
(globalThis as any).__IS_HEADLESS_RENDERER__);
|
|
321
|
+
|
|
322
|
+
if (isHeadless || props.forceWait) {
|
|
323
|
+
try {
|
|
324
|
+
lut = await lutStore.getOrLoad(lutUrl, ctx.device);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (
|
|
327
|
+
!(err instanceof DOMException && err.name === "AbortError") &&
|
|
328
|
+
!(
|
|
329
|
+
err &&
|
|
330
|
+
typeof err === "object" &&
|
|
331
|
+
"name" in err &&
|
|
332
|
+
err.name === "AbortError"
|
|
333
|
+
)
|
|
334
|
+
) {
|
|
335
|
+
console.error("[ApplyLUT] Failed to load LUT synchronously:", err);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// Trigger background load
|
|
340
|
+
lutStore.getOrLoad(lutUrl, ctx.device).catch((err) => {
|
|
341
|
+
if (
|
|
342
|
+
(err instanceof DOMException && err.name === "AbortError") ||
|
|
343
|
+
(err &&
|
|
344
|
+
typeof err === "object" &&
|
|
345
|
+
"name" in err &&
|
|
346
|
+
err.name === "AbortError")
|
|
347
|
+
) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.error("[ApplyLUT] Failed to load LUT in background:", err);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Fallback to any matching cached LUT for the same node
|
|
355
|
+
if (!lut && lutUrl.startsWith("runtime://lut/")) {
|
|
356
|
+
const prefix = lutUrl.substring(0, lutUrl.lastIndexOf("-") + 1);
|
|
357
|
+
if (prefix) {
|
|
358
|
+
lut = lutStore.getAnyMatching(prefix, ctx.device);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const tmpTex = ctx.renderer.getTemporaryTexture(width, height, [
|
|
364
|
+
...(props.excludeTextures || []),
|
|
365
|
+
targetTexture,
|
|
366
|
+
]);
|
|
367
|
+
const tmpView = tmpTex.createView();
|
|
368
|
+
|
|
369
|
+
// Clear temporary texture
|
|
370
|
+
const childPass = ctx.renderer.beginFrame(
|
|
371
|
+
encoder,
|
|
372
|
+
tmpView,
|
|
373
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
374
|
+
width,
|
|
375
|
+
height,
|
|
376
|
+
"clear",
|
|
377
|
+
);
|
|
378
|
+
childPass.end();
|
|
379
|
+
|
|
380
|
+
// Draw child frame into tmpTex
|
|
381
|
+
ctx.renderer.pushScissor({ x: 0, y: 0, width, height });
|
|
382
|
+
ctx.renderer.pushIdentity();
|
|
383
|
+
await drawChild(
|
|
384
|
+
childMedia,
|
|
385
|
+
{ ...props, virtualMedia: childMedia },
|
|
386
|
+
tmpView,
|
|
387
|
+
tmpTex,
|
|
388
|
+
width,
|
|
389
|
+
height,
|
|
390
|
+
);
|
|
391
|
+
ctx.renderer.popTransform();
|
|
392
|
+
ctx.renderer.popScissor();
|
|
393
|
+
|
|
394
|
+
// If no LUT is ready (and no fallback is available), draw the child frame directly as a passthrough
|
|
395
|
+
if (!lut) {
|
|
396
|
+
const finalPass = ctx.renderer.beginFrame(
|
|
397
|
+
encoder,
|
|
398
|
+
targetView,
|
|
399
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
400
|
+
targetWidth,
|
|
401
|
+
targetHeight,
|
|
402
|
+
"load",
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
ctx.renderer.drawTexture(
|
|
406
|
+
finalPass,
|
|
407
|
+
tmpTex,
|
|
408
|
+
{ x: 0, y: 0, width: targetWidth, height: targetHeight },
|
|
409
|
+
{ opacity: op.opacity ?? 1 },
|
|
410
|
+
);
|
|
411
|
+
args.pass = finalPass;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Fetch pipeline and layouts
|
|
416
|
+
const {
|
|
417
|
+
lutPipeline: pipeline,
|
|
418
|
+
lutUniformLayout: uLayout,
|
|
419
|
+
singleTextureLayout: tLayout,
|
|
420
|
+
lutTextureLayout: lLayout,
|
|
421
|
+
signalTextureLayout: sigLayout,
|
|
422
|
+
} = getLutResources(ctx.device, ctx.renderer.format, lut.type);
|
|
423
|
+
|
|
424
|
+
const sampler = ctx.renderer.samplerCache.getSampler(ctx.device);
|
|
425
|
+
|
|
426
|
+
// Setup intermediate output texture
|
|
427
|
+
const outTex = ctx.renderer.getTemporaryTexture(width, height, [
|
|
428
|
+
tmpTex,
|
|
429
|
+
targetTexture,
|
|
430
|
+
...(props.excludeTextures || []),
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
// Fetch dynamic intensity signal texture view
|
|
434
|
+
let signalView: GPUTextureView;
|
|
435
|
+
if (hasStaticSignal) {
|
|
436
|
+
const elapsedSeconds =
|
|
437
|
+
props.elapsedMs !== undefined ? props.elapsedMs / 1000 : frame / fps;
|
|
438
|
+
const durationSeconds = props.virtualMedia?.metadata?.durationMs
|
|
439
|
+
? props.virtualMedia.metadata.durationMs / 1000
|
|
440
|
+
: props.durationMs !== undefined
|
|
441
|
+
? props.durationMs / 1000
|
|
442
|
+
: 0;
|
|
443
|
+
signalView = signalRegistry.getOrCreate2DTextureView(
|
|
444
|
+
ctx.device,
|
|
445
|
+
encoder,
|
|
446
|
+
sd.nodeId ?? "lut_intensity_sig",
|
|
447
|
+
elapsedSeconds,
|
|
448
|
+
durationSeconds,
|
|
449
|
+
sd,
|
|
450
|
+
width,
|
|
451
|
+
height,
|
|
452
|
+
props.renderId,
|
|
453
|
+
frame,
|
|
454
|
+
fps,
|
|
455
|
+
);
|
|
456
|
+
} else {
|
|
457
|
+
signalView = signalRegistry.getDummy1x1TextureView(ctx.device);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Prepare uniforms
|
|
461
|
+
lutUniformData[0] = intensity;
|
|
462
|
+
lutUniformData[1] = lut.size;
|
|
463
|
+
lutUniformData[2] = hasStaticSignal ? 1 : 0;
|
|
464
|
+
lutUniformData[3] = 0; // padding
|
|
465
|
+
|
|
466
|
+
const uniformBuffer = ctx.renderer.getTemporaryBuffer(lutUniformData);
|
|
467
|
+
|
|
468
|
+
const renderPass = encoder.beginRenderPass({
|
|
469
|
+
colorAttachments: [
|
|
470
|
+
{
|
|
471
|
+
view: outTex.createView(),
|
|
472
|
+
loadOp: "clear",
|
|
473
|
+
storeOp: "store",
|
|
474
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
renderPass.setPipeline(pipeline!);
|
|
480
|
+
renderPass.setBindGroup(
|
|
481
|
+
0,
|
|
482
|
+
ctx.device.createBindGroup({
|
|
483
|
+
layout: uLayout!,
|
|
484
|
+
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
renderPass.setBindGroup(
|
|
488
|
+
1,
|
|
489
|
+
ctx.renderer.bindGroupCache.getBindGroup(
|
|
490
|
+
ctx.device,
|
|
491
|
+
tLayout!,
|
|
492
|
+
tmpTex,
|
|
493
|
+
sampler,
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
renderPass.setBindGroup(
|
|
497
|
+
2,
|
|
498
|
+
ctx.device.createBindGroup({
|
|
499
|
+
layout: lLayout!,
|
|
500
|
+
entries: [
|
|
501
|
+
{ binding: 0, resource: lut.texture.createView() },
|
|
502
|
+
{ binding: 1, resource: sampler },
|
|
503
|
+
],
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
renderPass.setBindGroup(
|
|
507
|
+
3,
|
|
508
|
+
ctx.device.createBindGroup({
|
|
509
|
+
layout: sigLayout!,
|
|
510
|
+
entries: [
|
|
511
|
+
{ binding: 0, resource: signalView },
|
|
512
|
+
{ binding: 1, resource: sampler },
|
|
513
|
+
],
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
renderPass.draw(4);
|
|
517
|
+
renderPass.end();
|
|
518
|
+
|
|
519
|
+
// Blending/opacity draw to the targetView
|
|
520
|
+
const finalPass = ctx.renderer.beginFrame(
|
|
521
|
+
encoder,
|
|
522
|
+
targetView,
|
|
523
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
524
|
+
targetWidth,
|
|
525
|
+
targetHeight,
|
|
526
|
+
"load",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
ctx.renderer.drawTexture(
|
|
530
|
+
finalPass,
|
|
531
|
+
outTex,
|
|
532
|
+
{ x: 0, y: 0, width: targetWidth, height: targetHeight },
|
|
533
|
+
{ opacity: op.opacity ?? 1 },
|
|
534
|
+
);
|
|
535
|
+
args.pass = finalPass;
|
|
536
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configBuilder,
|
|
3
|
+
ImageResultSchema,
|
|
4
|
+
VideoResultSchema,
|
|
5
|
+
} from "@framefields/node-sdk";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export const lutConfig = configBuilder()
|
|
9
|
+
.field("intensity", z.number().min(0).max(10).default(1.0), {
|
|
10
|
+
bindable: true,
|
|
11
|
+
dataTypes: ["Number", "Signal"],
|
|
12
|
+
label: "Intensity Signal",
|
|
13
|
+
})
|
|
14
|
+
.build();
|
|
15
|
+
|
|
16
|
+
export const LutNodeConfigSchema = lutConfig.schema;
|
|
17
|
+
|
|
18
|
+
export type LutNodeConfig = z.infer<typeof LutNodeConfigSchema>;
|
|
19
|
+
|
|
20
|
+
export const LutResultSchema = z.union([ImageResultSchema, VideoResultSchema]);
|
|
21
|
+
|
|
22
|
+
export type LutResult = z.infer<typeof LutResultSchema>;
|
|
23
|
+
|
|
24
|
+
export const LUT_OUTPUT_TYPE_MAP: Record<string, "Image" | "Video" | "GIF"> = {
|
|
25
|
+
Video: "Video",
|
|
26
|
+
Lottie: "Video",
|
|
27
|
+
GIF: "GIF",
|
|
28
|
+
Image: "Image",
|
|
29
|
+
SVG: "Image",
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./config.js";
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED