@framefields/node-compositor 0.0.0-stage → 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 +22 -0
- package/.turbo/turbo-test.log +21 -0
- package/CHANGELOG.md +32 -0
- package/LICENCE +202 -0
- package/SKILL.md +370 -0
- package/dist/index-B0ir9Z7o.d.mts +139 -0
- package/dist/index-B0ir9Z7o.d.mts.map +1 -0
- package/dist/index.d.mts +230 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +674 -0
- package/dist/index.mjs.map +1 -0
- package/dist/renderer.d.mts +2 -0
- package/dist/renderer.mjs +3 -0
- package/dist/renderers-DoYYCzgA.mjs +3326 -0
- package/dist/renderers-DoYYCzgA.mjs.map +1 -0
- package/package.json +51 -3
- package/src/index.ts +2 -0
- package/src/renderers/audio-processor.ts +123 -0
- package/src/renderers/container-bg.test.ts +22 -0
- package/src/renderers/container-bg.ts +15 -0
- package/src/renderers/index.ts +9 -0
- package/src/renderers/transform.test.ts +120 -0
- package/src/renderers/transform.ts +101 -0
- package/src/renderers/transform3d.test.ts +290 -0
- package/src/renderers/transform3d.ts +428 -0
- package/src/renderers/webgpu-renderer.ts +4038 -0
- package/src/shared/audio-reactive.test.ts +224 -0
- package/src/shared/bezier-solver.test.ts +92 -0
- package/src/shared/bezier-solver.ts +257 -0
- package/src/shared/compiler.test.ts +1309 -0
- package/src/shared/compiler.ts +996 -0
- package/src/shared/config.ts +44 -0
- package/src/shared/index.ts +3 -0
- package/src/shared/kinetic-typography.test.ts +138 -0
- package/src/shared/presets.test.ts +217 -0
- package/src/shared/presets.ts +644 -0
- package/src/shared/processor.ts +105 -0
- package/tsconfig.json +13 -0
- package/tsdown.config.ts +13 -0
- package/README.md +0 -3
|
@@ -0,0 +1,4038 @@
|
|
|
1
|
+
/// <reference types="webgpu" />
|
|
2
|
+
import { resolveMediaSourceUrl } from "@framefields/client-utils";
|
|
3
|
+
import { decodeAudioSource } from "@framefields/compositions";
|
|
4
|
+
import {
|
|
5
|
+
collectNodeOps,
|
|
6
|
+
computeLayout,
|
|
7
|
+
initLayout,
|
|
8
|
+
type LayoutNode,
|
|
9
|
+
type Rect,
|
|
10
|
+
} from "@framefields/compositions/program";
|
|
11
|
+
import type { MeshAudioDeformConfig } from "@framefields/core";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_DURATION_MS,
|
|
14
|
+
getActiveMediaMetadata,
|
|
15
|
+
type VirtualMediaData,
|
|
16
|
+
} from "@framefields/core";
|
|
17
|
+
import type { WebGPUNodeRenderer } from "@framefields/node-sdk";
|
|
18
|
+
import { measureText } from "@framefields/renderers";
|
|
19
|
+
import { TemporalDeflickerPipeline } from "@framefields/tensor-webgpu";
|
|
20
|
+
import {
|
|
21
|
+
AudioLatentTrackCache,
|
|
22
|
+
type BlendMode,
|
|
23
|
+
Camera3D,
|
|
24
|
+
type Camera3DPose,
|
|
25
|
+
type Color,
|
|
26
|
+
type DrawMesh3DOpts,
|
|
27
|
+
type DrawQuad3DOpts,
|
|
28
|
+
type ExtrudedTextGeometryOptions,
|
|
29
|
+
type GPUMeshBuffers,
|
|
30
|
+
generateExtrudedTextGeometry,
|
|
31
|
+
jointMatrices,
|
|
32
|
+
type Light3DOptions,
|
|
33
|
+
type Mat4,
|
|
34
|
+
Matrix4Math,
|
|
35
|
+
type Mesh3DData,
|
|
36
|
+
type Model3DData,
|
|
37
|
+
measureMaxCaptionHeight,
|
|
38
|
+
parseColor,
|
|
39
|
+
poseNodes,
|
|
40
|
+
type RenderContextValue,
|
|
41
|
+
Renderer3D,
|
|
42
|
+
ScreenSpaceRelightPipeline,
|
|
43
|
+
type ShapeFillConfig,
|
|
44
|
+
type ShapeGeometryConfig,
|
|
45
|
+
SlugFontCache,
|
|
46
|
+
type SlugGlyphBatch,
|
|
47
|
+
srtLoader,
|
|
48
|
+
Vector3Math,
|
|
49
|
+
} from "@framefields/webgpu-renderers";
|
|
50
|
+
|
|
51
|
+
import { compileTimeline } from "../shared/compiler.js";
|
|
52
|
+
import type { CompositorOperation } from "../shared/config.js";
|
|
53
|
+
import { shouldPaintContainerBackground } from "./container-bg.js";
|
|
54
|
+
import { buildLayerMatrix, toDOMMatrix } from "./transform.js";
|
|
55
|
+
import {
|
|
56
|
+
has3DTransform,
|
|
57
|
+
type Quad2D,
|
|
58
|
+
solveHomography,
|
|
59
|
+
Transform3DMath,
|
|
60
|
+
} from "./transform3d.js";
|
|
61
|
+
|
|
62
|
+
// Layout depends on the document tree (whose `children` array keeps its
|
|
63
|
+
// identity between frames), the viewport, media sizes and the few layout
|
|
64
|
+
// props a timeline can animate. A film's tree holds every scene, so
|
|
65
|
+
// re-running Yoga each frame is costly when none of those changed.
|
|
66
|
+
const LAYOUT_PROPS = [
|
|
67
|
+
"width",
|
|
68
|
+
"height",
|
|
69
|
+
"gap",
|
|
70
|
+
"padding",
|
|
71
|
+
"fontSize",
|
|
72
|
+
"letterSpacing",
|
|
73
|
+
] as const;
|
|
74
|
+
const layoutMemo = new WeakMap<
|
|
75
|
+
object,
|
|
76
|
+
{ key: string; rects: Record<string, Rect> }
|
|
77
|
+
>();
|
|
78
|
+
|
|
79
|
+
function layoutKey(
|
|
80
|
+
width: number,
|
|
81
|
+
height: number,
|
|
82
|
+
targetsById: Record<string, any>,
|
|
83
|
+
mediaDims: Map<string, { width: number; height: number }>,
|
|
84
|
+
): string {
|
|
85
|
+
const parts: unknown[] = [width, height];
|
|
86
|
+
for (const id in targetsById) {
|
|
87
|
+
const t = targetsById[id];
|
|
88
|
+
for (const k of LAYOUT_PROPS)
|
|
89
|
+
if (t[k] !== undefined) parts.push(id, k, t[k]);
|
|
90
|
+
}
|
|
91
|
+
for (const [id, d] of mediaDims) parts.push(id, d.width, d.height);
|
|
92
|
+
return JSON.stringify(parts);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const compositorHomographyWgsl = `
|
|
96
|
+
struct HomographyUniforms {
|
|
97
|
+
matrix_row0 : vec4<f32>,
|
|
98
|
+
matrix_row1 : vec4<f32>,
|
|
99
|
+
matrix_row2 : vec4<f32>,
|
|
100
|
+
params : vec4<f32>, // params.x = opacity
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
@group(0) @binding(0) var<uniform> u : HomographyUniforms;
|
|
104
|
+
@group(1) @binding(0) var tex : texture_2d<f32>;
|
|
105
|
+
@group(1) @binding(1) var samp : sampler;
|
|
106
|
+
|
|
107
|
+
struct VSOut {
|
|
108
|
+
@builtin(position) pos : vec4<f32>,
|
|
109
|
+
@location(0) uv : vec2<f32>,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> VSOut {
|
|
113
|
+
var pos = array<vec2<f32>, 4>(
|
|
114
|
+
vec2<f32>(-1.0, 1.0),
|
|
115
|
+
vec2<f32>(1.0, 1.0),
|
|
116
|
+
vec2<f32>(-1.0, -1.0),
|
|
117
|
+
vec2<f32>(1.0, -1.0)
|
|
118
|
+
);
|
|
119
|
+
var uv = array<vec2<f32>, 4>(
|
|
120
|
+
vec2<f32>(0.0, 0.0),
|
|
121
|
+
vec2<f32>(1.0, 0.0),
|
|
122
|
+
vec2<f32>(0.0, 1.0),
|
|
123
|
+
vec2<f32>(1.0, 1.0)
|
|
124
|
+
);
|
|
125
|
+
return VSOut(vec4<f32>(pos[vi], 0.0, 1.0), uv[vi]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@fragment fn fs(in : VSOut) -> @location(0) vec4<f32> {
|
|
129
|
+
let x = in.uv.x;
|
|
130
|
+
let y = in.uv.y;
|
|
131
|
+
|
|
132
|
+
let d = u.matrix_row2.x * x + u.matrix_row2.y * y + u.matrix_row2.z;
|
|
133
|
+
if (d <= 1e-6) {
|
|
134
|
+
return vec4<f32>(0.0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let src_u = (u.matrix_row0.x * x + u.matrix_row0.y * y + u.matrix_row0.z) / d;
|
|
138
|
+
let src_v = (u.matrix_row1.x * x + u.matrix_row1.y * y + u.matrix_row1.z) / d;
|
|
139
|
+
|
|
140
|
+
if (src_u < 0.0 || src_u > 1.0 || src_v < 0.0 || src_v > 1.0) {
|
|
141
|
+
return vec4<f32>(0.0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let color = textureSampleLevel(tex, samp, vec2<f32>(src_u, src_v), 0.0);
|
|
145
|
+
return color * u.params.x;
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
interface DeviceHomographyResources {
|
|
150
|
+
pipeline: GPURenderPipeline;
|
|
151
|
+
uniformLayout: GPUBindGroupLayout;
|
|
152
|
+
textureLayout: GPUBindGroupLayout;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const deviceHomographyCache = new WeakMap<
|
|
156
|
+
GPUDevice,
|
|
157
|
+
DeviceHomographyResources
|
|
158
|
+
>();
|
|
159
|
+
|
|
160
|
+
function getHomographyResources(
|
|
161
|
+
device: GPUDevice,
|
|
162
|
+
format: GPUTextureFormat,
|
|
163
|
+
): DeviceHomographyResources {
|
|
164
|
+
let res = deviceHomographyCache.get(device);
|
|
165
|
+
if (!res) {
|
|
166
|
+
const uniformLayout = device.createBindGroupLayout({
|
|
167
|
+
entries: [
|
|
168
|
+
{
|
|
169
|
+
binding: 0,
|
|
170
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
171
|
+
buffer: { type: "uniform" },
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const textureLayout = device.createBindGroupLayout({
|
|
177
|
+
entries: [
|
|
178
|
+
{
|
|
179
|
+
binding: 0,
|
|
180
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
181
|
+
texture: { sampleType: "float" },
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
binding: 1,
|
|
185
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
186
|
+
sampler: { type: "filtering" },
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const module = device.createShaderModule({
|
|
192
|
+
label: "compositor_homography.wgsl",
|
|
193
|
+
code: compositorHomographyWgsl,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const pipeline = device.createRenderPipeline({
|
|
197
|
+
label: "CompositorHomographyPipeline",
|
|
198
|
+
layout: device.createPipelineLayout({
|
|
199
|
+
bindGroupLayouts: [uniformLayout, textureLayout],
|
|
200
|
+
}),
|
|
201
|
+
vertex: {
|
|
202
|
+
module,
|
|
203
|
+
entryPoint: "vs",
|
|
204
|
+
},
|
|
205
|
+
fragment: {
|
|
206
|
+
module,
|
|
207
|
+
entryPoint: "fs",
|
|
208
|
+
targets: [
|
|
209
|
+
{
|
|
210
|
+
format,
|
|
211
|
+
blend: {
|
|
212
|
+
color: {
|
|
213
|
+
srcFactor: "one",
|
|
214
|
+
dstFactor: "one-minus-src-alpha",
|
|
215
|
+
operation: "add",
|
|
216
|
+
},
|
|
217
|
+
alpha: {
|
|
218
|
+
srcFactor: "one",
|
|
219
|
+
dstFactor: "one-minus-src-alpha",
|
|
220
|
+
operation: "add",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
primitive: {
|
|
227
|
+
topology: "triangle-strip",
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
res = { pipeline, uniformLayout, textureLayout };
|
|
232
|
+
deviceHomographyCache.set(device, res);
|
|
233
|
+
}
|
|
234
|
+
return res;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const audioTrackLatentCache = new Map<string, AudioLatentTrackCache>();
|
|
238
|
+
const deviceRenderer3DCache = new WeakMap<GPUDevice, Map<string, Renderer3D>>();
|
|
239
|
+
|
|
240
|
+
/** One Renderer3D per device, target format and MSAA sample count. */
|
|
241
|
+
function getRenderer3D(
|
|
242
|
+
device: GPUDevice,
|
|
243
|
+
format: GPUTextureFormat,
|
|
244
|
+
sampleCount: number,
|
|
245
|
+
): Renderer3D {
|
|
246
|
+
let byConfig = deviceRenderer3DCache.get(device);
|
|
247
|
+
if (!byConfig) {
|
|
248
|
+
byConfig = new Map();
|
|
249
|
+
deviceRenderer3DCache.set(device, byConfig);
|
|
250
|
+
}
|
|
251
|
+
const key = `${format}|${sampleCount}`;
|
|
252
|
+
let r3d = byConfig.get(key);
|
|
253
|
+
if (!r3d) {
|
|
254
|
+
r3d = new Renderer3D(device, format, sampleCount);
|
|
255
|
+
byConfig.set(key, r3d);
|
|
256
|
+
}
|
|
257
|
+
return r3d;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const deviceRelightPipelineCache = new WeakMap<
|
|
261
|
+
GPUDevice,
|
|
262
|
+
ScreenSpaceRelightPipeline
|
|
263
|
+
>();
|
|
264
|
+
|
|
265
|
+
function getScreenSpaceRelightPipeline(
|
|
266
|
+
device: GPUDevice,
|
|
267
|
+
format: GPUTextureFormat,
|
|
268
|
+
): ScreenSpaceRelightPipeline {
|
|
269
|
+
let relight = deviceRelightPipelineCache.get(device);
|
|
270
|
+
if (!relight || relight.format !== format) {
|
|
271
|
+
relight = new ScreenSpaceRelightPipeline(device, format);
|
|
272
|
+
deviceRelightPipelineCache.set(device, relight);
|
|
273
|
+
}
|
|
274
|
+
return relight;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const deviceDeflickerPipelineCache = new WeakMap<
|
|
278
|
+
GPUDevice,
|
|
279
|
+
Map<string, TemporalDeflickerPipeline>
|
|
280
|
+
>();
|
|
281
|
+
|
|
282
|
+
function getTemporalDeflickerPipeline(
|
|
283
|
+
device: GPUDevice,
|
|
284
|
+
layerKey: string,
|
|
285
|
+
): TemporalDeflickerPipeline {
|
|
286
|
+
let map = deviceDeflickerPipelineCache.get(device);
|
|
287
|
+
if (!map) {
|
|
288
|
+
map = new Map();
|
|
289
|
+
deviceDeflickerPipelineCache.set(device, map);
|
|
290
|
+
}
|
|
291
|
+
let pipe = map.get(layerKey);
|
|
292
|
+
if (!pipe) {
|
|
293
|
+
pipe = new TemporalDeflickerPipeline(device);
|
|
294
|
+
map.set(layerKey, pipe);
|
|
295
|
+
}
|
|
296
|
+
return pipe;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function resolveVal(
|
|
300
|
+
val: unknown,
|
|
301
|
+
fallback: number,
|
|
302
|
+
frame: number,
|
|
303
|
+
fps: number,
|
|
304
|
+
): number {
|
|
305
|
+
if (val === undefined || val === null) return fallback;
|
|
306
|
+
if (typeof val === "number") return Number.isFinite(val) ? val : fallback;
|
|
307
|
+
if (typeof val === "object" && val !== null) {
|
|
308
|
+
if (
|
|
309
|
+
"get" in (val as Record<string, unknown>) &&
|
|
310
|
+
typeof (val as { get: unknown }).get === "function"
|
|
311
|
+
) {
|
|
312
|
+
try {
|
|
313
|
+
const res = (val as { get: (ctx?: unknown) => unknown }).get({
|
|
314
|
+
frame,
|
|
315
|
+
fps,
|
|
316
|
+
time: fps > 0 ? frame / fps : 0,
|
|
317
|
+
duration: 0,
|
|
318
|
+
durationMs: 0,
|
|
319
|
+
progress: 0,
|
|
320
|
+
deltaTime: fps > 0 ? 1 / fps : 0,
|
|
321
|
+
});
|
|
322
|
+
const n = Number(res);
|
|
323
|
+
if (Number.isFinite(n)) return n;
|
|
324
|
+
} catch {
|
|
325
|
+
// fallback
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if ("value" in (val as Record<string, unknown>)) {
|
|
329
|
+
const n = Number((val as { value: unknown }).value);
|
|
330
|
+
if (Number.isFinite(n)) return n;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (typeof val === "function") {
|
|
334
|
+
try {
|
|
335
|
+
const n = Number(val({ frame, fps, time: fps > 0 ? frame / fps : 0 }));
|
|
336
|
+
if (Number.isFinite(n)) return n;
|
|
337
|
+
} catch {
|
|
338
|
+
// fallback
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const n = Number(val);
|
|
342
|
+
return Number.isFinite(n) ? n : fallback;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function synthesizeRelightLights(
|
|
346
|
+
relightOpts: Record<string, unknown>,
|
|
347
|
+
sceneLights: Light3DOptions[],
|
|
348
|
+
bounds: { x: number; y: number; width: number; height: number },
|
|
349
|
+
shiftedFrame: number,
|
|
350
|
+
fps: number,
|
|
351
|
+
): Light3DOptions[] {
|
|
352
|
+
if (
|
|
353
|
+
sceneLights.length > 0 ||
|
|
354
|
+
(relightOpts.lightType === undefined &&
|
|
355
|
+
relightOpts.intensity === undefined &&
|
|
356
|
+
relightOpts.lightPosX === undefined)
|
|
357
|
+
) {
|
|
358
|
+
return sceneLights;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const lightTypeStr = String(relightOpts.lightType ?? "point").toLowerCase();
|
|
362
|
+
const lightKind: Light3DOptions["lightType"] =
|
|
363
|
+
lightTypeStr === "directional"
|
|
364
|
+
? "directional"
|
|
365
|
+
: lightTypeStr === "spot"
|
|
366
|
+
? "spot"
|
|
367
|
+
: "point";
|
|
368
|
+
|
|
369
|
+
const rawIntensity = relightOpts.intensity ?? 1.0;
|
|
370
|
+
const intensity = resolveVal(rawIntensity, 1.0, shiftedFrame, fps);
|
|
371
|
+
|
|
372
|
+
const normX = resolveVal(relightOpts.lightPosX, 0.5, shiftedFrame, fps);
|
|
373
|
+
const normY = resolveVal(relightOpts.lightPosY, 0.5, shiftedFrame, fps);
|
|
374
|
+
const normZ = resolveVal(relightOpts.lightPosZ, 0.25, shiftedFrame, fps);
|
|
375
|
+
const normRadius = resolveVal(
|
|
376
|
+
relightOpts.lightRadius,
|
|
377
|
+
0.75,
|
|
378
|
+
shiftedFrame,
|
|
379
|
+
fps,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const lightX = bounds.x + normX * bounds.width;
|
|
383
|
+
const lightY = bounds.y + normY * bounds.height;
|
|
384
|
+
const lightZ = normZ * Math.max(bounds.width, bounds.height);
|
|
385
|
+
const lightRadius = normRadius * Math.max(bounds.width, bounds.height);
|
|
386
|
+
|
|
387
|
+
const synthLight: Light3DOptions = {
|
|
388
|
+
lightType: lightKind,
|
|
389
|
+
color: (relightOpts.lightColor as string) ?? "#ffffff",
|
|
390
|
+
intensity,
|
|
391
|
+
x: lightX,
|
|
392
|
+
y: lightY,
|
|
393
|
+
z: lightZ,
|
|
394
|
+
radius: lightRadius,
|
|
395
|
+
decay: 1.0,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const ambientInt = resolveVal(
|
|
399
|
+
relightOpts.ambientIntensity,
|
|
400
|
+
0.15,
|
|
401
|
+
shiftedFrame,
|
|
402
|
+
fps,
|
|
403
|
+
);
|
|
404
|
+
const synthAmbient: Light3DOptions = {
|
|
405
|
+
lightType: "ambient",
|
|
406
|
+
color: (relightOpts.ambientColor as string) ?? "#ffffff",
|
|
407
|
+
intensity: ambientInt,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
return [synthAmbient, synthLight];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function drawProjectedQuad(
|
|
414
|
+
ctx: RenderContextValue,
|
|
415
|
+
encoder: GPUCommandEncoder,
|
|
416
|
+
destView: GPUTextureView,
|
|
417
|
+
sourceTex: GPUTexture,
|
|
418
|
+
quad: Quad2D,
|
|
419
|
+
destWidth: number,
|
|
420
|
+
destHeight: number,
|
|
421
|
+
opacity: number,
|
|
422
|
+
): void {
|
|
423
|
+
const area = Math.abs(Transform3DMath.computeSignedArea(quad));
|
|
424
|
+
if (area < 1e-4) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const minX = Math.max(
|
|
429
|
+
0,
|
|
430
|
+
Math.min(
|
|
431
|
+
destWidth,
|
|
432
|
+
Math.floor(
|
|
433
|
+
Math.min(
|
|
434
|
+
quad.topLeft.x,
|
|
435
|
+
quad.topRight.x,
|
|
436
|
+
quad.bottomLeft.x,
|
|
437
|
+
quad.bottomRight.x,
|
|
438
|
+
),
|
|
439
|
+
),
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
const minY = Math.max(
|
|
443
|
+
0,
|
|
444
|
+
Math.min(
|
|
445
|
+
destHeight,
|
|
446
|
+
Math.floor(
|
|
447
|
+
Math.min(
|
|
448
|
+
quad.topLeft.y,
|
|
449
|
+
quad.topRight.y,
|
|
450
|
+
quad.bottomLeft.y,
|
|
451
|
+
quad.bottomRight.y,
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
const maxX = Math.max(
|
|
457
|
+
0,
|
|
458
|
+
Math.min(
|
|
459
|
+
destWidth,
|
|
460
|
+
Math.ceil(
|
|
461
|
+
Math.max(
|
|
462
|
+
quad.topLeft.x,
|
|
463
|
+
quad.topRight.x,
|
|
464
|
+
quad.bottomLeft.x,
|
|
465
|
+
quad.bottomRight.x,
|
|
466
|
+
),
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
const maxY = Math.max(
|
|
471
|
+
0,
|
|
472
|
+
Math.min(
|
|
473
|
+
destHeight,
|
|
474
|
+
Math.ceil(
|
|
475
|
+
Math.max(
|
|
476
|
+
quad.topLeft.y,
|
|
477
|
+
quad.topRight.y,
|
|
478
|
+
quad.bottomLeft.y,
|
|
479
|
+
quad.bottomRight.y,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const scissorW = maxX - minX;
|
|
486
|
+
const scissorH = maxY - minY;
|
|
487
|
+
if (scissorW <= 0 || scissorH <= 0) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const dstPoints = [
|
|
492
|
+
{ x: quad.topLeft.x / destWidth, y: quad.topLeft.y / destHeight },
|
|
493
|
+
{ x: quad.topRight.x / destWidth, y: quad.topRight.y / destHeight },
|
|
494
|
+
{ x: quad.bottomLeft.x / destWidth, y: quad.bottomLeft.y / destHeight },
|
|
495
|
+
{ x: quad.bottomRight.x / destWidth, y: quad.bottomRight.y / destHeight },
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
const h = solveHomography(dstPoints);
|
|
499
|
+
for (const val of h) {
|
|
500
|
+
if (!Number.isFinite(val)) return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const uniformData = new Float32Array(16);
|
|
504
|
+
uniformData[0] = h[0];
|
|
505
|
+
uniformData[1] = h[1];
|
|
506
|
+
uniformData[2] = h[2];
|
|
507
|
+
uniformData[3] = 0;
|
|
508
|
+
|
|
509
|
+
uniformData[4] = h[3];
|
|
510
|
+
uniformData[5] = h[4];
|
|
511
|
+
uniformData[6] = h[5];
|
|
512
|
+
uniformData[7] = 0;
|
|
513
|
+
|
|
514
|
+
uniformData[8] = h[6];
|
|
515
|
+
uniformData[9] = h[7];
|
|
516
|
+
uniformData[10] = h[8];
|
|
517
|
+
uniformData[11] = 0;
|
|
518
|
+
|
|
519
|
+
uniformData[12] = opacity;
|
|
520
|
+
uniformData[13] = 0;
|
|
521
|
+
uniformData[14] = 0;
|
|
522
|
+
uniformData[15] = 0;
|
|
523
|
+
|
|
524
|
+
const { pipeline, uniformLayout, textureLayout } = getHomographyResources(
|
|
525
|
+
ctx.device,
|
|
526
|
+
ctx.renderer.format,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const sampler = ctx.renderer.samplerCache.getSampler(ctx.device);
|
|
530
|
+
const uniformBuffer = ctx.renderer.getTemporaryBuffer(uniformData);
|
|
531
|
+
|
|
532
|
+
const uniformBindGroup = ctx.device.createBindGroup({
|
|
533
|
+
layout: uniformLayout,
|
|
534
|
+
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const textureBindGroup = ctx.renderer.bindGroupCache.getBindGroup(
|
|
538
|
+
ctx.device,
|
|
539
|
+
textureLayout,
|
|
540
|
+
sourceTex,
|
|
541
|
+
sampler,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const renderPass = encoder.beginRenderPass({
|
|
545
|
+
colorAttachments: [
|
|
546
|
+
{
|
|
547
|
+
view: destView,
|
|
548
|
+
loadOp: "load",
|
|
549
|
+
storeOp: "store",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
renderPass.setPipeline(pipeline);
|
|
555
|
+
renderPass.setScissorRect(minX, minY, scissorW, scissorH);
|
|
556
|
+
renderPass.setBindGroup(0, uniformBindGroup);
|
|
557
|
+
renderPass.setBindGroup(1, textureBindGroup);
|
|
558
|
+
renderPass.draw(4);
|
|
559
|
+
renderPass.end();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function computeContainerScissor(
|
|
563
|
+
matrix: DOMMatrix,
|
|
564
|
+
width: number,
|
|
565
|
+
height: number,
|
|
566
|
+
): Rect {
|
|
567
|
+
const corners = [
|
|
568
|
+
matrix.transformPoint(new DOMPoint(0, 0)),
|
|
569
|
+
matrix.transformPoint(new DOMPoint(width, 0)),
|
|
570
|
+
matrix.transformPoint(new DOMPoint(0, height)),
|
|
571
|
+
matrix.transformPoint(new DOMPoint(width, height)),
|
|
572
|
+
];
|
|
573
|
+
const xs = corners.map((p) => p.x);
|
|
574
|
+
const ys = corners.map((p) => p.y);
|
|
575
|
+
const minX = Math.min(...xs);
|
|
576
|
+
const minY = Math.min(...ys);
|
|
577
|
+
const maxX = Math.max(...xs);
|
|
578
|
+
const maxY = Math.max(...ys);
|
|
579
|
+
return {
|
|
580
|
+
x: Math.round(minX),
|
|
581
|
+
y: Math.round(minY),
|
|
582
|
+
width: Math.max(0, Math.round(maxX - minX)),
|
|
583
|
+
height: Math.max(0, Math.round(maxY - minY)),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export const CompositorWebGPURenderer: WebGPUNodeRenderer = async (args) => {
|
|
588
|
+
const {
|
|
589
|
+
ctx,
|
|
590
|
+
pass,
|
|
591
|
+
targetView,
|
|
592
|
+
targetTexture,
|
|
593
|
+
targetWidth,
|
|
594
|
+
targetHeight,
|
|
595
|
+
encoder,
|
|
596
|
+
props,
|
|
597
|
+
drawChild,
|
|
598
|
+
} = args;
|
|
599
|
+
const { virtualMedia, fps, frame } = props;
|
|
600
|
+
const op = virtualMedia.operation as any;
|
|
601
|
+
|
|
602
|
+
if (!op) return;
|
|
603
|
+
|
|
604
|
+
// Always end the incoming pass as we will be creating our own
|
|
605
|
+
pass.end();
|
|
606
|
+
|
|
607
|
+
if (op.op === "CompositorLayer") {
|
|
608
|
+
console.warn(
|
|
609
|
+
"CompositorWebGPURenderer called directly with legacy CompositorLayer operation. This is a bug.",
|
|
610
|
+
);
|
|
611
|
+
(args as any).pass = ctx.renderer.beginFrame(
|
|
612
|
+
encoder,
|
|
613
|
+
targetView,
|
|
614
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
615
|
+
targetWidth,
|
|
616
|
+
targetHeight,
|
|
617
|
+
"load",
|
|
618
|
+
);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (op.op === "Compositor") {
|
|
623
|
+
const vop = op as CompositorOperation;
|
|
624
|
+
// 4x MSAA in the 3D pass unless the document opts out (antialias3d: false).
|
|
625
|
+
const sampleCount3d = vop.antialias3d === false ? 1 : 4;
|
|
626
|
+
const containerWidth = props.containerWidth ?? ctx.surface.width;
|
|
627
|
+
const containerHeight = props.containerHeight ?? ctx.surface.height;
|
|
628
|
+
|
|
629
|
+
const nativeWidth = vop.width ?? containerWidth;
|
|
630
|
+
const nativeHeight = vop.height ?? containerHeight;
|
|
631
|
+
const opacity =
|
|
632
|
+
typeof (vop as any).opacity === "number" ? (vop as any).opacity : 1;
|
|
633
|
+
|
|
634
|
+
const isVideoMode =
|
|
635
|
+
props.isVideoMode ??
|
|
636
|
+
(vop.mode === "Video" ||
|
|
637
|
+
vop.dataType === "Video" ||
|
|
638
|
+
virtualMedia?.metadata?.durationMs != null);
|
|
639
|
+
|
|
640
|
+
const inheritedSeekOffset = props.inheritedSeekOffset ?? 0;
|
|
641
|
+
const shiftedFrame = Math.max(
|
|
642
|
+
0,
|
|
643
|
+
frame != null && fps != null
|
|
644
|
+
? frame + Math.round(inheritedSeekOffset * fps)
|
|
645
|
+
: (frame ?? 0),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// 1. Create a temporary texture at native resolution
|
|
649
|
+
const compTex = ctx.renderer.getTemporaryTexture(
|
|
650
|
+
nativeWidth,
|
|
651
|
+
nativeHeight,
|
|
652
|
+
[...(props.excludeTextures || []), targetTexture],
|
|
653
|
+
);
|
|
654
|
+
const compView = compTex.createView();
|
|
655
|
+
|
|
656
|
+
const composeDuration =
|
|
657
|
+
virtualMedia?.metadata?.durationMs ?? DEFAULT_DURATION_MS;
|
|
658
|
+
const durationSec = composeDuration / 1000;
|
|
659
|
+
const { tl, targetsById } = compileTimeline(
|
|
660
|
+
props.renderId,
|
|
661
|
+
virtualMedia as any,
|
|
662
|
+
{ fps: fps ?? 24, durationSec },
|
|
663
|
+
);
|
|
664
|
+
tl.seek(shiftedFrame / (fps ?? 24));
|
|
665
|
+
|
|
666
|
+
const r3d = getRenderer3D(
|
|
667
|
+
ctx.device,
|
|
668
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
669
|
+
sampleCount3d,
|
|
670
|
+
);
|
|
671
|
+
r3d.resetPools();
|
|
672
|
+
|
|
673
|
+
const relightPipe = getScreenSpaceRelightPipeline(
|
|
674
|
+
ctx.device,
|
|
675
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
676
|
+
);
|
|
677
|
+
relightPipe.resetPools();
|
|
678
|
+
|
|
679
|
+
// 2. Initial clear
|
|
680
|
+
const clearColor = parseColor(vop.backgroundColor);
|
|
681
|
+
const clearPass = ctx.renderer.beginFrame(
|
|
682
|
+
encoder,
|
|
683
|
+
compView,
|
|
684
|
+
clearColor,
|
|
685
|
+
nativeWidth,
|
|
686
|
+
nativeHeight,
|
|
687
|
+
"clear",
|
|
688
|
+
);
|
|
689
|
+
clearPass.end();
|
|
690
|
+
|
|
691
|
+
// 3. Draw all children into the native texture
|
|
692
|
+
let currentCompTex = compTex;
|
|
693
|
+
|
|
694
|
+
ctx.renderer.pushScissor({
|
|
695
|
+
x: 0,
|
|
696
|
+
y: 0,
|
|
697
|
+
width: nativeWidth,
|
|
698
|
+
height: nativeHeight,
|
|
699
|
+
});
|
|
700
|
+
ctx.renderer.pushIdentity();
|
|
701
|
+
// 3. Layout pass — deterministic geometry from the document (v2).
|
|
702
|
+
// Animated layout props (width/height/gap/padding) are sampled per
|
|
703
|
+
// frame from the compiled timeline into the tree via targetsById.
|
|
704
|
+
await initLayout();
|
|
705
|
+
const mediaDims = new Map<string, { width: number; height: number }>();
|
|
706
|
+
for (const vm of collectNodeOps(virtualMedia.children)) {
|
|
707
|
+
const lop = vm.operation as any;
|
|
708
|
+
if (lop.kind === "media") {
|
|
709
|
+
const meta = getActiveMediaMetadata(vm.children?.[0] as any);
|
|
710
|
+
if (meta?.width && meta?.height) {
|
|
711
|
+
mediaDims.set(lop.id, { width: meta.width, height: meta.height });
|
|
712
|
+
} else if (
|
|
713
|
+
lop.dataType === "Caption" ||
|
|
714
|
+
(vm.children?.[0]?.operation as any)?.dataType === "Caption" ||
|
|
715
|
+
(vm.children?.[0]?.operation as any)?.srtText !== undefined
|
|
716
|
+
) {
|
|
717
|
+
const lopWidth = targetsById[lop.id]?.width ?? lop.width;
|
|
718
|
+
const autoWidth =
|
|
719
|
+
typeof lopWidth === "number"
|
|
720
|
+
? lopWidth
|
|
721
|
+
: Math.round(nativeWidth * 0.8);
|
|
722
|
+
const lopHeight = targetsById[lop.id]?.height ?? lop.height;
|
|
723
|
+
let autoHeight = typeof lopHeight === "number" ? lopHeight : 160;
|
|
724
|
+
const contentVM = vm.children?.[0];
|
|
725
|
+
const src =
|
|
726
|
+
resolveMediaSourceUrl(contentVM) ??
|
|
727
|
+
(contentVM?.operation as any)?.url ??
|
|
728
|
+
(contentVM?.operation as any)?.src ??
|
|
729
|
+
(contentVM?.operation as any)?.srtText ??
|
|
730
|
+
lop.src ??
|
|
731
|
+
lop.url;
|
|
732
|
+
if (typeof lopHeight !== "number" && src) {
|
|
733
|
+
try {
|
|
734
|
+
const captions = await srtLoader.load(src);
|
|
735
|
+
if (captions && captions.length > 0) {
|
|
736
|
+
autoHeight = measureMaxCaptionHeight(captions, lop, autoWidth);
|
|
737
|
+
}
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
mediaDims.set(lop.id, { width: autoWidth, height: autoHeight });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const frameResolveNode = (vm: any): LayoutNode => {
|
|
745
|
+
const lop = vm?.operation ?? {};
|
|
746
|
+
const target = targetsById[lop.id];
|
|
747
|
+
const pick = (k: string) =>
|
|
748
|
+
target && target[k] !== undefined ? target[k] : lop[k];
|
|
749
|
+
const node: any = {
|
|
750
|
+
...lop,
|
|
751
|
+
width: pick("width"),
|
|
752
|
+
height: pick("height"),
|
|
753
|
+
gap: pick("gap"),
|
|
754
|
+
padding: pick("padding"),
|
|
755
|
+
fontSize: pick("fontSize"),
|
|
756
|
+
letterSpacing: pick("letterSpacing"),
|
|
757
|
+
};
|
|
758
|
+
node.children = (vm?.children ?? []).map(frameResolveNode);
|
|
759
|
+
if (!node.children.length) node.children = undefined;
|
|
760
|
+
return node;
|
|
761
|
+
};
|
|
762
|
+
const layoutRoots = (): LayoutNode[] =>
|
|
763
|
+
(virtualMedia.children ?? [])
|
|
764
|
+
.map(frameResolveNode)
|
|
765
|
+
.filter(
|
|
766
|
+
(node: any) => node?.kind !== "camera" && node?.kind !== "light",
|
|
767
|
+
);
|
|
768
|
+
const memoTree = virtualMedia.children as object | undefined;
|
|
769
|
+
const memoKey = layoutKey(
|
|
770
|
+
nativeWidth,
|
|
771
|
+
nativeHeight,
|
|
772
|
+
targetsById,
|
|
773
|
+
mediaDims,
|
|
774
|
+
);
|
|
775
|
+
const memo = memoTree ? layoutMemo.get(memoTree) : undefined;
|
|
776
|
+
const { rects } =
|
|
777
|
+
memo?.key === memoKey
|
|
778
|
+
? memo
|
|
779
|
+
: await computeLayout({
|
|
780
|
+
layout: layoutRoots(),
|
|
781
|
+
viewport: { width: nativeWidth, height: nativeHeight },
|
|
782
|
+
measure: (node, constraintWidth) => {
|
|
783
|
+
if (node.kind === "text") {
|
|
784
|
+
const textStyle = {
|
|
785
|
+
fontSize: node.fontSize ?? 48,
|
|
786
|
+
fontWeight: node.fontWeight,
|
|
787
|
+
fontFamily: node.fontFamily,
|
|
788
|
+
letterSpacing: node.letterSpacing,
|
|
789
|
+
lineHeight: node.lineHeight,
|
|
790
|
+
padding: node.padding,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
if (constraintWidth !== undefined) {
|
|
794
|
+
const isAuto = typeof node.width !== "number";
|
|
795
|
+
const wrapped = measureText(node.text ?? "", {
|
|
796
|
+
...textStyle,
|
|
797
|
+
width: isAuto ? undefined : constraintWidth,
|
|
798
|
+
keepNaturalWidth: true,
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
width: isAuto
|
|
802
|
+
? Math.ceil(wrapped.width)
|
|
803
|
+
: Math.min(constraintWidth, Math.ceil(wrapped.width)),
|
|
804
|
+
height: Math.ceil(wrapped.height),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const natural = measureText(node.text ?? "", textStyle);
|
|
809
|
+
if (typeof node.width !== "number") {
|
|
810
|
+
return {
|
|
811
|
+
width: Math.ceil(natural.width),
|
|
812
|
+
height: Math.ceil(natural.height),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const wrapped = measureText(node.text ?? "", {
|
|
816
|
+
...textStyle,
|
|
817
|
+
width: node.width,
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
width: Math.ceil(wrapped.width),
|
|
821
|
+
height: Math.ceil(wrapped.height),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (node.kind === "media") return mediaDims.get(node.id) ?? null;
|
|
825
|
+
return null;
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
if (memoTree && memo?.key !== memoKey)
|
|
829
|
+
layoutMemo.set(memoTree, { key: memoKey, rects });
|
|
830
|
+
// 4. 3D Camera and Multiplane Setup
|
|
831
|
+
const allNodeOps = collectNodeOps(virtualMedia.children ?? []);
|
|
832
|
+
// A film can cut between cameras: the active one is the camera whose
|
|
833
|
+
// window holds this frame, and the latest to start wins (a cut list).
|
|
834
|
+
const cameraVMs = allNodeOps.filter(
|
|
835
|
+
(vm) => (vm.operation as any)?.kind === "camera",
|
|
836
|
+
);
|
|
837
|
+
const cameraVM = isVideoMode
|
|
838
|
+
? cameraVMs
|
|
839
|
+
.filter((vm) => {
|
|
840
|
+
const op = vm.operation as any;
|
|
841
|
+
const start = op.startFrame ?? 0;
|
|
842
|
+
const dur = op.durationFrames;
|
|
843
|
+
return (
|
|
844
|
+
shiftedFrame >= start &&
|
|
845
|
+
(dur === undefined || shiftedFrame < start + dur)
|
|
846
|
+
);
|
|
847
|
+
})
|
|
848
|
+
.sort(
|
|
849
|
+
(a, b) =>
|
|
850
|
+
((b.operation as any).startFrame ?? 0) -
|
|
851
|
+
((a.operation as any).startFrame ?? 0),
|
|
852
|
+
)[0]
|
|
853
|
+
: cameraVMs[0];
|
|
854
|
+
const lightVMs = allNodeOps.filter(
|
|
855
|
+
(vm) => (vm.operation as any)?.kind === "light",
|
|
856
|
+
);
|
|
857
|
+
const has3DLayer = allNodeOps.some((vm) => {
|
|
858
|
+
const lop = vm.operation as any;
|
|
859
|
+
const target = lop.id ? targetsById[lop.id] : undefined;
|
|
860
|
+
return (
|
|
861
|
+
lop.kind !== "camera" &&
|
|
862
|
+
lop.kind !== "light" &&
|
|
863
|
+
(Boolean(target?.is3D ?? lop.is3D) || (target?.z ?? lop.z ?? 0) !== 0)
|
|
864
|
+
);
|
|
865
|
+
});
|
|
866
|
+
const is3DScene = Boolean(cameraVM || has3DLayer || lightVMs.length > 0);
|
|
867
|
+
|
|
868
|
+
let camera: Camera3DPose | null = null;
|
|
869
|
+
let dofEnabled = false;
|
|
870
|
+
const sceneLights: Light3DOptions[] = [];
|
|
871
|
+
|
|
872
|
+
if (is3DScene) {
|
|
873
|
+
const camOp = cameraVM?.operation as any;
|
|
874
|
+
const camStub = camOp?.id ? targetsById[camOp.id] : undefined;
|
|
875
|
+
const fov = camStub?.cameraFov ?? camOp?.lens?.fov ?? 50;
|
|
876
|
+
camera = Camera3D.createDefaultCamera(nativeWidth, nativeHeight, fov);
|
|
877
|
+
camera.near = camOp?.lens?.near ?? 1.0;
|
|
878
|
+
camera.far = camOp?.lens?.far ?? 50000.0;
|
|
879
|
+
camera.zoom = camStub?.cameraZoom ?? camOp?.lens?.zoom ?? 1.0;
|
|
880
|
+
|
|
881
|
+
const defaultEye = camera.eye;
|
|
882
|
+
const defaultTarget = camera.target;
|
|
883
|
+
|
|
884
|
+
// Eye
|
|
885
|
+
const eyeX = camStub?.cameraX ?? camStub?.x ?? camOp?.x ?? defaultEye[0];
|
|
886
|
+
const eyeY = camStub?.cameraY ?? camStub?.y ?? camOp?.y ?? defaultEye[1];
|
|
887
|
+
const eyeZ = camStub?.cameraZ ?? camStub?.z ?? camOp?.z ?? defaultEye[2];
|
|
888
|
+
camera.eye = [eyeX, eyeY, eyeZ];
|
|
889
|
+
|
|
890
|
+
// Target
|
|
891
|
+
const hasCustomTarget =
|
|
892
|
+
camStub?.targetX !== undefined ||
|
|
893
|
+
camStub?.targetY !== undefined ||
|
|
894
|
+
camStub?.targetZ !== undefined ||
|
|
895
|
+
camOp?.targetX !== undefined ||
|
|
896
|
+
camOp?.targetY !== undefined ||
|
|
897
|
+
camOp?.targetZ !== undefined;
|
|
898
|
+
if (hasCustomTarget) {
|
|
899
|
+
camera.target = [
|
|
900
|
+
camStub?.targetX ?? camOp?.targetX ?? defaultTarget[0],
|
|
901
|
+
camStub?.targetY ?? camOp?.targetY ?? defaultTarget[1],
|
|
902
|
+
camStub?.targetZ ?? camOp?.targetZ ?? defaultTarget[2],
|
|
903
|
+
];
|
|
904
|
+
} else {
|
|
905
|
+
// Pure truck / pedestal shifts target along with eye
|
|
906
|
+
camera.target = [eyeX, eyeY, 0];
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Orientation (Pitch / Yaw / Roll)
|
|
910
|
+
const pitch = camStub?.cameraPitch ?? camOp?.pitch ?? 0;
|
|
911
|
+
const yaw = camStub?.cameraYaw ?? camOp?.yaw ?? 0;
|
|
912
|
+
const roll = camStub?.cameraRoll ?? camOp?.roll ?? 0;
|
|
913
|
+
if (pitch !== 0 || yaw !== 0 || roll !== 0) {
|
|
914
|
+
camera = Camera3D.applyOrientation(camera, pitch, yaw, roll);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Orbit turntable
|
|
918
|
+
const orbitAzimuth = camStub?.orbitAzimuth;
|
|
919
|
+
const orbitElevation = camStub?.orbitElevation;
|
|
920
|
+
const orbitRadius = camStub?.orbitRadius;
|
|
921
|
+
if (
|
|
922
|
+
orbitAzimuth !== undefined ||
|
|
923
|
+
orbitElevation !== undefined ||
|
|
924
|
+
orbitRadius !== undefined
|
|
925
|
+
) {
|
|
926
|
+
const radius =
|
|
927
|
+
orbitRadius ?? Vector3Math.distance(camera.eye, camera.target);
|
|
928
|
+
camera.eye = Camera3D.orbit(
|
|
929
|
+
camera.target,
|
|
930
|
+
radius,
|
|
931
|
+
orbitAzimuth ?? 0,
|
|
932
|
+
orbitElevation ?? 0,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Procedural Handheld Shake
|
|
937
|
+
const shakeTrans =
|
|
938
|
+
camStub?.shakeTranslation ??
|
|
939
|
+
(camOp?.shake?.enabled ? camOp?.shake?.translationAmplitude : 0) ??
|
|
940
|
+
0;
|
|
941
|
+
const shakeRot =
|
|
942
|
+
camStub?.shakeRotation ??
|
|
943
|
+
(camOp?.shake?.enabled ? camOp?.shake?.rotationAmplitude : 0) ??
|
|
944
|
+
0;
|
|
945
|
+
if (shakeTrans > 0 || shakeRot > 0) {
|
|
946
|
+
const t = shiftedFrame / (fps ?? 24);
|
|
947
|
+
camera = Camera3D.applyCameraShake(camera, t, {
|
|
948
|
+
translationAmplitude: shakeTrans,
|
|
949
|
+
rotationAmplitude: shakeRot,
|
|
950
|
+
frequency: camOp?.shake?.frequency ?? 1.5,
|
|
951
|
+
octaves: camOp?.shake?.octaves ?? 3,
|
|
952
|
+
seed: camOp?.shake?.seed ?? 42,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Depth of Field
|
|
957
|
+
dofEnabled = Boolean(
|
|
958
|
+
camOp?.dof?.enabled || camStub?.focusDistance !== undefined,
|
|
959
|
+
);
|
|
960
|
+
if (dofEnabled) {
|
|
961
|
+
camera.focusDistance =
|
|
962
|
+
camStub?.focusDistance ??
|
|
963
|
+
camOp?.dof?.focusDistance ??
|
|
964
|
+
Vector3Math.distance(camera.eye, camera.target);
|
|
965
|
+
camera.fStop = camStub?.fStop ?? camOp?.dof?.fStop ?? 2.8;
|
|
966
|
+
camera.maxBlurRadius =
|
|
967
|
+
camStub?.maxBlurRadius ?? camOp?.dof?.maxBlurRadius ?? 24;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Collect Active Lights
|
|
971
|
+
for (const lvm of lightVMs) {
|
|
972
|
+
const lop = lvm.operation as any;
|
|
973
|
+
const targetId = lop.id || lop.inputHandleId;
|
|
974
|
+
const target = targetId ? targetsById[targetId] : undefined;
|
|
975
|
+
if (target?.hidden || lop.hidden) continue;
|
|
976
|
+
|
|
977
|
+
if (isVideoMode) {
|
|
978
|
+
const startFrame = lop.startFrame ?? 0;
|
|
979
|
+
const durationFrames = lop.durationFrames;
|
|
980
|
+
if (shiftedFrame < startFrame) continue;
|
|
981
|
+
if (
|
|
982
|
+
durationFrames !== undefined &&
|
|
983
|
+
shiftedFrame >= startFrame + durationFrames
|
|
984
|
+
) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const lightType = (target?.lightType ?? lop.lightType ?? "point") as
|
|
990
|
+
| "ambient"
|
|
991
|
+
| "directional"
|
|
992
|
+
| "point"
|
|
993
|
+
| "spot";
|
|
994
|
+
const rawColor = target?.color ?? lop.color ?? "#ffffff";
|
|
995
|
+
const color = parseColor(rawColor);
|
|
996
|
+
const intensity = target?.intensity ?? lop.intensity ?? 1.0;
|
|
997
|
+
const x = target?.x ?? lop.x ?? 0;
|
|
998
|
+
const y = target?.y ?? lop.y ?? 0;
|
|
999
|
+
const z = target?.z ?? lop.z ?? 0;
|
|
1000
|
+
const targetX = target?.targetX ?? lop.targetX ?? 0;
|
|
1001
|
+
const targetY = target?.targetY ?? lop.targetY ?? 0;
|
|
1002
|
+
const targetZ = target?.targetZ ?? lop.targetZ ?? 0;
|
|
1003
|
+
const radius = target?.radius ?? lop.radius ?? 1000;
|
|
1004
|
+
const angle = target?.angle ?? lop.angle ?? 45;
|
|
1005
|
+
const penumbra = target?.penumbra ?? lop.penumbra ?? 0.2;
|
|
1006
|
+
const decay = target?.decay ?? lop.decay ?? 1;
|
|
1007
|
+
|
|
1008
|
+
sceneLights.push({
|
|
1009
|
+
type: lightType,
|
|
1010
|
+
color,
|
|
1011
|
+
intensity,
|
|
1012
|
+
position: [x, y, z],
|
|
1013
|
+
target: [targetX, targetY, targetZ],
|
|
1014
|
+
radius,
|
|
1015
|
+
angle,
|
|
1016
|
+
penumbra,
|
|
1017
|
+
decay,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
type Pending3DItem =
|
|
1023
|
+
| { kind: "quad"; texture: GPUTexture; opts: DrawQuad3DOpts }
|
|
1024
|
+
| {
|
|
1025
|
+
// A text layer's glyphs, drawn natively on its plane.
|
|
1026
|
+
kind: "slug";
|
|
1027
|
+
texture?: undefined;
|
|
1028
|
+
batches: SlugGlyphBatch[];
|
|
1029
|
+
opts: DrawQuad3DOpts;
|
|
1030
|
+
/** Passes already on this plane (its backdrop quad) */
|
|
1031
|
+
stackBase: number;
|
|
1032
|
+
}
|
|
1033
|
+
| {
|
|
1034
|
+
kind: "mesh";
|
|
1035
|
+
mesh: Mesh3DData;
|
|
1036
|
+
texture?: GPUTexture;
|
|
1037
|
+
opts: DrawMesh3DOpts;
|
|
1038
|
+
meshBuffers?: GPUMeshBuffers;
|
|
1039
|
+
};
|
|
1040
|
+
const pending3DItems: Pending3DItem[] = [];
|
|
1041
|
+
|
|
1042
|
+
const flush3DPass = (
|
|
1043
|
+
curTex: GPUTexture,
|
|
1044
|
+
curView: GPUTextureView,
|
|
1045
|
+
curExcludes: GPUTexture[],
|
|
1046
|
+
): { finalTex: GPUTexture; finalView: GPUTextureView } => {
|
|
1047
|
+
if (pending3DItems.length === 0 || !camera) {
|
|
1048
|
+
return { finalTex: curTex, finalView: curView };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const r3d = getRenderer3D(
|
|
1052
|
+
ctx.device,
|
|
1053
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
1054
|
+
sampleCount3d,
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
// Execute audio mesh deformation compute passes before beginning render pass
|
|
1058
|
+
for (const item of pending3DItems) {
|
|
1059
|
+
if (item.kind === "mesh" && item.opts.audioDeform) {
|
|
1060
|
+
item.meshBuffers = r3d.deformMesh(
|
|
1061
|
+
encoder,
|
|
1062
|
+
item.mesh,
|
|
1063
|
+
item.opts.audioDeform,
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const itemTextures = pending3DItems
|
|
1069
|
+
.map((q) => (q.kind === "quad" ? q.texture : q.texture))
|
|
1070
|
+
.filter((t): t is GPUTexture => Boolean(t));
|
|
1071
|
+
|
|
1072
|
+
const scene3dTex = ctx.renderer.getTemporaryTexture(
|
|
1073
|
+
nativeWidth,
|
|
1074
|
+
nativeHeight,
|
|
1075
|
+
[curTex, targetTexture, ...curExcludes, ...itemTextures],
|
|
1076
|
+
);
|
|
1077
|
+
const scene3dView = scene3dTex.createView();
|
|
1078
|
+
|
|
1079
|
+
let linearDepthTex: GPUTexture | undefined;
|
|
1080
|
+
let linearDepthView: GPUTextureView | undefined;
|
|
1081
|
+
if (dofEnabled) {
|
|
1082
|
+
linearDepthTex = r3d.getOrCreateLinearDepthTexture(
|
|
1083
|
+
nativeWidth,
|
|
1084
|
+
nativeHeight,
|
|
1085
|
+
);
|
|
1086
|
+
linearDepthView = linearDepthTex.createView();
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const {
|
|
1090
|
+
pass: pass3d,
|
|
1091
|
+
cameraBindGroup,
|
|
1092
|
+
cameraBuffer,
|
|
1093
|
+
lightsBindGroup,
|
|
1094
|
+
} = r3d.beginPass(
|
|
1095
|
+
encoder,
|
|
1096
|
+
scene3dView,
|
|
1097
|
+
nativeWidth,
|
|
1098
|
+
nativeHeight,
|
|
1099
|
+
camera,
|
|
1100
|
+
"clear",
|
|
1101
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
1102
|
+
linearDepthView,
|
|
1103
|
+
sceneLights,
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
for (const item of pending3DItems) {
|
|
1107
|
+
if (item.kind === "quad") {
|
|
1108
|
+
r3d.drawTextureQuad(
|
|
1109
|
+
pass3d,
|
|
1110
|
+
cameraBindGroup,
|
|
1111
|
+
lightsBindGroup,
|
|
1112
|
+
item.texture,
|
|
1113
|
+
item.opts,
|
|
1114
|
+
dofEnabled,
|
|
1115
|
+
);
|
|
1116
|
+
} else if (item.kind === "slug") {
|
|
1117
|
+
r3d.drawSlugText(
|
|
1118
|
+
pass3d,
|
|
1119
|
+
cameraBindGroup,
|
|
1120
|
+
lightsBindGroup,
|
|
1121
|
+
item.batches,
|
|
1122
|
+
item.opts,
|
|
1123
|
+
item.stackBase,
|
|
1124
|
+
dofEnabled,
|
|
1125
|
+
);
|
|
1126
|
+
} else if (item.kind === "mesh") {
|
|
1127
|
+
r3d.drawMesh3D(
|
|
1128
|
+
pass3d,
|
|
1129
|
+
cameraBindGroup,
|
|
1130
|
+
lightsBindGroup,
|
|
1131
|
+
item.meshBuffers ?? item.mesh,
|
|
1132
|
+
item.texture,
|
|
1133
|
+
item.opts,
|
|
1134
|
+
dofEnabled,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
pass3d.end();
|
|
1140
|
+
|
|
1141
|
+
let final3dTex = scene3dTex;
|
|
1142
|
+
if (dofEnabled && linearDepthTex) {
|
|
1143
|
+
const dofResultTex = ctx.renderer.getTemporaryTexture(
|
|
1144
|
+
nativeWidth,
|
|
1145
|
+
nativeHeight,
|
|
1146
|
+
[curTex, targetTexture, scene3dTex, ...curExcludes],
|
|
1147
|
+
);
|
|
1148
|
+
r3d.applyDepthOfField(
|
|
1149
|
+
encoder,
|
|
1150
|
+
scene3dTex,
|
|
1151
|
+
linearDepthTex,
|
|
1152
|
+
dofResultTex.createView(),
|
|
1153
|
+
cameraBuffer,
|
|
1154
|
+
);
|
|
1155
|
+
final3dTex = dofResultTex;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const nextTex = ctx.renderer.composite(
|
|
1159
|
+
encoder,
|
|
1160
|
+
curTex,
|
|
1161
|
+
final3dTex,
|
|
1162
|
+
"normal",
|
|
1163
|
+
[targetTexture, ...curExcludes],
|
|
1164
|
+
);
|
|
1165
|
+
pending3DItems.length = 0;
|
|
1166
|
+
return { finalTex: nextTex, finalView: nextTex.createView() };
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// 5. Draw the node tree into the native texture (z-sorted per level).
|
|
1170
|
+
let nodeIndex = 0;
|
|
1171
|
+
|
|
1172
|
+
const renderNodeTree = async (
|
|
1173
|
+
vms: VirtualMediaData[],
|
|
1174
|
+
initialTex: GPUTexture,
|
|
1175
|
+
initialView: GPUTextureView,
|
|
1176
|
+
excludes: GPUTexture[],
|
|
1177
|
+
accumulatedOpacity = 1.0,
|
|
1178
|
+
parentMatrix = new DOMMatrix(),
|
|
1179
|
+
parentRect: Rect | null = null,
|
|
1180
|
+
targetW: number = nativeWidth,
|
|
1181
|
+
targetH: number = nativeHeight,
|
|
1182
|
+
parent3DMatrix: Mat4 | null = null,
|
|
1183
|
+
isRootScene = false,
|
|
1184
|
+
): Promise<{ finalTex: GPUTexture; finalView: GPUTextureView }> => {
|
|
1185
|
+
const sorted = [...vms].sort((a, b) => {
|
|
1186
|
+
const za = (a.operation as any)?.zIndex ?? 0;
|
|
1187
|
+
const zb = (b.operation as any)?.zIndex ?? 0;
|
|
1188
|
+
return za - zb;
|
|
1189
|
+
});
|
|
1190
|
+
let activeTex = initialTex;
|
|
1191
|
+
let activeView = initialView;
|
|
1192
|
+
let activeExcludes = excludes;
|
|
1193
|
+
for (const child of sorted) {
|
|
1194
|
+
const lop = child.operation as any;
|
|
1195
|
+
if (lop.kind === "camera" || lop.kind === "light") continue;
|
|
1196
|
+
const targetId = lop.id || lop.inputHandleId;
|
|
1197
|
+
const target = targetId ? targetsById[targetId] : undefined;
|
|
1198
|
+
const isChild3D =
|
|
1199
|
+
Boolean(target?.is3D ?? lop.is3D) ||
|
|
1200
|
+
Boolean(camera && ((target?.z ?? lop.z ?? 0) !== 0 || lop.is3D)) ||
|
|
1201
|
+
Boolean(parent3DMatrix);
|
|
1202
|
+
|
|
1203
|
+
if (isRootScene && !isChild3D && pending3DItems.length > 0) {
|
|
1204
|
+
const flushed = flush3DPass(activeTex, activeView, activeExcludes);
|
|
1205
|
+
if (flushed.finalTex !== activeTex) {
|
|
1206
|
+
activeExcludes = [...activeExcludes, flushed.finalTex];
|
|
1207
|
+
}
|
|
1208
|
+
activeTex = flushed.finalTex;
|
|
1209
|
+
activeView = flushed.finalView;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
if (lop.op !== "CompositorLayer") {
|
|
1213
|
+
await drawChild(
|
|
1214
|
+
child,
|
|
1215
|
+
{
|
|
1216
|
+
containerWidth: targetW,
|
|
1217
|
+
containerHeight: targetH,
|
|
1218
|
+
renderId: `${props.renderId}-c${nodeIndex}-${lop.op}`,
|
|
1219
|
+
fps,
|
|
1220
|
+
isVideoMode,
|
|
1221
|
+
frame: shiftedFrame,
|
|
1222
|
+
// The child's effect passes must not borrow textures still in use here.
|
|
1223
|
+
excludeTextures: [targetTexture, ...activeExcludes],
|
|
1224
|
+
},
|
|
1225
|
+
activeView,
|
|
1226
|
+
activeTex,
|
|
1227
|
+
targetW,
|
|
1228
|
+
targetH,
|
|
1229
|
+
);
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
nodeIndex += 1;
|
|
1233
|
+
const res = await renderLayerVM(
|
|
1234
|
+
child,
|
|
1235
|
+
nodeIndex,
|
|
1236
|
+
activeTex,
|
|
1237
|
+
activeView,
|
|
1238
|
+
activeExcludes,
|
|
1239
|
+
accumulatedOpacity,
|
|
1240
|
+
parentMatrix,
|
|
1241
|
+
parentRect,
|
|
1242
|
+
targetW,
|
|
1243
|
+
targetH,
|
|
1244
|
+
parent3DMatrix,
|
|
1245
|
+
isRootScene,
|
|
1246
|
+
);
|
|
1247
|
+
if (res) {
|
|
1248
|
+
if (res.finalTex !== activeTex) {
|
|
1249
|
+
activeExcludes = [...activeExcludes, res.finalTex];
|
|
1250
|
+
}
|
|
1251
|
+
activeTex = res.finalTex;
|
|
1252
|
+
activeView = res.finalView;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (isRootScene && pending3DItems.length > 0) {
|
|
1257
|
+
const flushed = flush3DPass(activeTex, activeView, activeExcludes);
|
|
1258
|
+
if (flushed.finalTex !== activeTex) {
|
|
1259
|
+
activeExcludes = [...activeExcludes, flushed.finalTex];
|
|
1260
|
+
}
|
|
1261
|
+
activeTex = flushed.finalTex;
|
|
1262
|
+
activeView = flushed.finalView;
|
|
1263
|
+
}
|
|
1264
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const renderLayerVM = async (
|
|
1268
|
+
nodeVM: VirtualMediaData,
|
|
1269
|
+
i: number,
|
|
1270
|
+
destTex: GPUTexture,
|
|
1271
|
+
destView: GPUTextureView,
|
|
1272
|
+
baseExcludes: GPUTexture[],
|
|
1273
|
+
accumulatedOpacity: number,
|
|
1274
|
+
parentMatrix: DOMMatrix,
|
|
1275
|
+
parentRect: Rect | null,
|
|
1276
|
+
targetW: number = nativeWidth,
|
|
1277
|
+
targetH: number = nativeHeight,
|
|
1278
|
+
parent3DMatrix: Mat4 | null = null,
|
|
1279
|
+
isRootScene = false,
|
|
1280
|
+
): Promise<
|
|
1281
|
+
{ finalTex: GPUTexture; finalView: GPUTextureView } | undefined
|
|
1282
|
+
> => {
|
|
1283
|
+
// Textures queued for the pending 3D flush are still live: a later
|
|
1284
|
+
// layer of the same size must not be handed one from the pool.
|
|
1285
|
+
const excludes = [
|
|
1286
|
+
...baseExcludes,
|
|
1287
|
+
...pending3DItems
|
|
1288
|
+
.map((q) => q.texture)
|
|
1289
|
+
.filter((t): t is GPUTexture => Boolean(t)),
|
|
1290
|
+
];
|
|
1291
|
+
const lop = nodeVM.operation as any;
|
|
1292
|
+
if (lop.kind === "camera" || lop.kind === "light") return;
|
|
1293
|
+
const targetId = lop.id || lop.inputHandleId;
|
|
1294
|
+
const target = targetId ? targetsById[targetId] : undefined;
|
|
1295
|
+
if (target?.hidden || lop.hidden) return;
|
|
1296
|
+
|
|
1297
|
+
const kind = lop.kind;
|
|
1298
|
+
const isContainer = kind === "flex" || kind === "block" || kind === "box";
|
|
1299
|
+
|
|
1300
|
+
// Temporal gate (video mode): clip-relative duration.
|
|
1301
|
+
if (isVideoMode) {
|
|
1302
|
+
const startFrame = lop.startFrame ?? 0;
|
|
1303
|
+
const durationFrames = lop.durationFrames;
|
|
1304
|
+
if (shiftedFrame < startFrame) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (
|
|
1308
|
+
durationFrames !== undefined &&
|
|
1309
|
+
shiftedFrame >= startFrame + durationFrames
|
|
1310
|
+
) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (kind === "model3d") {
|
|
1315
|
+
const r3d = getRenderer3D(
|
|
1316
|
+
ctx.device,
|
|
1317
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
1318
|
+
sampleCount3d,
|
|
1319
|
+
);
|
|
1320
|
+
let modelSrc = lop.modelData ?? lop.src;
|
|
1321
|
+
|
|
1322
|
+
if (!modelSrc && lop.text3dOptions) {
|
|
1323
|
+
// The node's Layer3D.extrudedText options: geometry options plus a family name.
|
|
1324
|
+
const tOpts = lop.text3dOptions as Omit<
|
|
1325
|
+
ExtrudedTextGeometryOptions,
|
|
1326
|
+
"font"
|
|
1327
|
+
> & { fontFamily?: string };
|
|
1328
|
+
const font =
|
|
1329
|
+
(tOpts.fontFamily
|
|
1330
|
+
? SlugFontCache.getParsed(tOpts.fontFamily)
|
|
1331
|
+
: null) ??
|
|
1332
|
+
SlugFontCache.getParsed("Inter") ??
|
|
1333
|
+
SlugFontCache.getParsed("Unbounded") ??
|
|
1334
|
+
SlugFontCache.getFirstParsed();
|
|
1335
|
+
if (font) {
|
|
1336
|
+
const res = generateExtrudedTextGeometry({
|
|
1337
|
+
...tOpts,
|
|
1338
|
+
// SlugFontCache holds fontkit fonts under a narrower type.
|
|
1339
|
+
font: font as unknown as ExtrudedTextGeometryOptions["font"],
|
|
1340
|
+
});
|
|
1341
|
+
modelSrc = res.modelData;
|
|
1342
|
+
lop.modelData = modelSrc;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!modelSrc) return;
|
|
1347
|
+
|
|
1348
|
+
let model: Model3DData | undefined;
|
|
1349
|
+
try {
|
|
1350
|
+
model =
|
|
1351
|
+
typeof modelSrc === "object" &&
|
|
1352
|
+
modelSrc !== null &&
|
|
1353
|
+
"meshes" in modelSrc
|
|
1354
|
+
? (modelSrc as Model3DData)
|
|
1355
|
+
: r3d.loadModel(modelSrc, lop.modelFormat ?? "auto", {
|
|
1356
|
+
center: lop.center,
|
|
1357
|
+
normalizeSize: lop.normalizeSize,
|
|
1358
|
+
});
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
console.error("Failed to load 3D model:", err);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (!model || model.meshes.length === 0) return;
|
|
1365
|
+
|
|
1366
|
+
const posX = target?.x ?? lop.x ?? 0;
|
|
1367
|
+
const posY = target?.y ?? lop.y ?? 0;
|
|
1368
|
+
const posZ = target?.z ?? lop.z ?? 0;
|
|
1369
|
+
|
|
1370
|
+
const rotX = target?.rotateX ?? lop.rotateX ?? 0;
|
|
1371
|
+
const rotY = target?.rotateY ?? lop.rotateY ?? 0;
|
|
1372
|
+
const rotZ =
|
|
1373
|
+
(target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
1374
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
1375
|
+
: (target?.rotation ?? lop.rotation ?? 0);
|
|
1376
|
+
|
|
1377
|
+
const sc = target?.scale ?? lop.scale ?? 1;
|
|
1378
|
+
const scX = (target?.scaleX ?? lop.scaleX ?? 1) * sc;
|
|
1379
|
+
const scY = (target?.scaleY ?? lop.scaleY ?? 1) * sc;
|
|
1380
|
+
const scZ = (target?.scaleZ ?? lop.scaleZ ?? 1) * sc;
|
|
1381
|
+
|
|
1382
|
+
let localM = Matrix4Math.identity();
|
|
1383
|
+
localM = Matrix4Math.multiply(
|
|
1384
|
+
localM,
|
|
1385
|
+
Matrix4Math.translate(posX, posY, posZ),
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
const rxRad = (rotX * Math.PI) / 180;
|
|
1389
|
+
const ryRad = (rotY * Math.PI) / 180;
|
|
1390
|
+
const rzRad = (rotZ * Math.PI) / 180;
|
|
1391
|
+
|
|
1392
|
+
if (rzRad !== 0)
|
|
1393
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateZ(rzRad));
|
|
1394
|
+
if (ryRad !== 0)
|
|
1395
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateY(ryRad));
|
|
1396
|
+
if (rxRad !== 0)
|
|
1397
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateX(rxRad));
|
|
1398
|
+
|
|
1399
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.scale(scX, scY, scZ));
|
|
1400
|
+
|
|
1401
|
+
const finalModelMatrix = parent3DMatrix
|
|
1402
|
+
? Matrix4Math.multiply(parent3DMatrix, localM)
|
|
1403
|
+
: localM;
|
|
1404
|
+
|
|
1405
|
+
const colorParsed =
|
|
1406
|
+
target?.colorTint || target?.color || lop.colorTint || lop.color
|
|
1407
|
+
? parseColor(
|
|
1408
|
+
target?.colorTint ??
|
|
1409
|
+
target?.color ??
|
|
1410
|
+
lop.colorTint ??
|
|
1411
|
+
lop.color,
|
|
1412
|
+
)
|
|
1413
|
+
: undefined;
|
|
1414
|
+
|
|
1415
|
+
const colorTint: [number, number, number, number] = colorParsed
|
|
1416
|
+
? [colorParsed.r, colorParsed.g, colorParsed.b, colorParsed.a]
|
|
1417
|
+
: [1, 1, 1, 1];
|
|
1418
|
+
|
|
1419
|
+
const modelOpacity =
|
|
1420
|
+
(target?.opacity ?? lop.opacity ?? 1) * accumulatedOpacity;
|
|
1421
|
+
|
|
1422
|
+
// Animation evaluation
|
|
1423
|
+
let skinningMatrices: Float32Array | undefined;
|
|
1424
|
+
const animProgress =
|
|
1425
|
+
target?.animationProgress ?? lop.animationProgress ?? 0;
|
|
1426
|
+
const animTime = target?.animationTime ?? lop.animationTime ?? 0;
|
|
1427
|
+
const animSpeed = target?.animationSpeed ?? lop.animationSpeed ?? 1.0;
|
|
1428
|
+
const clip =
|
|
1429
|
+
(lop.animationName
|
|
1430
|
+
? model.animations.find((a) => a.name === lop.animationName)
|
|
1431
|
+
: undefined) ?? model.animations[0];
|
|
1432
|
+
let clipSec = 0;
|
|
1433
|
+
if (clip && clip.duration > 0) {
|
|
1434
|
+
clipSec =
|
|
1435
|
+
animProgress > 0
|
|
1436
|
+
? animProgress * clip.duration
|
|
1437
|
+
: animTime + ((shiftedFrame ?? 0) / (fps || 30)) * animSpeed;
|
|
1438
|
+
if (lop.loop ?? true) clipSec %= clip.duration;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// A scene graph (glTF) is posed node by node: unskinned meshes ride
|
|
1442
|
+
// their node, skinned ones take their skin's joint matrices.
|
|
1443
|
+
const sceneRoot = Matrix4Math.multiply(
|
|
1444
|
+
finalModelMatrix,
|
|
1445
|
+
model.rootTransform ?? Matrix4Math.identity(),
|
|
1446
|
+
);
|
|
1447
|
+
const posed = model.nodes ? poseNodes(model, clip, clipSec) : undefined;
|
|
1448
|
+
const skinCache = new Map<number, Float32Array>();
|
|
1449
|
+
const placeMesh = (
|
|
1450
|
+
mesh: Mesh3DData,
|
|
1451
|
+
): { matrix: Mat4; joints?: Float32Array } => {
|
|
1452
|
+
if (!posed) return { matrix: sceneRoot };
|
|
1453
|
+
if (mesh.skin !== undefined) {
|
|
1454
|
+
let joints = skinCache.get(mesh.skin);
|
|
1455
|
+
if (!joints) {
|
|
1456
|
+
joints = jointMatrices(model, mesh.skin, posed);
|
|
1457
|
+
skinCache.set(mesh.skin, joints);
|
|
1458
|
+
}
|
|
1459
|
+
return { matrix: sceneRoot, joints };
|
|
1460
|
+
}
|
|
1461
|
+
if (mesh.node !== undefined)
|
|
1462
|
+
return {
|
|
1463
|
+
matrix: Matrix4Math.multiply(sceneRoot, posed[mesh.node]!),
|
|
1464
|
+
};
|
|
1465
|
+
return { matrix: sceneRoot };
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
if (!posed && clip && clip.duration > 0) {
|
|
1469
|
+
const currentSec = clipSec;
|
|
1470
|
+
if (model.skeleton && model.skeleton.bones.length > 0) {
|
|
1471
|
+
const numBones = Math.min(128, model.skeleton.bones.length);
|
|
1472
|
+
skinningMatrices = new Float32Array(128 * 16);
|
|
1473
|
+
for (let b = 0; b < 128; b++) {
|
|
1474
|
+
skinningMatrices[b * 16] = 1;
|
|
1475
|
+
skinningMatrices[b * 16 + 5] = 1;
|
|
1476
|
+
skinningMatrices[b * 16 + 10] = 1;
|
|
1477
|
+
skinningMatrices[b * 16 + 15] = 1;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
for (const track of clip.tracks) {
|
|
1481
|
+
const boneIdx = model.skeleton.boneIndicesByName[track.nodeName];
|
|
1482
|
+
if (boneIdx !== undefined && boneIdx < numBones) {
|
|
1483
|
+
const times = track.times;
|
|
1484
|
+
let sampleIdx = 0;
|
|
1485
|
+
for (let k = 0; k < times.length - 1; k++) {
|
|
1486
|
+
if (currentSec >= times[k]! && currentSec <= times[k + 1]!) {
|
|
1487
|
+
sampleIdx = k;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
const t0 = times[sampleIdx] ?? 0;
|
|
1492
|
+
const t1 = times[sampleIdx + 1] ?? t0;
|
|
1493
|
+
const alpha = t1 > t0 ? (currentSec - t0) / (t1 - t0) : 0;
|
|
1494
|
+
|
|
1495
|
+
if (track.translations) {
|
|
1496
|
+
const x0 = track.translations[sampleIdx * 3] ?? 0;
|
|
1497
|
+
const y0 = track.translations[sampleIdx * 3 + 1] ?? 0;
|
|
1498
|
+
const z0 = track.translations[sampleIdx * 3 + 2] ?? 0;
|
|
1499
|
+
const x1 = track.translations[(sampleIdx + 1) * 3] ?? x0;
|
|
1500
|
+
const y1 = track.translations[(sampleIdx + 1) * 3 + 1] ?? y0;
|
|
1501
|
+
const z1 = track.translations[(sampleIdx + 1) * 3 + 2] ?? z0;
|
|
1502
|
+
const curX = x0 + (x1 - x0) * alpha;
|
|
1503
|
+
const curY = y0 + (y1 - y0) * alpha;
|
|
1504
|
+
const curZ = z0 + (z1 - z0) * alpha;
|
|
1505
|
+
|
|
1506
|
+
const boneMat = Matrix4Math.translate(curX, curY, curZ);
|
|
1507
|
+
const bone = model.skeleton.bones[boneIdx];
|
|
1508
|
+
const skinMat = bone
|
|
1509
|
+
? Matrix4Math.multiply(boneMat, bone.inverseBindMatrix)
|
|
1510
|
+
: boneMat;
|
|
1511
|
+
skinningMatrices.set(skinMat, boneIdx * 16);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Blended materials draw after everything opaque behind them.
|
|
1519
|
+
const isBlend = (m: Mesh3DData) =>
|
|
1520
|
+
m.materialName
|
|
1521
|
+
? model.materials[m.materialName]?.alphaMode === "blend"
|
|
1522
|
+
: false;
|
|
1523
|
+
const drawOrder = [...model.meshes].sort(
|
|
1524
|
+
(a, b) => Number(isBlend(a)) - Number(isBlend(b)),
|
|
1525
|
+
);
|
|
1526
|
+
for (const mesh of drawOrder) {
|
|
1527
|
+
const mat = mesh.materialName
|
|
1528
|
+
? model.materials[mesh.materialName]
|
|
1529
|
+
: undefined;
|
|
1530
|
+
const placed = placeMesh(mesh);
|
|
1531
|
+
const meshSkin = placed.joints ?? skinningMatrices;
|
|
1532
|
+
|
|
1533
|
+
let meshColorTint: [number, number, number, number];
|
|
1534
|
+
if (colorParsed) {
|
|
1535
|
+
if (mat?.diffuseColor) {
|
|
1536
|
+
meshColorTint = [
|
|
1537
|
+
mat.diffuseColor[0] * colorTint[0],
|
|
1538
|
+
mat.diffuseColor[1] * colorTint[1],
|
|
1539
|
+
mat.diffuseColor[2] * colorTint[2],
|
|
1540
|
+
(mat.diffuseColor[3] ?? 1.0) * colorTint[3],
|
|
1541
|
+
];
|
|
1542
|
+
} else {
|
|
1543
|
+
meshColorTint = colorTint;
|
|
1544
|
+
}
|
|
1545
|
+
} else if (mat?.diffuseColor) {
|
|
1546
|
+
meshColorTint = mat.diffuseColor;
|
|
1547
|
+
} else {
|
|
1548
|
+
meshColorTint = [0.8, 0.8, 0.85, 1.0];
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Audio latent 3D mesh deformation
|
|
1552
|
+
const audioDeformConfig = (target?.audioDeform ?? lop.audioDeform) as
|
|
1553
|
+
| MeshAudioDeformConfig
|
|
1554
|
+
| undefined;
|
|
1555
|
+
|
|
1556
|
+
let meshAudioDeform: DrawMesh3DOpts["audioDeform"];
|
|
1557
|
+
if (audioDeformConfig) {
|
|
1558
|
+
const currentSec = (shiftedFrame ?? 0) / (fps || 30);
|
|
1559
|
+
const timeMs = currentSec * 1000;
|
|
1560
|
+
|
|
1561
|
+
let latentBuffer = (audioDeformConfig as any).latentBuffer as
|
|
1562
|
+
| GPUBuffer
|
|
1563
|
+
| undefined;
|
|
1564
|
+
if (!latentBuffer && (audioDeformConfig as any).audioBuffer) {
|
|
1565
|
+
latentBuffer = (audioDeformConfig as any)
|
|
1566
|
+
.audioBuffer as GPUBuffer;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (!latentBuffer) {
|
|
1570
|
+
const directAudio = (audioDeformConfig as any).audioData as
|
|
1571
|
+
| Float32Array[]
|
|
1572
|
+
| { channels: Float32Array[]; sampleRate: number }
|
|
1573
|
+
| undefined;
|
|
1574
|
+
|
|
1575
|
+
if (directAudio) {
|
|
1576
|
+
const cacheKey = (lop.id ?? "audio-mesh") + "-direct-audio";
|
|
1577
|
+
let trackCache = audioTrackLatentCache.get(cacheKey);
|
|
1578
|
+
if (!trackCache) {
|
|
1579
|
+
const chs = Array.isArray(directAudio)
|
|
1580
|
+
? directAudio
|
|
1581
|
+
: directAudio.channels;
|
|
1582
|
+
const sRate = Array.isArray(directAudio)
|
|
1583
|
+
? 48000
|
|
1584
|
+
: directAudio.sampleRate;
|
|
1585
|
+
const totalF = Math.max(
|
|
1586
|
+
10,
|
|
1587
|
+
Math.ceil(((chs[0]?.length ?? 0) / sRate) * (fps || 30)) +
|
|
1588
|
+
10,
|
|
1589
|
+
);
|
|
1590
|
+
trackCache = new AudioLatentTrackCache(
|
|
1591
|
+
chs,
|
|
1592
|
+
sRate,
|
|
1593
|
+
fps || 30,
|
|
1594
|
+
totalF,
|
|
1595
|
+
);
|
|
1596
|
+
audioTrackLatentCache.set(cacheKey, trackCache);
|
|
1597
|
+
}
|
|
1598
|
+
latentBuffer = trackCache.getGpuBuffer(
|
|
1599
|
+
ctx.device,
|
|
1600
|
+
shiftedFrame ?? 0,
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (!latentBuffer && audioDeformConfig.audioTrackId) {
|
|
1606
|
+
const trackId = audioDeformConfig.audioTrackId;
|
|
1607
|
+
let foundAudioUrl: string | undefined;
|
|
1608
|
+
const searchVms = (list: VirtualMediaData[]) => {
|
|
1609
|
+
for (const v of list) {
|
|
1610
|
+
const op = v.operation as Record<string, unknown> | undefined;
|
|
1611
|
+
if (op) {
|
|
1612
|
+
if (op.id === trackId || op.inputHandleId === trackId) {
|
|
1613
|
+
foundAudioUrl =
|
|
1614
|
+
(op.inputHandleId as string) ||
|
|
1615
|
+
(op.src as string) ||
|
|
1616
|
+
(op.url as string);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (v.children) searchVms(v.children);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
if (virtualMedia.children) searchVms(virtualMedia.children);
|
|
1624
|
+
if (
|
|
1625
|
+
!foundAudioUrl &&
|
|
1626
|
+
(trackId.endsWith(".mp3") ||
|
|
1627
|
+
trackId.endsWith(".wav") ||
|
|
1628
|
+
trackId.endsWith(".ogg"))
|
|
1629
|
+
) {
|
|
1630
|
+
foundAudioUrl = trackId;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (foundAudioUrl) {
|
|
1634
|
+
try {
|
|
1635
|
+
let trackCache = audioTrackLatentCache.get(foundAudioUrl);
|
|
1636
|
+
if (!trackCache) {
|
|
1637
|
+
const decoded = await decodeAudioSource(foundAudioUrl);
|
|
1638
|
+
if (decoded && decoded.channels.length > 0) {
|
|
1639
|
+
const sRate = decoded.sampleRate || 48000;
|
|
1640
|
+
const ch0Len = decoded.channels[0]?.length ?? 0;
|
|
1641
|
+
const durationSec =
|
|
1642
|
+
(decoded as any).durationSec ?? ch0Len / sRate;
|
|
1643
|
+
const totalF = Math.max(
|
|
1644
|
+
10,
|
|
1645
|
+
Math.ceil(durationSec * (fps || 30)) + 10,
|
|
1646
|
+
);
|
|
1647
|
+
trackCache = new AudioLatentTrackCache(
|
|
1648
|
+
decoded.channels,
|
|
1649
|
+
sRate,
|
|
1650
|
+
fps || 30,
|
|
1651
|
+
totalF,
|
|
1652
|
+
);
|
|
1653
|
+
audioTrackLatentCache.set(foundAudioUrl, trackCache);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (trackCache) {
|
|
1657
|
+
latentBuffer = trackCache.getGpuBuffer(
|
|
1658
|
+
ctx.device,
|
|
1659
|
+
shiftedFrame ?? 0,
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
} catch (err) {
|
|
1663
|
+
console.warn(
|
|
1664
|
+
"[webgpu-renderer] Failed to decode audio for deformWithAudio:",
|
|
1665
|
+
err,
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (latentBuffer) {
|
|
1672
|
+
let amplitudeMultiplier =
|
|
1673
|
+
audioDeformConfig.amplitudeMultiplier ?? 1.0;
|
|
1674
|
+
if (
|
|
1675
|
+
typeof amplitudeMultiplier !== "number" &&
|
|
1676
|
+
typeof (amplitudeMultiplier as any)?.get === "function"
|
|
1677
|
+
) {
|
|
1678
|
+
const frameCtx = {
|
|
1679
|
+
frame: shiftedFrame ?? 0,
|
|
1680
|
+
fps: fps || 30,
|
|
1681
|
+
time: currentSec,
|
|
1682
|
+
duration: 0,
|
|
1683
|
+
durationMs: 0,
|
|
1684
|
+
progress: 0,
|
|
1685
|
+
deltaTime: 1 / (fps || 30),
|
|
1686
|
+
};
|
|
1687
|
+
amplitudeMultiplier = Number(
|
|
1688
|
+
(amplitudeMultiplier as any).get(frameCtx),
|
|
1689
|
+
);
|
|
1690
|
+
} else if (
|
|
1691
|
+
typeof amplitudeMultiplier !== "number" &&
|
|
1692
|
+
typeof (amplitudeMultiplier as any)?.value === "number"
|
|
1693
|
+
) {
|
|
1694
|
+
amplitudeMultiplier = (amplitudeMultiplier as any).value;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
meshAudioDeform = {
|
|
1698
|
+
config: {
|
|
1699
|
+
...audioDeformConfig,
|
|
1700
|
+
amplitudeMultiplier: Number.isFinite(
|
|
1701
|
+
amplitudeMultiplier as number,
|
|
1702
|
+
)
|
|
1703
|
+
? (amplitudeMultiplier as number)
|
|
1704
|
+
: 1.0,
|
|
1705
|
+
},
|
|
1706
|
+
latentBuffer,
|
|
1707
|
+
timeMs,
|
|
1708
|
+
sampleRate: 48000,
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const meshOpts: DrawMesh3DOpts = {
|
|
1714
|
+
modelMatrix: placed.matrix,
|
|
1715
|
+
opacity: modelOpacity * (mat?.opacity ?? 1.0),
|
|
1716
|
+
colorTint: meshColorTint,
|
|
1717
|
+
twoSided: target?.twoSided ?? lop.twoSided ?? mat?.twoSided ?? true,
|
|
1718
|
+
wireframe: Boolean(target?.wireframe ?? lop.wireframe),
|
|
1719
|
+
isSkinned: Boolean(
|
|
1720
|
+
meshSkin || (mesh.jointIndices && mesh.jointWeights),
|
|
1721
|
+
),
|
|
1722
|
+
skinningMatrices: meshSkin,
|
|
1723
|
+
material: target?.material ?? lop.material ?? mat?.shading ?? "lit",
|
|
1724
|
+
shadeColor: mat?.shadeColor,
|
|
1725
|
+
alphaCutoff: mat?.alphaCutoff,
|
|
1726
|
+
textureWrap: mat?.textureWrap,
|
|
1727
|
+
shininess:
|
|
1728
|
+
target?.shininess ?? lop.shininess ?? mat?.shininess ?? 32,
|
|
1729
|
+
roughness: target?.roughness ?? lop.roughness ?? mat?.roughness,
|
|
1730
|
+
specularIntensity:
|
|
1731
|
+
target?.specularIntensity ?? lop.specularIntensity ?? 0.5,
|
|
1732
|
+
ambientIntensity:
|
|
1733
|
+
target?.ambientIntensity ?? lop.ambientIntensity ?? 1.0,
|
|
1734
|
+
metallic: target?.metallic ?? lop.metallic ?? mat?.metallic,
|
|
1735
|
+
audioDeform: meshAudioDeform,
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
pending3DItems.push({
|
|
1739
|
+
kind: "mesh",
|
|
1740
|
+
mesh,
|
|
1741
|
+
texture: mat ? await r3d.materialTexture(mat) : undefined,
|
|
1742
|
+
opts: meshOpts,
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
return { finalTex: destTex, finalView: destView };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
const rect = rects[lop.id];
|
|
1750
|
+
if (!rect) {
|
|
1751
|
+
if (isContainer) {
|
|
1752
|
+
return await renderNodeTree(
|
|
1753
|
+
nodeVM.children ?? [],
|
|
1754
|
+
destTex,
|
|
1755
|
+
destView,
|
|
1756
|
+
excludes,
|
|
1757
|
+
accumulatedOpacity,
|
|
1758
|
+
parentMatrix,
|
|
1759
|
+
parentRect,
|
|
1760
|
+
targetW,
|
|
1761
|
+
targetH,
|
|
1762
|
+
parent3DMatrix,
|
|
1763
|
+
isRootScene,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// Affine = layout rect + animated delta (CSS-transform semantics: a
|
|
1770
|
+
// track on x/y translates the node within/out of its laid-out slot).
|
|
1771
|
+
const baseX = lop.x ?? 0;
|
|
1772
|
+
const baseY = lop.y ?? 0;
|
|
1773
|
+
const sampledX = target?.x ?? baseX;
|
|
1774
|
+
const sampledY = target?.y ?? baseY;
|
|
1775
|
+
|
|
1776
|
+
let localX = 0;
|
|
1777
|
+
let localY = 0;
|
|
1778
|
+
if (parentRect) {
|
|
1779
|
+
const localLayoutX = rect.x - parentRect.x;
|
|
1780
|
+
const localLayoutY = rect.y - parentRect.y;
|
|
1781
|
+
localX = localLayoutX + (sampledX - baseX);
|
|
1782
|
+
localY = localLayoutY + (sampledY - baseY);
|
|
1783
|
+
} else {
|
|
1784
|
+
localX = rect.x + (sampledX - baseX);
|
|
1785
|
+
localY = rect.y + (sampledY - baseY);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const scale = target?.scale ?? lop.scale ?? 1;
|
|
1789
|
+
const rotation = target?.rotation ?? lop.rotation ?? 0;
|
|
1790
|
+
const layerOpacity =
|
|
1791
|
+
(target?.opacity ?? lop.opacity ?? 1) * accumulatedOpacity;
|
|
1792
|
+
|
|
1793
|
+
const rawAnimators = target?.animators ?? lop.animators;
|
|
1794
|
+
const isPathText =
|
|
1795
|
+
kind === "text" && Boolean(lop.pathOptions || target?.pathOptions);
|
|
1796
|
+
const hasKineticAnimators =
|
|
1797
|
+
kind === "text" &&
|
|
1798
|
+
Array.isArray(rawAnimators) &&
|
|
1799
|
+
rawAnimators.length > 0;
|
|
1800
|
+
const hasFormation =
|
|
1801
|
+
kind === "text" &&
|
|
1802
|
+
Array.isArray(rawAnimators) &&
|
|
1803
|
+
rawAnimators.some((a: unknown) => {
|
|
1804
|
+
const anim = a as
|
|
1805
|
+
| {
|
|
1806
|
+
props?: { formation?: unknown };
|
|
1807
|
+
formation?: unknown;
|
|
1808
|
+
}
|
|
1809
|
+
| null
|
|
1810
|
+
| undefined;
|
|
1811
|
+
return (
|
|
1812
|
+
anim?.props?.formation !== undefined ||
|
|
1813
|
+
anim?.formation !== undefined
|
|
1814
|
+
);
|
|
1815
|
+
});
|
|
1816
|
+
let padX = 0;
|
|
1817
|
+
let padY = 0;
|
|
1818
|
+
if (hasFormation) {
|
|
1819
|
+
let formationRadius = 0;
|
|
1820
|
+
let formationPitch = 0;
|
|
1821
|
+
for (const a of rawAnimators as unknown[]) {
|
|
1822
|
+
const anim = a as
|
|
1823
|
+
| {
|
|
1824
|
+
props?: { formation?: { radius?: number; pitch?: number } };
|
|
1825
|
+
formation?: { radius?: number; pitch?: number };
|
|
1826
|
+
}
|
|
1827
|
+
| null
|
|
1828
|
+
| undefined;
|
|
1829
|
+
const f = anim?.props?.formation ?? anim?.formation;
|
|
1830
|
+
if (f) {
|
|
1831
|
+
if (typeof f.radius === "number") {
|
|
1832
|
+
formationRadius = Math.max(formationRadius, f.radius);
|
|
1833
|
+
}
|
|
1834
|
+
if (typeof f.pitch === "number") {
|
|
1835
|
+
formationPitch = Math.max(formationPitch, f.pitch);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
padX = Math.max(160, Math.ceil(formationRadius + 100));
|
|
1840
|
+
padY = Math.max(
|
|
1841
|
+
100,
|
|
1842
|
+
Math.ceil(formationRadius + formationPitch * 15 + 100),
|
|
1843
|
+
);
|
|
1844
|
+
} else if (!isPathText && hasKineticAnimators) {
|
|
1845
|
+
padX = Math.max(80, Math.ceil(rect.width * 0.2));
|
|
1846
|
+
padY = Math.max(50, Math.ceil(rect.height * 0.5));
|
|
1847
|
+
}
|
|
1848
|
+
// Path text draws into a canvas-sized layer so glyphs following a path
|
|
1849
|
+
// beyond the node box aren't clipped. Path coordinates stay local to
|
|
1850
|
+
// the node (like every other child), so the layer still sits at the
|
|
1851
|
+
// node's layout position.
|
|
1852
|
+
const layerW = isPathText ? targetW : rect.width + padX * 2;
|
|
1853
|
+
const layerH = isPathText ? targetH : rect.height + padY * 2;
|
|
1854
|
+
if (!isPathText && hasKineticAnimators) {
|
|
1855
|
+
localX -= padX;
|
|
1856
|
+
localY -= padY;
|
|
1857
|
+
}
|
|
1858
|
+
// Anchor-aware pivot (H2): scale/rotation happen AROUND the node's
|
|
1859
|
+
// anchor point (0.5/0.5 = center) instead of its top-left corner.
|
|
1860
|
+
// For path text the anchor is a point of the node box, not of the
|
|
1861
|
+
// oversized layer, so a badge spins around its own centre.
|
|
1862
|
+
const anchorX = lop.anchorX ?? 0.5;
|
|
1863
|
+
const anchorY = lop.anchorY ?? 0.5;
|
|
1864
|
+
const localMatrix = toDOMMatrix(
|
|
1865
|
+
buildLayerMatrix({
|
|
1866
|
+
x: localX,
|
|
1867
|
+
y: localY,
|
|
1868
|
+
width: layerW,
|
|
1869
|
+
height: layerH,
|
|
1870
|
+
rotation,
|
|
1871
|
+
scale,
|
|
1872
|
+
anchorX: isPathText ? (anchorX * rect.width) / layerW : anchorX,
|
|
1873
|
+
anchorY: isPathText ? (anchorY * rect.height) / layerH : anchorY,
|
|
1874
|
+
}),
|
|
1875
|
+
);
|
|
1876
|
+
const layerMatrix = parentMatrix.multiply(localMatrix);
|
|
1877
|
+
const blendMode = (lop.blendMode as BlendMode) || "normal";
|
|
1878
|
+
|
|
1879
|
+
let activeTex = destTex;
|
|
1880
|
+
let activeView = destView;
|
|
1881
|
+
|
|
1882
|
+
const is3D = has3DTransform(target, lop);
|
|
1883
|
+
const rotateX = target?.rotateX ?? lop.rotateX ?? 0;
|
|
1884
|
+
const rotateY = target?.rotateY ?? lop.rotateY ?? 0;
|
|
1885
|
+
const rotateZ = target?.rotateZ ?? lop.rotateZ ?? 0;
|
|
1886
|
+
const translateZ = target?.translateZ ?? lop.translateZ ?? 0;
|
|
1887
|
+
const perspective = target?.perspective ?? lop.perspective ?? 0;
|
|
1888
|
+
const perspectiveOriginX =
|
|
1889
|
+
target?.perspectiveOriginX ??
|
|
1890
|
+
lop.perspectiveOriginX ??
|
|
1891
|
+
lop.anchorX ??
|
|
1892
|
+
0.5;
|
|
1893
|
+
const perspectiveOriginY =
|
|
1894
|
+
target?.perspectiveOriginY ??
|
|
1895
|
+
lop.perspectiveOriginY ??
|
|
1896
|
+
lop.anchorY ??
|
|
1897
|
+
0.5;
|
|
1898
|
+
const backfaceVisibility =
|
|
1899
|
+
target?.backfaceVisibility ?? lop.backfaceVisibility ?? "visible";
|
|
1900
|
+
|
|
1901
|
+
let quad3D: Quad2D | null = null;
|
|
1902
|
+
if (is3D) {
|
|
1903
|
+
const m3d = Transform3DMath.buildLayer3DMatrix({
|
|
1904
|
+
x: localX,
|
|
1905
|
+
y: localY,
|
|
1906
|
+
width: layerW,
|
|
1907
|
+
height: layerH,
|
|
1908
|
+
rotateXDeg: rotateX,
|
|
1909
|
+
rotateYDeg: rotateY,
|
|
1910
|
+
rotateZDeg: rotateZ !== 0 ? rotateZ : rotation,
|
|
1911
|
+
scaleX: scale,
|
|
1912
|
+
scaleY: scale,
|
|
1913
|
+
perspectivePx: perspective,
|
|
1914
|
+
originXRatio: perspectiveOriginX,
|
|
1915
|
+
originYRatio: perspectiveOriginY,
|
|
1916
|
+
translateZPx: translateZ,
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
let q = Transform3DMath.projectRectangleCorners(m3d, layerW, layerH);
|
|
1920
|
+
if (!parentMatrix.isIdentity) {
|
|
1921
|
+
q = {
|
|
1922
|
+
topLeft: parentMatrix.transformPoint(
|
|
1923
|
+
new DOMPoint(q.topLeft.x, q.topLeft.y),
|
|
1924
|
+
),
|
|
1925
|
+
topRight: parentMatrix.transformPoint(
|
|
1926
|
+
new DOMPoint(q.topRight.x, q.topRight.y),
|
|
1927
|
+
),
|
|
1928
|
+
bottomLeft: parentMatrix.transformPoint(
|
|
1929
|
+
new DOMPoint(q.bottomLeft.x, q.bottomLeft.y),
|
|
1930
|
+
),
|
|
1931
|
+
bottomRight: parentMatrix.transformPoint(
|
|
1932
|
+
new DOMPoint(q.bottomRight.x, q.bottomRight.y),
|
|
1933
|
+
),
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (
|
|
1938
|
+
backfaceVisibility === "hidden" &&
|
|
1939
|
+
Transform3DMath.computeSignedArea(q) < 0
|
|
1940
|
+
) {
|
|
1941
|
+
return; // Culled by backface visibility
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
quad3D = q;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const isTrue3D =
|
|
1948
|
+
Boolean(target?.is3D ?? lop.is3D) ||
|
|
1949
|
+
Boolean(camera && ((target?.z ?? lop.z ?? 0) !== 0 || lop.is3D));
|
|
1950
|
+
|
|
1951
|
+
// Container rendering
|
|
1952
|
+
if (isContainer) {
|
|
1953
|
+
if (lop.transformStyle === "preserve-3d" && isTrue3D) {
|
|
1954
|
+
let current3DMatrix: Mat4 = Matrix4Math.identity();
|
|
1955
|
+
const containerX =
|
|
1956
|
+
parent3DMatrix !== null
|
|
1957
|
+
? (target?.x ?? lop.x ?? 0)
|
|
1958
|
+
: (target?.x ?? lop.x ?? localX + layerW * 0.5);
|
|
1959
|
+
const containerY =
|
|
1960
|
+
parent3DMatrix !== null
|
|
1961
|
+
? (target?.y ?? lop.y ?? 0)
|
|
1962
|
+
: (target?.y ?? lop.y ?? localY + layerH * 0.5);
|
|
1963
|
+
const containerZ = target?.z ?? lop.z ?? 0;
|
|
1964
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
1965
|
+
current3DMatrix,
|
|
1966
|
+
Matrix4Math.translate(containerX, containerY, containerZ),
|
|
1967
|
+
);
|
|
1968
|
+
const rxRad = ((target?.rotateX ?? lop.rotateX ?? 0) * Math.PI) / 180;
|
|
1969
|
+
const ryRad = ((target?.rotateY ?? lop.rotateY ?? 0) * Math.PI) / 180;
|
|
1970
|
+
const rzRad =
|
|
1971
|
+
((target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
1972
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
1973
|
+
: rotation) *
|
|
1974
|
+
(Math.PI / 180);
|
|
1975
|
+
if (rzRad !== 0)
|
|
1976
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
1977
|
+
current3DMatrix,
|
|
1978
|
+
Matrix4Math.rotateZ(rzRad),
|
|
1979
|
+
);
|
|
1980
|
+
if (ryRad !== 0)
|
|
1981
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
1982
|
+
current3DMatrix,
|
|
1983
|
+
Matrix4Math.rotateY(ryRad),
|
|
1984
|
+
);
|
|
1985
|
+
if (rxRad !== 0)
|
|
1986
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
1987
|
+
current3DMatrix,
|
|
1988
|
+
Matrix4Math.rotateX(rxRad),
|
|
1989
|
+
);
|
|
1990
|
+
const csX = (target?.scaleX ?? lop.scaleX ?? 1) * scale;
|
|
1991
|
+
const csY = (target?.scaleY ?? lop.scaleY ?? 1) * scale;
|
|
1992
|
+
const csZ = (target?.scaleZ ?? lop.scaleZ ?? 1) * scale;
|
|
1993
|
+
if (csX !== 1 || csY !== 1 || csZ !== 1)
|
|
1994
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
1995
|
+
current3DMatrix,
|
|
1996
|
+
Matrix4Math.scale(csX, csY, csZ),
|
|
1997
|
+
);
|
|
1998
|
+
|
|
1999
|
+
if (parent3DMatrix) {
|
|
2000
|
+
current3DMatrix = Matrix4Math.multiply(
|
|
2001
|
+
parent3DMatrix,
|
|
2002
|
+
current3DMatrix,
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (nodeVM.children && nodeVM.children.length > 0) {
|
|
2007
|
+
const res = await renderNodeTree(
|
|
2008
|
+
nodeVM.children,
|
|
2009
|
+
activeTex,
|
|
2010
|
+
activeView,
|
|
2011
|
+
excludes,
|
|
2012
|
+
layerOpacity,
|
|
2013
|
+
new DOMMatrix(),
|
|
2014
|
+
null,
|
|
2015
|
+
layerW,
|
|
2016
|
+
layerH,
|
|
2017
|
+
current3DMatrix,
|
|
2018
|
+
false,
|
|
2019
|
+
);
|
|
2020
|
+
activeTex = res.finalTex;
|
|
2021
|
+
activeView = res.finalView;
|
|
2022
|
+
}
|
|
2023
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (isTrue3D) {
|
|
2027
|
+
const texW = Math.max(1, Math.ceil(layerW));
|
|
2028
|
+
const texH = Math.max(1, Math.ceil(layerH));
|
|
2029
|
+
const groupExcludes = [
|
|
2030
|
+
activeTex,
|
|
2031
|
+
targetTexture,
|
|
2032
|
+
...excludes,
|
|
2033
|
+
...pending3DItems
|
|
2034
|
+
.map((q) => q.texture)
|
|
2035
|
+
.filter((t): t is GPUTexture => Boolean(t)),
|
|
2036
|
+
];
|
|
2037
|
+
const groupTex = ctx.renderer.getTemporaryTexture(
|
|
2038
|
+
texW,
|
|
2039
|
+
texH,
|
|
2040
|
+
groupExcludes,
|
|
2041
|
+
);
|
|
2042
|
+
const groupView = groupTex.createView();
|
|
2043
|
+
|
|
2044
|
+
const groupClearPass = ctx.renderer.beginFrame(
|
|
2045
|
+
encoder,
|
|
2046
|
+
groupView,
|
|
2047
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2048
|
+
texW,
|
|
2049
|
+
texH,
|
|
2050
|
+
"clear",
|
|
2051
|
+
);
|
|
2052
|
+
groupClearPass.end();
|
|
2053
|
+
|
|
2054
|
+
const br = Math.min(
|
|
2055
|
+
lop.borderRadius ?? lop.strokeRadius ?? 0,
|
|
2056
|
+
layerW / 2,
|
|
2057
|
+
layerH / 2,
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
if (shouldPaintContainerBackground(kind, lop.background)) {
|
|
2061
|
+
const bgPass = ctx.renderer.beginFrame(
|
|
2062
|
+
encoder,
|
|
2063
|
+
groupView,
|
|
2064
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2065
|
+
texW,
|
|
2066
|
+
texH,
|
|
2067
|
+
"load",
|
|
2068
|
+
);
|
|
2069
|
+
ctx.renderer.drawRect(
|
|
2070
|
+
bgPass,
|
|
2071
|
+
{ x: 0, y: 0, width: layerW, height: layerH },
|
|
2072
|
+
lop.background,
|
|
2073
|
+
0,
|
|
2074
|
+
{ opacity: 1 },
|
|
2075
|
+
);
|
|
2076
|
+
bgPass.end();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
let activeGroupTex = groupTex;
|
|
2080
|
+
let activeGroupView = groupView;
|
|
2081
|
+
if (nodeVM.children && nodeVM.children.length > 0) {
|
|
2082
|
+
const res = await renderNodeTree(
|
|
2083
|
+
nodeVM.children,
|
|
2084
|
+
groupTex,
|
|
2085
|
+
groupView,
|
|
2086
|
+
[groupTex, ...groupExcludes],
|
|
2087
|
+
1.0,
|
|
2088
|
+
new DOMMatrix(),
|
|
2089
|
+
rect,
|
|
2090
|
+
texW,
|
|
2091
|
+
texH,
|
|
2092
|
+
null,
|
|
2093
|
+
false,
|
|
2094
|
+
);
|
|
2095
|
+
activeGroupTex = res.finalTex;
|
|
2096
|
+
activeGroupView = res.finalView;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
const borderResult =
|
|
2100
|
+
!isTrue3D && lop.borderWidth !== undefined && lop.borderColor
|
|
2101
|
+
? createBorderTexture(
|
|
2102
|
+
ctx,
|
|
2103
|
+
encoder,
|
|
2104
|
+
layerW,
|
|
2105
|
+
layerH,
|
|
2106
|
+
br,
|
|
2107
|
+
lop.borderWidth,
|
|
2108
|
+
lop.borderColor,
|
|
2109
|
+
lop.strokeAlign ?? "inside",
|
|
2110
|
+
[activeGroupTex, ...groupExcludes],
|
|
2111
|
+
)
|
|
2112
|
+
: null;
|
|
2113
|
+
|
|
2114
|
+
if (borderResult) {
|
|
2115
|
+
const borderPass = ctx.renderer.beginFrame(
|
|
2116
|
+
encoder,
|
|
2117
|
+
activeGroupView,
|
|
2118
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2119
|
+
texW,
|
|
2120
|
+
texH,
|
|
2121
|
+
"load",
|
|
2122
|
+
);
|
|
2123
|
+
ctx.renderer.drawTexture(
|
|
2124
|
+
borderPass,
|
|
2125
|
+
borderResult.borderTex,
|
|
2126
|
+
{
|
|
2127
|
+
x: borderResult.ox,
|
|
2128
|
+
y: borderResult.oy,
|
|
2129
|
+
width: borderResult.ow,
|
|
2130
|
+
height: borderResult.oh,
|
|
2131
|
+
},
|
|
2132
|
+
{ opacity: 1 },
|
|
2133
|
+
);
|
|
2134
|
+
borderPass.end();
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
let modelMatrix: Mat4 | undefined;
|
|
2138
|
+
if (parent3DMatrix) {
|
|
2139
|
+
let localM = Matrix4Math.identity();
|
|
2140
|
+
const childX = target?.x ?? lop.x ?? 0;
|
|
2141
|
+
const childY = target?.y ?? lop.y ?? 0;
|
|
2142
|
+
const childZ = target?.z ?? lop.z ?? 0;
|
|
2143
|
+
localM = Matrix4Math.multiply(
|
|
2144
|
+
localM,
|
|
2145
|
+
Matrix4Math.translate(childX, childY, childZ),
|
|
2146
|
+
);
|
|
2147
|
+
const rxRad =
|
|
2148
|
+
((target?.rotateX ?? lop.rotateX ?? 0) * Math.PI) / 180;
|
|
2149
|
+
const ryRad =
|
|
2150
|
+
((target?.rotateY ?? lop.rotateY ?? 0) * Math.PI) / 180;
|
|
2151
|
+
const rzRad =
|
|
2152
|
+
((target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
2153
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
2154
|
+
: rotation) *
|
|
2155
|
+
(Math.PI / 180);
|
|
2156
|
+
if (rzRad !== 0)
|
|
2157
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateZ(rzRad));
|
|
2158
|
+
if (ryRad !== 0)
|
|
2159
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateY(ryRad));
|
|
2160
|
+
if (rxRad !== 0)
|
|
2161
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateX(rxRad));
|
|
2162
|
+
|
|
2163
|
+
const sx = layerW * scale;
|
|
2164
|
+
const sy = layerH * scale;
|
|
2165
|
+
const sz = target?.scaleZ ?? lop.scaleZ ?? 1;
|
|
2166
|
+
localM = Matrix4Math.multiply(
|
|
2167
|
+
localM,
|
|
2168
|
+
Matrix4Math.scale(sx, sy, sz),
|
|
2169
|
+
);
|
|
2170
|
+
|
|
2171
|
+
const ax = lop.anchorX ?? 0.5;
|
|
2172
|
+
const ay = lop.anchorY ?? 0.5;
|
|
2173
|
+
const pivotX = 0.5 - ax;
|
|
2174
|
+
const pivotY = 0.5 - ay;
|
|
2175
|
+
if (pivotX !== 0 || pivotY !== 0) {
|
|
2176
|
+
localM = Matrix4Math.multiply(
|
|
2177
|
+
localM,
|
|
2178
|
+
Matrix4Math.translate(pivotX, pivotY, 0),
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
modelMatrix = Matrix4Math.multiply(parent3DMatrix, localM);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
const quadOpts: DrawQuad3DOpts = {
|
|
2186
|
+
position: [
|
|
2187
|
+
localX + layerW * 0.5,
|
|
2188
|
+
localY + layerH * 0.5,
|
|
2189
|
+
target?.z ?? lop.z ?? 0,
|
|
2190
|
+
],
|
|
2191
|
+
width: layerW,
|
|
2192
|
+
height: layerH,
|
|
2193
|
+
textureExtent: [
|
|
2194
|
+
layerW / activeGroupTex.width,
|
|
2195
|
+
layerH / activeGroupTex.height,
|
|
2196
|
+
],
|
|
2197
|
+
rotation: [
|
|
2198
|
+
target?.rotateX ?? lop.rotateX ?? 0,
|
|
2199
|
+
target?.rotateY ?? lop.rotateY ?? 0,
|
|
2200
|
+
(target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
2201
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
2202
|
+
: rotation,
|
|
2203
|
+
],
|
|
2204
|
+
scale: [scale, scale, target?.scaleZ ?? lop.scaleZ ?? 1],
|
|
2205
|
+
anchor: [lop.anchorX ?? 0.5, lop.anchorY ?? 0.5],
|
|
2206
|
+
borderRadius: br,
|
|
2207
|
+
borderWidth: lop.borderWidth,
|
|
2208
|
+
borderColor: lop.borderColor,
|
|
2209
|
+
opacity: layerOpacity,
|
|
2210
|
+
twoSided: target?.twoSided ?? lop.twoSided ?? true,
|
|
2211
|
+
material: target?.material ?? lop.material,
|
|
2212
|
+
shininess: target?.shininess ?? lop.shininess,
|
|
2213
|
+
roughness: target?.roughness ?? lop.roughness,
|
|
2214
|
+
specularIntensity:
|
|
2215
|
+
target?.specularIntensity ?? lop.specularIntensity,
|
|
2216
|
+
ambientIntensity: target?.ambientIntensity ?? lop.ambientIntensity,
|
|
2217
|
+
metallic: target?.metallic ?? lop.metallic,
|
|
2218
|
+
...(modelMatrix && { modelMatrix }),
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
pending3DItems.push({
|
|
2222
|
+
kind: "quad",
|
|
2223
|
+
texture: activeGroupTex,
|
|
2224
|
+
opts: quadOpts,
|
|
2225
|
+
});
|
|
2226
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if (is3D && quad3D) {
|
|
2230
|
+
const texW = Math.max(1, Math.ceil(layerW));
|
|
2231
|
+
const texH = Math.max(1, Math.ceil(layerH));
|
|
2232
|
+
const groupExcludes = [activeTex, targetTexture, ...excludes];
|
|
2233
|
+
const groupTex = ctx.renderer.getTemporaryTexture(
|
|
2234
|
+
texW,
|
|
2235
|
+
texH,
|
|
2236
|
+
groupExcludes,
|
|
2237
|
+
);
|
|
2238
|
+
const groupView = groupTex.createView();
|
|
2239
|
+
|
|
2240
|
+
const groupClearPass = ctx.renderer.beginFrame(
|
|
2241
|
+
encoder,
|
|
2242
|
+
groupView,
|
|
2243
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2244
|
+
texW,
|
|
2245
|
+
texH,
|
|
2246
|
+
"clear",
|
|
2247
|
+
);
|
|
2248
|
+
groupClearPass.end();
|
|
2249
|
+
|
|
2250
|
+
const br = Math.min(
|
|
2251
|
+
lop.borderRadius ?? lop.strokeRadius ?? 0,
|
|
2252
|
+
layerW / 2,
|
|
2253
|
+
layerH / 2,
|
|
2254
|
+
);
|
|
2255
|
+
|
|
2256
|
+
if (shouldPaintContainerBackground(kind, lop.background)) {
|
|
2257
|
+
const bgPass = ctx.renderer.beginFrame(
|
|
2258
|
+
encoder,
|
|
2259
|
+
groupView,
|
|
2260
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2261
|
+
texW,
|
|
2262
|
+
texH,
|
|
2263
|
+
"load",
|
|
2264
|
+
);
|
|
2265
|
+
ctx.renderer.drawRect(
|
|
2266
|
+
bgPass,
|
|
2267
|
+
{ x: 0, y: 0, width: layerW, height: layerH },
|
|
2268
|
+
lop.background,
|
|
2269
|
+
br,
|
|
2270
|
+
{ opacity: 1 },
|
|
2271
|
+
);
|
|
2272
|
+
bgPass.end();
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
let activeGroupTex = groupTex;
|
|
2276
|
+
let activeGroupView = groupView;
|
|
2277
|
+
if (nodeVM.children && nodeVM.children.length > 0) {
|
|
2278
|
+
const res = await renderNodeTree(
|
|
2279
|
+
nodeVM.children,
|
|
2280
|
+
groupTex,
|
|
2281
|
+
groupView,
|
|
2282
|
+
[groupTex, ...groupExcludes],
|
|
2283
|
+
1.0,
|
|
2284
|
+
new DOMMatrix(),
|
|
2285
|
+
rect,
|
|
2286
|
+
texW,
|
|
2287
|
+
texH,
|
|
2288
|
+
null,
|
|
2289
|
+
false,
|
|
2290
|
+
);
|
|
2291
|
+
activeGroupTex = res.finalTex;
|
|
2292
|
+
activeGroupView = res.finalView;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const borderResult =
|
|
2296
|
+
lop.borderWidth !== undefined && lop.borderColor
|
|
2297
|
+
? createBorderTexture(
|
|
2298
|
+
ctx,
|
|
2299
|
+
encoder,
|
|
2300
|
+
layerW,
|
|
2301
|
+
layerH,
|
|
2302
|
+
br,
|
|
2303
|
+
lop.borderWidth,
|
|
2304
|
+
lop.borderColor,
|
|
2305
|
+
lop.strokeAlign ?? "inside",
|
|
2306
|
+
[activeGroupTex, ...groupExcludes],
|
|
2307
|
+
)
|
|
2308
|
+
: null;
|
|
2309
|
+
|
|
2310
|
+
if (borderResult) {
|
|
2311
|
+
const borderPass = ctx.renderer.beginFrame(
|
|
2312
|
+
encoder,
|
|
2313
|
+
activeGroupView,
|
|
2314
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2315
|
+
texW,
|
|
2316
|
+
texH,
|
|
2317
|
+
"load",
|
|
2318
|
+
);
|
|
2319
|
+
ctx.renderer.drawTexture(
|
|
2320
|
+
borderPass,
|
|
2321
|
+
borderResult.borderTex,
|
|
2322
|
+
{
|
|
2323
|
+
x: borderResult.ox,
|
|
2324
|
+
y: borderResult.oy,
|
|
2325
|
+
width: borderResult.ow,
|
|
2326
|
+
height: borderResult.oh,
|
|
2327
|
+
},
|
|
2328
|
+
{ opacity: 1 },
|
|
2329
|
+
);
|
|
2330
|
+
borderPass.end();
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
const container3DShutter =
|
|
2334
|
+
target?.motionBlurShutter ?? lop.motionBlurShutter ?? 0;
|
|
2335
|
+
if (container3DShutter > 0 && fps && shiftedFrame !== undefined) {
|
|
2336
|
+
const currentT = shiftedFrame / fps;
|
|
2337
|
+
const prevT = (shiftedFrame - 1) / fps;
|
|
2338
|
+
tl.seek(prevT);
|
|
2339
|
+
const prevTarget = targetId ? targetsById[targetId] : undefined;
|
|
2340
|
+
const prevX = prevTarget?.x ?? baseX;
|
|
2341
|
+
const prevY = prevTarget?.y ?? baseY;
|
|
2342
|
+
tl.seek(currentT);
|
|
2343
|
+
|
|
2344
|
+
const vx = sampledX - prevX;
|
|
2345
|
+
const vy = sampledY - prevY;
|
|
2346
|
+
|
|
2347
|
+
if (Math.hypot(vx, vy) > 0.5) {
|
|
2348
|
+
activeGroupTex = ctx.renderer.applyMotionBlur(
|
|
2349
|
+
encoder,
|
|
2350
|
+
activeGroupTex,
|
|
2351
|
+
vx,
|
|
2352
|
+
vy,
|
|
2353
|
+
container3DShutter,
|
|
2354
|
+
16,
|
|
2355
|
+
[activeTex, targetTexture, ...excludes],
|
|
2356
|
+
);
|
|
2357
|
+
activeGroupView = activeGroupTex.createView();
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (blendMode === "normal" || blendMode === "source-over") {
|
|
2362
|
+
drawProjectedQuad(
|
|
2363
|
+
ctx,
|
|
2364
|
+
encoder,
|
|
2365
|
+
activeView,
|
|
2366
|
+
activeGroupTex,
|
|
2367
|
+
quad3D,
|
|
2368
|
+
targetW,
|
|
2369
|
+
targetH,
|
|
2370
|
+
layerOpacity,
|
|
2371
|
+
);
|
|
2372
|
+
} else {
|
|
2373
|
+
const compositeExcludes = [
|
|
2374
|
+
activeTex,
|
|
2375
|
+
targetTexture,
|
|
2376
|
+
activeGroupTex,
|
|
2377
|
+
...excludes,
|
|
2378
|
+
];
|
|
2379
|
+
if (borderResult) compositeExcludes.push(borderResult.borderTex);
|
|
2380
|
+
const layerTex = ctx.renderer.getTemporaryTexture(
|
|
2381
|
+
targetW,
|
|
2382
|
+
targetH,
|
|
2383
|
+
compositeExcludes,
|
|
2384
|
+
);
|
|
2385
|
+
const layerView = layerTex.createView();
|
|
2386
|
+
const layerClearPass = ctx.renderer.beginFrame(
|
|
2387
|
+
encoder,
|
|
2388
|
+
layerView,
|
|
2389
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2390
|
+
targetW,
|
|
2391
|
+
targetH,
|
|
2392
|
+
"clear",
|
|
2393
|
+
);
|
|
2394
|
+
layerClearPass.end();
|
|
2395
|
+
|
|
2396
|
+
drawProjectedQuad(
|
|
2397
|
+
ctx,
|
|
2398
|
+
encoder,
|
|
2399
|
+
layerView,
|
|
2400
|
+
activeGroupTex,
|
|
2401
|
+
quad3D,
|
|
2402
|
+
targetW,
|
|
2403
|
+
targetH,
|
|
2404
|
+
layerOpacity,
|
|
2405
|
+
);
|
|
2406
|
+
|
|
2407
|
+
activeTex = ctx.renderer.composite(
|
|
2408
|
+
encoder,
|
|
2409
|
+
activeTex,
|
|
2410
|
+
layerTex,
|
|
2411
|
+
blendMode,
|
|
2412
|
+
[targetTexture, ...excludes],
|
|
2413
|
+
);
|
|
2414
|
+
activeView = activeTex.createView();
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
2418
|
+
}
|
|
2419
|
+
const shouldClip = lop.overflow !== "visible";
|
|
2420
|
+
const scissorRect = shouldClip
|
|
2421
|
+
? computeContainerScissor(layerMatrix, layerW, layerH)
|
|
2422
|
+
: null;
|
|
2423
|
+
// A scissor clips to the box; rounded corners need a mask pass.
|
|
2424
|
+
const clipRadius = shouldClip
|
|
2425
|
+
? Math.min(
|
|
2426
|
+
target?.borderRadius ?? lop.borderRadius ?? 0,
|
|
2427
|
+
layerW / 2,
|
|
2428
|
+
layerH / 2,
|
|
2429
|
+
)
|
|
2430
|
+
: 0;
|
|
2431
|
+
|
|
2432
|
+
// Transparent optimized flex/block — only when there is no
|
|
2433
|
+
// background to paint (a flex/block with a background takes the
|
|
2434
|
+
// group-texture path below).
|
|
2435
|
+
if (
|
|
2436
|
+
(kind === "flex" || kind === "block") &&
|
|
2437
|
+
clipRadius <= 0 &&
|
|
2438
|
+
!shouldPaintContainerBackground(kind, lop.background) &&
|
|
2439
|
+
layerOpacity === 1 &&
|
|
2440
|
+
blendMode === "normal" &&
|
|
2441
|
+
!lop.borderWidth &&
|
|
2442
|
+
!lop.borderColor &&
|
|
2443
|
+
(typeof lop.relighting !== "object" || !lop.relighting) &&
|
|
2444
|
+
(typeof lop.relight !== "object" || !lop.relight) &&
|
|
2445
|
+
(typeof target?.relighting !== "object" || !target?.relighting) &&
|
|
2446
|
+
(typeof target?.relight !== "object" || !target?.relight) &&
|
|
2447
|
+
(typeof lop.deflicker !== "object" || !lop.deflicker) &&
|
|
2448
|
+
(typeof target?.deflicker !== "object" || !target?.deflicker)
|
|
2449
|
+
) {
|
|
2450
|
+
if (scissorRect) {
|
|
2451
|
+
ctx.renderer.pushScissor(scissorRect);
|
|
2452
|
+
}
|
|
2453
|
+
const res = await renderNodeTree(
|
|
2454
|
+
nodeVM.children ?? [],
|
|
2455
|
+
activeTex,
|
|
2456
|
+
activeView,
|
|
2457
|
+
excludes,
|
|
2458
|
+
accumulatedOpacity,
|
|
2459
|
+
layerMatrix,
|
|
2460
|
+
rect,
|
|
2461
|
+
targetW,
|
|
2462
|
+
targetH,
|
|
2463
|
+
null,
|
|
2464
|
+
isRootScene,
|
|
2465
|
+
);
|
|
2466
|
+
if (scissorRect) {
|
|
2467
|
+
ctx.renderer.popScissor();
|
|
2468
|
+
}
|
|
2469
|
+
return res;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// Otherwise, draw container background + children + border onto a temporary group texture,
|
|
2473
|
+
// and composite/blend the group texture onto destTex.
|
|
2474
|
+
const groupExcludes = [activeTex, targetTexture, ...excludes];
|
|
2475
|
+
const groupTex = ctx.renderer.getTemporaryTexture(
|
|
2476
|
+
targetW,
|
|
2477
|
+
targetH,
|
|
2478
|
+
groupExcludes,
|
|
2479
|
+
);
|
|
2480
|
+
const groupView = groupTex.createView();
|
|
2481
|
+
|
|
2482
|
+
const groupClearPass = ctx.renderer.beginFrame(
|
|
2483
|
+
encoder,
|
|
2484
|
+
groupView,
|
|
2485
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2486
|
+
targetW,
|
|
2487
|
+
targetH,
|
|
2488
|
+
"clear",
|
|
2489
|
+
);
|
|
2490
|
+
groupClearPass.end();
|
|
2491
|
+
|
|
2492
|
+
const br = Math.min(
|
|
2493
|
+
target?.borderRadius ?? lop.borderRadius ?? lop.strokeRadius ?? 0,
|
|
2494
|
+
layerW / 2,
|
|
2495
|
+
layerH / 2,
|
|
2496
|
+
);
|
|
2497
|
+
|
|
2498
|
+
// Background fill pass — any container kind (flex/block/box).
|
|
2499
|
+
if (shouldPaintContainerBackground(kind, lop.background)) {
|
|
2500
|
+
const bgPass = ctx.renderer.beginFrame(
|
|
2501
|
+
encoder,
|
|
2502
|
+
groupView,
|
|
2503
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2504
|
+
targetW,
|
|
2505
|
+
targetH,
|
|
2506
|
+
"load",
|
|
2507
|
+
);
|
|
2508
|
+
ctx.renderer.pushTransform(layerMatrix);
|
|
2509
|
+
ctx.renderer.drawRect(
|
|
2510
|
+
bgPass,
|
|
2511
|
+
{ x: 0, y: 0, width: layerW, height: layerH },
|
|
2512
|
+
lop.background,
|
|
2513
|
+
br,
|
|
2514
|
+
{ opacity: layerOpacity },
|
|
2515
|
+
);
|
|
2516
|
+
ctx.renderer.popTransform();
|
|
2517
|
+
bgPass.end();
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
let activeGroupTex = groupTex;
|
|
2521
|
+
let activeGroupView = groupView;
|
|
2522
|
+
if (nodeVM.children && nodeVM.children.length > 0) {
|
|
2523
|
+
if (scissorRect) {
|
|
2524
|
+
ctx.renderer.pushScissor(scissorRect);
|
|
2525
|
+
}
|
|
2526
|
+
const res = await renderNodeTree(
|
|
2527
|
+
nodeVM.children,
|
|
2528
|
+
groupTex,
|
|
2529
|
+
groupView,
|
|
2530
|
+
[groupTex, ...groupExcludes],
|
|
2531
|
+
layerOpacity,
|
|
2532
|
+
layerMatrix,
|
|
2533
|
+
rect,
|
|
2534
|
+
targetW,
|
|
2535
|
+
targetH,
|
|
2536
|
+
null,
|
|
2537
|
+
false,
|
|
2538
|
+
);
|
|
2539
|
+
if (scissorRect) {
|
|
2540
|
+
ctx.renderer.popScissor();
|
|
2541
|
+
}
|
|
2542
|
+
activeGroupTex = res.finalTex;
|
|
2543
|
+
activeGroupView = res.finalView;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// overflow: hidden with a border radius: keep only what falls
|
|
2547
|
+
// inside the rounded box (background and children alike).
|
|
2548
|
+
if (clipRadius > 0) {
|
|
2549
|
+
const maskTex = ctx.renderer.getTemporaryTexture(targetW, targetH, [
|
|
2550
|
+
activeGroupTex,
|
|
2551
|
+
...groupExcludes,
|
|
2552
|
+
]);
|
|
2553
|
+
const maskPass = ctx.renderer.beginFrame(
|
|
2554
|
+
encoder,
|
|
2555
|
+
maskTex.createView(),
|
|
2556
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2557
|
+
targetW,
|
|
2558
|
+
targetH,
|
|
2559
|
+
"clear",
|
|
2560
|
+
);
|
|
2561
|
+
ctx.renderer.pushTransform(layerMatrix);
|
|
2562
|
+
ctx.renderer.drawRect(
|
|
2563
|
+
maskPass,
|
|
2564
|
+
{ x: 0, y: 0, width: layerW, height: layerH },
|
|
2565
|
+
"#ffffff",
|
|
2566
|
+
clipRadius,
|
|
2567
|
+
);
|
|
2568
|
+
ctx.renderer.popTransform();
|
|
2569
|
+
maskPass.end();
|
|
2570
|
+
activeGroupTex = ctx.renderer.composite(
|
|
2571
|
+
encoder,
|
|
2572
|
+
activeGroupTex,
|
|
2573
|
+
maskTex,
|
|
2574
|
+
"mask-in",
|
|
2575
|
+
[activeGroupTex, maskTex, ...groupExcludes],
|
|
2576
|
+
);
|
|
2577
|
+
activeGroupView = activeGroupTex.createView();
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
const borderResult =
|
|
2581
|
+
lop.borderWidth !== undefined && lop.borderColor
|
|
2582
|
+
? createBorderTexture(
|
|
2583
|
+
ctx,
|
|
2584
|
+
encoder,
|
|
2585
|
+
layerW,
|
|
2586
|
+
layerH,
|
|
2587
|
+
br,
|
|
2588
|
+
lop.borderWidth,
|
|
2589
|
+
lop.borderColor,
|
|
2590
|
+
lop.strokeAlign ?? "inside",
|
|
2591
|
+
[activeGroupTex, ...groupExcludes],
|
|
2592
|
+
)
|
|
2593
|
+
: null;
|
|
2594
|
+
|
|
2595
|
+
if (borderResult) {
|
|
2596
|
+
const borderPass = ctx.renderer.beginFrame(
|
|
2597
|
+
encoder,
|
|
2598
|
+
activeGroupView,
|
|
2599
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2600
|
+
targetW,
|
|
2601
|
+
targetH,
|
|
2602
|
+
"load",
|
|
2603
|
+
);
|
|
2604
|
+
ctx.renderer.pushTransform(layerMatrix);
|
|
2605
|
+
ctx.renderer.drawTexture(
|
|
2606
|
+
borderPass,
|
|
2607
|
+
borderResult.borderTex,
|
|
2608
|
+
{
|
|
2609
|
+
x: borderResult.ox,
|
|
2610
|
+
y: borderResult.oy,
|
|
2611
|
+
width: borderResult.ow,
|
|
2612
|
+
height: borderResult.oh,
|
|
2613
|
+
},
|
|
2614
|
+
{ opacity: layerOpacity },
|
|
2615
|
+
);
|
|
2616
|
+
ctx.renderer.popTransform();
|
|
2617
|
+
borderPass.end();
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
const containerShutter =
|
|
2621
|
+
target?.motionBlurShutter ?? lop.motionBlurShutter ?? 0;
|
|
2622
|
+
if (containerShutter > 0 && fps && shiftedFrame !== undefined) {
|
|
2623
|
+
const currentT = shiftedFrame / fps;
|
|
2624
|
+
const prevT = (shiftedFrame - 1) / fps;
|
|
2625
|
+
tl.seek(prevT);
|
|
2626
|
+
const prevTarget = targetId ? targetsById[targetId] : undefined;
|
|
2627
|
+
const prevX = prevTarget?.x ?? baseX;
|
|
2628
|
+
const prevY = prevTarget?.y ?? baseY;
|
|
2629
|
+
tl.seek(currentT);
|
|
2630
|
+
|
|
2631
|
+
const vx = sampledX - prevX;
|
|
2632
|
+
const vy = sampledY - prevY;
|
|
2633
|
+
|
|
2634
|
+
if (Math.hypot(vx, vy) > 0.5) {
|
|
2635
|
+
activeGroupTex = ctx.renderer.applyMotionBlur(
|
|
2636
|
+
encoder,
|
|
2637
|
+
activeGroupTex,
|
|
2638
|
+
vx,
|
|
2639
|
+
vy,
|
|
2640
|
+
containerShutter,
|
|
2641
|
+
16,
|
|
2642
|
+
[activeTex, targetTexture, ...excludes],
|
|
2643
|
+
);
|
|
2644
|
+
activeGroupView = activeGroupTex.createView();
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Temporal De-flickering & Optical Flow Warping
|
|
2649
|
+
const deflickerFromEffects = Array.isArray(lop.effects)
|
|
2650
|
+
? lop.effects.find(
|
|
2651
|
+
(e: unknown) =>
|
|
2652
|
+
(e as { op?: string })?.op === "TemporalDeflicker" ||
|
|
2653
|
+
(e as { op?: string })?.op === "Deflicker" ||
|
|
2654
|
+
(e as { constructor?: { name?: string } })?.constructor
|
|
2655
|
+
?.name === "TemporalDeflicker",
|
|
2656
|
+
)
|
|
2657
|
+
: undefined;
|
|
2658
|
+
const rawDeflicker =
|
|
2659
|
+
target?.deflicker ?? lop.deflicker ?? deflickerFromEffects;
|
|
2660
|
+
const deflickerOpts =
|
|
2661
|
+
typeof rawDeflicker === "object" && rawDeflicker !== null
|
|
2662
|
+
? rawDeflicker
|
|
2663
|
+
: undefined;
|
|
2664
|
+
if (deflickerOpts) {
|
|
2665
|
+
const deflickerPipe = getTemporalDeflickerPipeline(
|
|
2666
|
+
ctx.device,
|
|
2667
|
+
lop.id || `layer_${i}`,
|
|
2668
|
+
);
|
|
2669
|
+
if (shiftedFrame === 0) {
|
|
2670
|
+
deflickerPipe.reset();
|
|
2671
|
+
}
|
|
2672
|
+
activeGroupTex = deflickerPipe.execute(
|
|
2673
|
+
activeGroupTex,
|
|
2674
|
+
deflickerOpts,
|
|
2675
|
+
encoder,
|
|
2676
|
+
);
|
|
2677
|
+
activeGroupView = activeGroupTex.createView();
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// Screen-Space 3D Relighting
|
|
2681
|
+
const relightEffect = Array.isArray(lop.effects)
|
|
2682
|
+
? lop.effects.find(
|
|
2683
|
+
(e: unknown) =>
|
|
2684
|
+
(e as { op?: string })?.op === "Relight3D" ||
|
|
2685
|
+
(e as { constructor?: { name?: string } })?.constructor
|
|
2686
|
+
?.name === "Relight3D",
|
|
2687
|
+
)
|
|
2688
|
+
: undefined;
|
|
2689
|
+
const rawRelight =
|
|
2690
|
+
target?.relighting ??
|
|
2691
|
+
lop.relighting ??
|
|
2692
|
+
(typeof lop.relight === "object" ? lop.relight : undefined) ??
|
|
2693
|
+
relightEffect;
|
|
2694
|
+
const relightOpts =
|
|
2695
|
+
typeof rawRelight === "object" && rawRelight !== null
|
|
2696
|
+
? rawRelight
|
|
2697
|
+
: undefined;
|
|
2698
|
+
if (relightOpts) {
|
|
2699
|
+
const relightPipeline = getScreenSpaceRelightPipeline(
|
|
2700
|
+
ctx.device,
|
|
2701
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
2702
|
+
);
|
|
2703
|
+
const r3dInstance = getRenderer3D(
|
|
2704
|
+
ctx.device,
|
|
2705
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
2706
|
+
sampleCount3d,
|
|
2707
|
+
);
|
|
2708
|
+
|
|
2709
|
+
const activeLights = synthesizeRelightLights(
|
|
2710
|
+
relightOpts as Record<string, unknown>,
|
|
2711
|
+
sceneLights,
|
|
2712
|
+
rect,
|
|
2713
|
+
shiftedFrame,
|
|
2714
|
+
fps ?? 24,
|
|
2715
|
+
);
|
|
2716
|
+
|
|
2717
|
+
const lightsBuffer =
|
|
2718
|
+
r3dInstance.getOrCreateLightsBuffer(activeLights);
|
|
2719
|
+
|
|
2720
|
+
const relitTex = ctx.renderer.getTemporaryTexture(targetW, targetH, [
|
|
2721
|
+
activeGroupTex,
|
|
2722
|
+
activeTex,
|
|
2723
|
+
targetTexture,
|
|
2724
|
+
...excludes,
|
|
2725
|
+
]);
|
|
2726
|
+
const relitView = relitTex.createView();
|
|
2727
|
+
|
|
2728
|
+
const normalTex = relightOpts.normalTexture ?? relightOpts.normalMap;
|
|
2729
|
+
|
|
2730
|
+
relightPipeline.execute(
|
|
2731
|
+
encoder,
|
|
2732
|
+
relitView,
|
|
2733
|
+
activeGroupTex,
|
|
2734
|
+
normalTex,
|
|
2735
|
+
lightsBuffer,
|
|
2736
|
+
{
|
|
2737
|
+
viewportWidth: targetW,
|
|
2738
|
+
viewportHeight: targetH,
|
|
2739
|
+
layerX: 0,
|
|
2740
|
+
layerY: 0,
|
|
2741
|
+
roughness: resolveVal(
|
|
2742
|
+
target?.roughness ??
|
|
2743
|
+
target?.relightRoughness ??
|
|
2744
|
+
relightOpts.specularRoughness ??
|
|
2745
|
+
relightOpts.roughness,
|
|
2746
|
+
0.35,
|
|
2747
|
+
shiftedFrame,
|
|
2748
|
+
fps ?? 24,
|
|
2749
|
+
),
|
|
2750
|
+
specularStrength: resolveVal(
|
|
2751
|
+
target?.specularStrength ??
|
|
2752
|
+
target?.relightSpecularStrength ??
|
|
2753
|
+
relightOpts.specularStrength,
|
|
2754
|
+
0.7,
|
|
2755
|
+
shiftedFrame,
|
|
2756
|
+
fps ?? 24,
|
|
2757
|
+
),
|
|
2758
|
+
metallic: resolveVal(
|
|
2759
|
+
target?.metallic ??
|
|
2760
|
+
target?.relightMetallic ??
|
|
2761
|
+
relightOpts.metallic,
|
|
2762
|
+
0.0,
|
|
2763
|
+
shiftedFrame,
|
|
2764
|
+
fps ?? 24,
|
|
2765
|
+
),
|
|
2766
|
+
ambientIntensity: resolveVal(
|
|
2767
|
+
target?.ambientIntensity ??
|
|
2768
|
+
target?.relightAmbientIntensity ??
|
|
2769
|
+
relightOpts.ambientIntensity,
|
|
2770
|
+
0.15,
|
|
2771
|
+
shiftedFrame,
|
|
2772
|
+
fps ?? 24,
|
|
2773
|
+
),
|
|
2774
|
+
depthScale: resolveVal(
|
|
2775
|
+
target?.depthScale ??
|
|
2776
|
+
target?.relightDepthScale ??
|
|
2777
|
+
relightOpts.depthScale,
|
|
2778
|
+
1.0,
|
|
2779
|
+
shiftedFrame,
|
|
2780
|
+
fps ?? 24,
|
|
2781
|
+
),
|
|
2782
|
+
opacity: 1.0,
|
|
2783
|
+
},
|
|
2784
|
+
"clear",
|
|
2785
|
+
);
|
|
2786
|
+
|
|
2787
|
+
activeGroupTex = relitTex;
|
|
2788
|
+
activeGroupView = relitView;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// Always composite container groups using the composite shader
|
|
2792
|
+
const compositeExcludes = [activeTex, targetTexture, ...excludes];
|
|
2793
|
+
if (borderResult) compositeExcludes.push(borderResult.borderTex);
|
|
2794
|
+
const nextDestTex = ctx.renderer.composite(
|
|
2795
|
+
encoder,
|
|
2796
|
+
activeTex,
|
|
2797
|
+
activeGroupTex,
|
|
2798
|
+
blendMode,
|
|
2799
|
+
compositeExcludes,
|
|
2800
|
+
);
|
|
2801
|
+
activeTex = nextDestTex;
|
|
2802
|
+
activeView = activeTex.createView();
|
|
2803
|
+
|
|
2804
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Leaf layers (text/media)
|
|
2808
|
+
if (!(layerW > 0 && layerH > 0))
|
|
2809
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
2810
|
+
|
|
2811
|
+
const br = Math.min(
|
|
2812
|
+
lop.borderRadius ?? lop.strokeRadius ?? 0,
|
|
2813
|
+
layerW / 2,
|
|
2814
|
+
layerH / 2,
|
|
2815
|
+
);
|
|
2816
|
+
const texW = Math.ceil(layerW);
|
|
2817
|
+
const texH = Math.ceil(layerH);
|
|
2818
|
+
|
|
2819
|
+
// Suspend any parent container scissors while rendering isolated offscreen localTex
|
|
2820
|
+
const savedScissors = ctx.renderer.suspendScissors?.() ?? [];
|
|
2821
|
+
|
|
2822
|
+
const localTex = ctx.renderer.getTemporaryTexture(texW, texH, [
|
|
2823
|
+
activeTex,
|
|
2824
|
+
targetTexture,
|
|
2825
|
+
...excludes,
|
|
2826
|
+
]);
|
|
2827
|
+
const localView = localTex.createView();
|
|
2828
|
+
|
|
2829
|
+
const localClearPass = ctx.renderer.beginFrame(
|
|
2830
|
+
encoder,
|
|
2831
|
+
localView,
|
|
2832
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
2833
|
+
texW,
|
|
2834
|
+
texH,
|
|
2835
|
+
"clear",
|
|
2836
|
+
);
|
|
2837
|
+
localClearPass.end();
|
|
2838
|
+
|
|
2839
|
+
// Content: media → bound source VM; text → synthetic text op.
|
|
2840
|
+
let content: VirtualMediaData | undefined;
|
|
2841
|
+
let isCaption = false;
|
|
2842
|
+
if (kind === "media") {
|
|
2843
|
+
content = nodeVM.children?.[0];
|
|
2844
|
+
isCaption =
|
|
2845
|
+
content?.operation?.dataType === "Caption" ||
|
|
2846
|
+
lop.dataType === "Caption" ||
|
|
2847
|
+
(content?.operation as any)?.srtText !== undefined;
|
|
2848
|
+
} else if (kind === "text") {
|
|
2849
|
+
const fullText = lop.text ?? "";
|
|
2850
|
+
let displayText = fullText;
|
|
2851
|
+
|
|
2852
|
+
// Kinetic typography animators (Section 5)
|
|
2853
|
+
let resolvedAnimators = rawAnimators;
|
|
2854
|
+
if (Array.isArray(rawAnimators) && rawAnimators.length > 0) {
|
|
2855
|
+
resolvedAnimators = rawAnimators.map((animator: any, idx: number) => {
|
|
2856
|
+
const a = {
|
|
2857
|
+
...animator,
|
|
2858
|
+
transform: { ...(animator.transform ?? {}) },
|
|
2859
|
+
};
|
|
2860
|
+
if (idx === 0) {
|
|
2861
|
+
const offsetVal =
|
|
2862
|
+
typeof target?.offset === "number"
|
|
2863
|
+
? target.offset
|
|
2864
|
+
: typeof target?.animatorOffset === "number"
|
|
2865
|
+
? target.animatorOffset
|
|
2866
|
+
: undefined;
|
|
2867
|
+
const rangeStartVal =
|
|
2868
|
+
typeof target?.rangeStart === "number"
|
|
2869
|
+
? target.rangeStart
|
|
2870
|
+
: typeof target?.animatorRangeStart === "number"
|
|
2871
|
+
? target.animatorRangeStart
|
|
2872
|
+
: undefined;
|
|
2873
|
+
const rangeEndVal =
|
|
2874
|
+
typeof target?.rangeEnd === "number"
|
|
2875
|
+
? target.rangeEnd
|
|
2876
|
+
: typeof target?.animatorRangeEnd === "number"
|
|
2877
|
+
? target.animatorRangeEnd
|
|
2878
|
+
: undefined;
|
|
2879
|
+
|
|
2880
|
+
if (offsetVal !== undefined) a.offset = offsetVal;
|
|
2881
|
+
if (rangeStartVal !== undefined) a.rangeStart = rangeStartVal;
|
|
2882
|
+
if (rangeEndVal !== undefined) a.rangeEnd = rangeEndVal;
|
|
2883
|
+
|
|
2884
|
+
if (Array.isArray(a.selectors)) {
|
|
2885
|
+
a.selectors = a.selectors.map((s: any) => {
|
|
2886
|
+
if (s.type === "range") {
|
|
2887
|
+
return {
|
|
2888
|
+
...s,
|
|
2889
|
+
...(offsetVal !== undefined ? { offset: offsetVal } : {}),
|
|
2890
|
+
...(rangeStartVal !== undefined
|
|
2891
|
+
? { start: rangeStartVal }
|
|
2892
|
+
: {}),
|
|
2893
|
+
...(rangeEndVal !== undefined
|
|
2894
|
+
? { end: rangeEndVal }
|
|
2895
|
+
: {}),
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
return s;
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return a;
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
const hasKineticAnimators =
|
|
2907
|
+
Array.isArray(resolvedAnimators) && resolvedAnimators.length > 0;
|
|
2908
|
+
const hasTypewriter = Boolean(lop.typewriter || target?.typewriter);
|
|
2909
|
+
|
|
2910
|
+
// Check if there is an active text track or progress value evaluated by GSAP
|
|
2911
|
+
const textTrack = lop.animation?.tracks?.find(
|
|
2912
|
+
(t: { prop: string }) => t.prop === "text",
|
|
2913
|
+
);
|
|
2914
|
+
const hasTextAnimation =
|
|
2915
|
+
!hasKineticAnimators &&
|
|
2916
|
+
!hasTypewriter &&
|
|
2917
|
+
(textTrack !== undefined || typeof target?.text === "number");
|
|
2918
|
+
|
|
2919
|
+
if (hasTextAnimation && fullText.length > 0) {
|
|
2920
|
+
const progress =
|
|
2921
|
+
typeof target?.text === "number"
|
|
2922
|
+
? Math.max(0, Math.min(1, target.text))
|
|
2923
|
+
: 1;
|
|
2924
|
+
|
|
2925
|
+
if (progress >= 1) {
|
|
2926
|
+
displayText = fullText;
|
|
2927
|
+
} else if (progress <= 0) {
|
|
2928
|
+
displayText = "";
|
|
2929
|
+
} else {
|
|
2930
|
+
// Determine preset style (default to typewriter)
|
|
2931
|
+
const presetType =
|
|
2932
|
+
textTrack?.keyframes?.find(
|
|
2933
|
+
(k: { presetType?: string }) => k.presetType,
|
|
2934
|
+
)?.presetType ?? "typewriter";
|
|
2935
|
+
|
|
2936
|
+
const textNumeric =
|
|
2937
|
+
typeof target?.text === "number" ? target.text : null;
|
|
2938
|
+
const isDirectCount = textNumeric !== null && textNumeric > 1.0001;
|
|
2939
|
+
|
|
2940
|
+
if (presetType === "word-reveal" || presetType === "karaoke") {
|
|
2941
|
+
const tokens = fullText.split(/(\s+)/);
|
|
2942
|
+
const wordIndices: number[] = [];
|
|
2943
|
+
tokens.forEach((token: string, idx: number) => {
|
|
2944
|
+
if (token.trim().length > 0) wordIndices.push(idx);
|
|
2945
|
+
});
|
|
2946
|
+
const visibleWordCount =
|
|
2947
|
+
isDirectCount && textNumeric !== null
|
|
2948
|
+
? Math.min(wordIndices.length, Math.round(textNumeric))
|
|
2949
|
+
: Math.round(wordIndices.length * progress);
|
|
2950
|
+
if (visibleWordCount === 0) {
|
|
2951
|
+
displayText = "";
|
|
2952
|
+
} else {
|
|
2953
|
+
const lastTokenIdx =
|
|
2954
|
+
wordIndices[
|
|
2955
|
+
Math.min(wordIndices.length - 1, visibleWordCount - 1)
|
|
2956
|
+
]!;
|
|
2957
|
+
displayText = tokens.slice(0, lastTokenIdx + 1).join("");
|
|
2958
|
+
}
|
|
2959
|
+
} else if (presetType === "line-reveal") {
|
|
2960
|
+
const lines = fullText.split("\n");
|
|
2961
|
+
const visibleLineCount =
|
|
2962
|
+
isDirectCount && textNumeric !== null
|
|
2963
|
+
? Math.min(lines.length, Math.round(textNumeric))
|
|
2964
|
+
: Math.round(lines.length * progress);
|
|
2965
|
+
displayText = lines
|
|
2966
|
+
.slice(0, Math.max(0, visibleLineCount))
|
|
2967
|
+
.join("\n");
|
|
2968
|
+
} else {
|
|
2969
|
+
// typewriter (character-level reveal)
|
|
2970
|
+
const visibleChars =
|
|
2971
|
+
isDirectCount && textNumeric !== null
|
|
2972
|
+
? Math.min(fullText.length, Math.round(textNumeric))
|
|
2973
|
+
: Math.round(fullText.length * progress);
|
|
2974
|
+
displayText = fullText.slice(0, visibleChars);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
content = {
|
|
2980
|
+
metadata: {},
|
|
2981
|
+
operation: {
|
|
2982
|
+
op: "text",
|
|
2983
|
+
text: displayText,
|
|
2984
|
+
animators: resolvedAnimators,
|
|
2985
|
+
pathOptions: target?.pathOptions ?? lop.pathOptions,
|
|
2986
|
+
typewriter: target?.typewriter ?? lop.typewriter,
|
|
2987
|
+
marquee: target?.marquee ?? (lop as { marquee?: unknown }).marquee,
|
|
2988
|
+
textProgress:
|
|
2989
|
+
typeof target?.text === "number" ? target.text : undefined,
|
|
2990
|
+
firstMargin: target?.firstMargin ?? lop.firstMargin,
|
|
2991
|
+
lastMargin: target?.lastMargin ?? lop.lastMargin,
|
|
2992
|
+
baselineShift: target?.baselineShift ?? lop.baselineShift,
|
|
2993
|
+
signals:
|
|
2994
|
+
(virtualMedia as any)?.operation?.signals ??
|
|
2995
|
+
(virtualMedia as any)?.signals,
|
|
2996
|
+
fontSize: target?.fontSize ?? lop.fontSize,
|
|
2997
|
+
fontFamily: lop.fontFamily,
|
|
2998
|
+
fontStyle: lop.fontStyle,
|
|
2999
|
+
fontWeight: lop.fontWeight,
|
|
3000
|
+
fill: target?.fill ?? target?.color ?? lop.fill,
|
|
3001
|
+
align:
|
|
3002
|
+
lop.align === "start"
|
|
3003
|
+
? "left"
|
|
3004
|
+
: lop.align === "end"
|
|
3005
|
+
? "right"
|
|
3006
|
+
: lop.align,
|
|
3007
|
+
verticalAlign: lop.verticalAlign,
|
|
3008
|
+
letterSpacing: target?.letterSpacing ?? lop.letterSpacing,
|
|
3009
|
+
lineHeight: lop.lineHeight,
|
|
3010
|
+
padding: lop.padding,
|
|
3011
|
+
textBackgroundColor:
|
|
3012
|
+
target?.textBackgroundColor ??
|
|
3013
|
+
target?.background ??
|
|
3014
|
+
lop.background,
|
|
3015
|
+
borderRadius: lop.borderRadius,
|
|
3016
|
+
stroke: target?.stroke ?? target?.strokeColor ?? lop.stroke,
|
|
3017
|
+
strokeWidth: target?.strokeWidth ?? lop.strokeWidth,
|
|
3018
|
+
strokeAlign: lop.strokeAlign,
|
|
3019
|
+
textShadow: lop.textShadow,
|
|
3020
|
+
shadows: lop.shadows,
|
|
3021
|
+
x: isPathText ? 0 : padX,
|
|
3022
|
+
y: isPathText ? 0 : padY,
|
|
3023
|
+
width: isPathText
|
|
3024
|
+
? targetW
|
|
3025
|
+
: lop.width !== undefined
|
|
3026
|
+
? rect.width
|
|
3027
|
+
: undefined,
|
|
3028
|
+
height: isPathText
|
|
3029
|
+
? targetH
|
|
3030
|
+
: lop.height !== undefined
|
|
3031
|
+
? rect.height
|
|
3032
|
+
: undefined,
|
|
3033
|
+
},
|
|
3034
|
+
children: [],
|
|
3035
|
+
} as unknown as VirtualMediaData;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// A true-3D text layer hands its glyphs to the 3D pass as outlines, to
|
|
3039
|
+
// be resolved per screen pixel on its plane: a texture of the text
|
|
3040
|
+
// blurs up close and aliases at a glancing angle. Whatever else the
|
|
3041
|
+
// layer draws (a backdrop, emoji) still lands in its texture.
|
|
3042
|
+
let glyphBatches: SlugGlyphBatch[] | null = null;
|
|
3043
|
+
let layerHasRaster = true;
|
|
3044
|
+
if (content) {
|
|
3045
|
+
const contentMeta = getActiveMediaMetadata(content);
|
|
3046
|
+
const contentDurationMs = contentMeta?.durationMs ?? composeDuration;
|
|
3047
|
+
const layerDurationFrames = Math.max(
|
|
3048
|
+
1,
|
|
3049
|
+
Math.round((contentDurationMs / 1000) * (fps ?? 24)),
|
|
3050
|
+
);
|
|
3051
|
+
const layerDurationInMS = (layerDurationFrames / (fps ?? 24)) * 1000;
|
|
3052
|
+
const contentIsImageCompositor =
|
|
3053
|
+
content.operation?.op === "Compositor" &&
|
|
3054
|
+
content.operation?.dataType === "Image";
|
|
3055
|
+
|
|
3056
|
+
const captureGlyphs = isTrue3D && kind === "text";
|
|
3057
|
+
const rasterDrawsBefore = ctx.renderer.rasterDrawCount;
|
|
3058
|
+
if (captureGlyphs) ctx.renderer.slugPipeline.beginCapture();
|
|
3059
|
+
try {
|
|
3060
|
+
await drawChild(
|
|
3061
|
+
content,
|
|
3062
|
+
{
|
|
3063
|
+
containerWidth: layerW,
|
|
3064
|
+
containerHeight: layerH,
|
|
3065
|
+
renderId: `${props.renderId}-l${i}`,
|
|
3066
|
+
durationMs: layerDurationInMS,
|
|
3067
|
+
opacity: 1.0,
|
|
3068
|
+
fps,
|
|
3069
|
+
// A media layer's effect chain (decode → passes) allocates from the
|
|
3070
|
+
// shared pool: keep it off the destination this layer composites onto,
|
|
3071
|
+
// or the raw frame lands there wherever the clip does not cover it.
|
|
3072
|
+
excludeTextures: [activeTex, targetTexture, ...excludes],
|
|
3073
|
+
isVideoMode: isVideoMode && !contentIsImageCompositor,
|
|
3074
|
+
...(isVideoMode && {
|
|
3075
|
+
frame:
|
|
3076
|
+
Math.max(0, shiftedFrame - (lop.startFrame ?? 0)) +
|
|
3077
|
+
(lop.mediaStartFrame ??
|
|
3078
|
+
lop.trimStartFrames ??
|
|
3079
|
+
(lop.trimStartSec != null
|
|
3080
|
+
? Math.round(lop.trimStartSec * (fps ?? 24))
|
|
3081
|
+
: 0)),
|
|
3082
|
+
}),
|
|
3083
|
+
...(isCaption && {
|
|
3084
|
+
fontFamily: lop.fontFamily,
|
|
3085
|
+
fontSize: target?.fontSize ?? lop.fontSize,
|
|
3086
|
+
fontWeight: lop.fontWeight,
|
|
3087
|
+
fontStyle: lop.fontStyle,
|
|
3088
|
+
fill: lop.fill,
|
|
3089
|
+
color: lop.fill,
|
|
3090
|
+
align:
|
|
3091
|
+
lop.align === "start"
|
|
3092
|
+
? "left"
|
|
3093
|
+
: lop.align === "end"
|
|
3094
|
+
? "right"
|
|
3095
|
+
: (lop.align ?? "center"),
|
|
3096
|
+
verticalAlign: lop.verticalAlign ?? lop.textAlignVertical,
|
|
3097
|
+
lineHeight: lop.lineHeight,
|
|
3098
|
+
letterSpacing: lop.letterSpacing,
|
|
3099
|
+
textBackgroundColor: lop.background ?? lop.textBackgroundColor,
|
|
3100
|
+
borderRadius: lop.borderRadius ?? lop.strokeRadius,
|
|
3101
|
+
strokeRadius: lop.strokeRadius ?? lop.borderRadius,
|
|
3102
|
+
stroke: lop.stroke,
|
|
3103
|
+
strokeWidth: lop.strokeWidth,
|
|
3104
|
+
shadows: lop.shadows,
|
|
3105
|
+
padding: lop.padding,
|
|
3106
|
+
maxWidth: lop.maxWidth ?? layerW,
|
|
3107
|
+
}),
|
|
3108
|
+
},
|
|
3109
|
+
localView,
|
|
3110
|
+
localTex,
|
|
3111
|
+
texW,
|
|
3112
|
+
texH,
|
|
3113
|
+
);
|
|
3114
|
+
} finally {
|
|
3115
|
+
if (captureGlyphs) {
|
|
3116
|
+
glyphBatches = ctx.renderer.slugPipeline.endCapture();
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
layerHasRaster = ctx.renderer.rasterDrawCount !== rasterDrawsBefore;
|
|
3120
|
+
} else if (kind === "shape") {
|
|
3121
|
+
const shapePass = ctx.renderer.beginFrame(
|
|
3122
|
+
encoder,
|
|
3123
|
+
localView,
|
|
3124
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3125
|
+
texW,
|
|
3126
|
+
texH,
|
|
3127
|
+
"load",
|
|
3128
|
+
);
|
|
3129
|
+
|
|
3130
|
+
const shapeType = lop.shapeType ?? lop.shape ?? "rect";
|
|
3131
|
+
const shapeConfig: ShapeGeometryConfig = {
|
|
3132
|
+
shapeType,
|
|
3133
|
+
width: layerW,
|
|
3134
|
+
height: layerH,
|
|
3135
|
+
path: lop.d ?? lop.path,
|
|
3136
|
+
cornerRadius:
|
|
3137
|
+
target?.cornerRadius ??
|
|
3138
|
+
lop.cornerRadius ??
|
|
3139
|
+
lop.borderRadius ??
|
|
3140
|
+
lop.radius ??
|
|
3141
|
+
0,
|
|
3142
|
+
starPoints: lop.starPoints ?? 5,
|
|
3143
|
+
starInnerRadiusRatio:
|
|
3144
|
+
target?.starInnerRadiusRatio ?? lop.starInnerRadiusRatio ?? 0.5,
|
|
3145
|
+
polygonSides: lop.polygonSides ?? 5,
|
|
3146
|
+
};
|
|
3147
|
+
|
|
3148
|
+
const fillType = lop.fillType ?? "solid";
|
|
3149
|
+
const fillColor =
|
|
3150
|
+
target?.fillColor ??
|
|
3151
|
+
target?.fill ??
|
|
3152
|
+
lop.fillColor ??
|
|
3153
|
+
lop.fill ??
|
|
3154
|
+
"#38bdf8";
|
|
3155
|
+
const color2 =
|
|
3156
|
+
target?.gradientEndColor ??
|
|
3157
|
+
lop.gradientEndColor ??
|
|
3158
|
+
lop.fillColor2 ??
|
|
3159
|
+
"#9333ea";
|
|
3160
|
+
|
|
3161
|
+
let startX = lop.fillStartX;
|
|
3162
|
+
let startY = lop.fillStartY;
|
|
3163
|
+
let endX = lop.fillEndX;
|
|
3164
|
+
let endY = lop.fillEndY;
|
|
3165
|
+
if (
|
|
3166
|
+
fillType === "linear" &&
|
|
3167
|
+
startX === undefined &&
|
|
3168
|
+
lop.gradientAngle !== undefined
|
|
3169
|
+
) {
|
|
3170
|
+
const rad = (lop.gradientAngle * Math.PI) / 180;
|
|
3171
|
+
const cx = layerW / 2;
|
|
3172
|
+
const cy = layerH / 2;
|
|
3173
|
+
// Edge to edge along the angle (0° left→right, 90° top→bottom),
|
|
3174
|
+
// matching the shape generator's SVG gradients.
|
|
3175
|
+
const half =
|
|
3176
|
+
(Math.abs(layerW * Math.cos(rad)) +
|
|
3177
|
+
Math.abs(layerH * Math.sin(rad))) /
|
|
3178
|
+
2;
|
|
3179
|
+
startX = cx - Math.cos(rad) * half;
|
|
3180
|
+
startY = cy - Math.sin(rad) * half;
|
|
3181
|
+
endX = cx + Math.cos(rad) * half;
|
|
3182
|
+
endY = cy + Math.sin(rad) * half;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
const fillConfig: ShapeFillConfig = {
|
|
3186
|
+
type: fillType,
|
|
3187
|
+
color: fillColor,
|
|
3188
|
+
color2,
|
|
3189
|
+
startX,
|
|
3190
|
+
startY,
|
|
3191
|
+
endX,
|
|
3192
|
+
endY,
|
|
3193
|
+
cx: lop.fillCx,
|
|
3194
|
+
cy: lop.fillCy,
|
|
3195
|
+
radius: lop.fillRadius,
|
|
3196
|
+
};
|
|
3197
|
+
|
|
3198
|
+
const strokeWidth = target?.strokeWidth ?? lop.strokeWidth ?? 0;
|
|
3199
|
+
const strokeColor =
|
|
3200
|
+
target?.strokeColor ?? lop.strokeColor ?? lop.stroke ?? "#ffffff";
|
|
3201
|
+
const strokeLineCap = lop.strokeLineCap ?? "round";
|
|
3202
|
+
const strokeDashArray = lop.strokeDashArray;
|
|
3203
|
+
const strokeDashOffset =
|
|
3204
|
+
target?.strokeDashOffset ?? lop.strokeDashOffset ?? 0;
|
|
3205
|
+
const trimStart = target?.trimStart ?? lop.trimStart ?? 0;
|
|
3206
|
+
const trimEnd = target?.trimEnd ?? lop.trimEnd ?? 1;
|
|
3207
|
+
const trimOffset = target?.trimOffset ?? lop.trimOffset ?? 0;
|
|
3208
|
+
|
|
3209
|
+
ctx.renderer.drawShape(
|
|
3210
|
+
shapePass,
|
|
3211
|
+
shapeConfig,
|
|
3212
|
+
fillConfig,
|
|
3213
|
+
strokeWidth > 0
|
|
3214
|
+
? {
|
|
3215
|
+
color: strokeColor,
|
|
3216
|
+
width: strokeWidth,
|
|
3217
|
+
cap: strokeLineCap,
|
|
3218
|
+
trimStart,
|
|
3219
|
+
trimEnd,
|
|
3220
|
+
trimOffset,
|
|
3221
|
+
dashArray: strokeDashArray,
|
|
3222
|
+
dashOffset: strokeDashOffset,
|
|
3223
|
+
}
|
|
3224
|
+
: undefined,
|
|
3225
|
+
);
|
|
3226
|
+
|
|
3227
|
+
shapePass.end();
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// Temporal De-flickering & Optical Flow Warping
|
|
3231
|
+
const leafDeflickerEffect = Array.isArray(lop.effects)
|
|
3232
|
+
? lop.effects.find(
|
|
3233
|
+
(e: unknown) =>
|
|
3234
|
+
(e as { op?: string })?.op === "TemporalDeflicker" ||
|
|
3235
|
+
(e as { constructor?: { name?: string } })?.constructor?.name ===
|
|
3236
|
+
"TemporalDeflicker",
|
|
3237
|
+
)
|
|
3238
|
+
: undefined;
|
|
3239
|
+
const deflickerOpts =
|
|
3240
|
+
target?.deflicker ?? lop.deflicker ?? leafDeflickerEffect;
|
|
3241
|
+
|
|
3242
|
+
// Screen-Space 3D Relighting
|
|
3243
|
+
const leafRelightEffect = Array.isArray(lop.effects)
|
|
3244
|
+
? lop.effects.find(
|
|
3245
|
+
(e: unknown) =>
|
|
3246
|
+
(e as { op?: string })?.op === "Relight3D" ||
|
|
3247
|
+
(e as { constructor?: { name?: string } })?.constructor?.name ===
|
|
3248
|
+
"Relight3D",
|
|
3249
|
+
)
|
|
3250
|
+
: undefined;
|
|
3251
|
+
const rawLeafRelight =
|
|
3252
|
+
target?.relighting ??
|
|
3253
|
+
lop.relighting ??
|
|
3254
|
+
(typeof lop.relight === "object" ? lop.relight : undefined) ??
|
|
3255
|
+
leafRelightEffect;
|
|
3256
|
+
const relightOpts =
|
|
3257
|
+
typeof rawLeafRelight === "object" && rawLeafRelight !== null
|
|
3258
|
+
? rawLeafRelight
|
|
3259
|
+
: undefined;
|
|
3260
|
+
|
|
3261
|
+
// Directional velocity motion blur
|
|
3262
|
+
const shutterAngle =
|
|
3263
|
+
target?.motionBlurShutter ?? lop.motionBlurShutter ?? 0;
|
|
3264
|
+
|
|
3265
|
+
// Effects work on the layer's pixels: glyphs held back for the 3D
|
|
3266
|
+
// pass are drawn into its texture after all.
|
|
3267
|
+
if (
|
|
3268
|
+
glyphBatches &&
|
|
3269
|
+
glyphBatches.length > 0 &&
|
|
3270
|
+
(deflickerOpts || relightOpts || shutterAngle > 0)
|
|
3271
|
+
) {
|
|
3272
|
+
const glyphPass = ctx.renderer.beginFrame(
|
|
3273
|
+
encoder,
|
|
3274
|
+
localView,
|
|
3275
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3276
|
+
texW,
|
|
3277
|
+
texH,
|
|
3278
|
+
"load",
|
|
3279
|
+
);
|
|
3280
|
+
ctx.renderer.slugPipeline.drawBatches(
|
|
3281
|
+
glyphPass,
|
|
3282
|
+
glyphBatches,
|
|
3283
|
+
texW,
|
|
3284
|
+
texH,
|
|
3285
|
+
);
|
|
3286
|
+
glyphPass.end();
|
|
3287
|
+
glyphBatches = null;
|
|
3288
|
+
layerHasRaster = true;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
let finalTex = localTex;
|
|
3292
|
+
if (!isTrue3D && br > 0 && !isCaption && kind !== "shape") {
|
|
3293
|
+
const maskTex = ctx.renderer.getTemporaryTexture(texW, texH, [
|
|
3294
|
+
localTex,
|
|
3295
|
+
activeTex,
|
|
3296
|
+
targetTexture,
|
|
3297
|
+
...excludes,
|
|
3298
|
+
]);
|
|
3299
|
+
const maskView = maskTex.createView();
|
|
3300
|
+
const maskClearPass = ctx.renderer.beginFrame(
|
|
3301
|
+
encoder,
|
|
3302
|
+
maskView,
|
|
3303
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3304
|
+
texW,
|
|
3305
|
+
texH,
|
|
3306
|
+
"clear",
|
|
3307
|
+
);
|
|
3308
|
+
maskClearPass.end();
|
|
3309
|
+
|
|
3310
|
+
const maskPass = ctx.renderer.beginFrame(
|
|
3311
|
+
encoder,
|
|
3312
|
+
maskView,
|
|
3313
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3314
|
+
texW,
|
|
3315
|
+
texH,
|
|
3316
|
+
"load",
|
|
3317
|
+
);
|
|
3318
|
+
ctx.renderer.drawRect(
|
|
3319
|
+
maskPass,
|
|
3320
|
+
{ x: 0, y: 0, width: layerW, height: layerH },
|
|
3321
|
+
{ r: 1, g: 1, b: 1, a: 1 },
|
|
3322
|
+
br,
|
|
3323
|
+
);
|
|
3324
|
+
maskPass.end();
|
|
3325
|
+
|
|
3326
|
+
finalTex = ctx.renderer.composite(
|
|
3327
|
+
encoder,
|
|
3328
|
+
localTex,
|
|
3329
|
+
maskTex,
|
|
3330
|
+
"mask-in",
|
|
3331
|
+
[activeTex, targetTexture, ...excludes],
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
// Temporal De-flickering & Optical Flow Warping
|
|
3336
|
+
if (deflickerOpts) {
|
|
3337
|
+
const deflickerPipe = getTemporalDeflickerPipeline(
|
|
3338
|
+
ctx.device,
|
|
3339
|
+
lop.id || `layer_${i}`,
|
|
3340
|
+
);
|
|
3341
|
+
if (shiftedFrame === 0) {
|
|
3342
|
+
deflickerPipe.reset();
|
|
3343
|
+
}
|
|
3344
|
+
finalTex = deflickerPipe.execute(finalTex, deflickerOpts, encoder);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// Screen-Space 3D Relighting
|
|
3348
|
+
if (relightOpts) {
|
|
3349
|
+
const relightPipeline = getScreenSpaceRelightPipeline(
|
|
3350
|
+
ctx.device,
|
|
3351
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
3352
|
+
);
|
|
3353
|
+
const r3dInstance = getRenderer3D(
|
|
3354
|
+
ctx.device,
|
|
3355
|
+
ctx.renderer.format ?? "rgba8unorm",
|
|
3356
|
+
sampleCount3d,
|
|
3357
|
+
);
|
|
3358
|
+
const activeLights = synthesizeRelightLights(
|
|
3359
|
+
relightOpts as Record<string, unknown>,
|
|
3360
|
+
sceneLights,
|
|
3361
|
+
{ x: sampledX, y: sampledY, width: texW, height: texH },
|
|
3362
|
+
shiftedFrame,
|
|
3363
|
+
fps ?? 24,
|
|
3364
|
+
);
|
|
3365
|
+
const lightsBuffer = r3dInstance.getOrCreateLightsBuffer(activeLights);
|
|
3366
|
+
|
|
3367
|
+
const relitTex = ctx.renderer.getTemporaryTexture(texW, texH, [
|
|
3368
|
+
finalTex,
|
|
3369
|
+
activeTex,
|
|
3370
|
+
targetTexture,
|
|
3371
|
+
...excludes,
|
|
3372
|
+
]);
|
|
3373
|
+
const relitView = relitTex.createView();
|
|
3374
|
+
|
|
3375
|
+
const normalTex = relightOpts.normalTexture ?? relightOpts.normalMap;
|
|
3376
|
+
|
|
3377
|
+
relightPipeline.execute(
|
|
3378
|
+
encoder,
|
|
3379
|
+
relitView,
|
|
3380
|
+
finalTex,
|
|
3381
|
+
normalTex,
|
|
3382
|
+
lightsBuffer,
|
|
3383
|
+
{
|
|
3384
|
+
viewportWidth: texW,
|
|
3385
|
+
viewportHeight: texH,
|
|
3386
|
+
layerX: sampledX,
|
|
3387
|
+
layerY: sampledY,
|
|
3388
|
+
roughness: resolveVal(
|
|
3389
|
+
target?.roughness ??
|
|
3390
|
+
target?.relightRoughness ??
|
|
3391
|
+
relightOpts.specularRoughness ??
|
|
3392
|
+
relightOpts.roughness,
|
|
3393
|
+
0.35,
|
|
3394
|
+
shiftedFrame,
|
|
3395
|
+
fps ?? 24,
|
|
3396
|
+
),
|
|
3397
|
+
specularStrength: resolveVal(
|
|
3398
|
+
target?.specularStrength ??
|
|
3399
|
+
target?.relightSpecularStrength ??
|
|
3400
|
+
relightOpts.specularStrength,
|
|
3401
|
+
0.7,
|
|
3402
|
+
shiftedFrame,
|
|
3403
|
+
fps ?? 24,
|
|
3404
|
+
),
|
|
3405
|
+
metallic: resolveVal(
|
|
3406
|
+
target?.metallic ??
|
|
3407
|
+
target?.relightMetallic ??
|
|
3408
|
+
relightOpts.metallic,
|
|
3409
|
+
0.0,
|
|
3410
|
+
shiftedFrame,
|
|
3411
|
+
fps ?? 24,
|
|
3412
|
+
),
|
|
3413
|
+
ambientIntensity: resolveVal(
|
|
3414
|
+
target?.ambientIntensity ??
|
|
3415
|
+
target?.relightAmbientIntensity ??
|
|
3416
|
+
relightOpts.ambientIntensity,
|
|
3417
|
+
0.15,
|
|
3418
|
+
shiftedFrame,
|
|
3419
|
+
fps ?? 24,
|
|
3420
|
+
),
|
|
3421
|
+
depthScale: resolveVal(
|
|
3422
|
+
target?.depthScale ??
|
|
3423
|
+
target?.relightDepthScale ??
|
|
3424
|
+
relightOpts.depthScale,
|
|
3425
|
+
1.0,
|
|
3426
|
+
shiftedFrame,
|
|
3427
|
+
fps ?? 24,
|
|
3428
|
+
),
|
|
3429
|
+
opacity: 1.0,
|
|
3430
|
+
},
|
|
3431
|
+
"clear",
|
|
3432
|
+
);
|
|
3433
|
+
|
|
3434
|
+
finalTex = relitTex;
|
|
3435
|
+
}
|
|
3436
|
+
|
|
3437
|
+
// Directional velocity motion blur (last: the shutter integrates the final look)
|
|
3438
|
+
let blurPadX = 0;
|
|
3439
|
+
let blurPadY = 0;
|
|
3440
|
+
if (shutterAngle > 0 && fps && shiftedFrame !== undefined) {
|
|
3441
|
+
const currentT = shiftedFrame / fps;
|
|
3442
|
+
const prevT = (shiftedFrame - 1) / fps;
|
|
3443
|
+
tl.seek(prevT);
|
|
3444
|
+
const prevTarget = targetId ? targetsById[targetId] : undefined;
|
|
3445
|
+
const prevX = prevTarget?.x ?? baseX;
|
|
3446
|
+
const prevY = prevTarget?.y ?? baseY;
|
|
3447
|
+
tl.seek(currentT);
|
|
3448
|
+
|
|
3449
|
+
const vx = sampledX - prevX;
|
|
3450
|
+
const vy = sampledY - prevY;
|
|
3451
|
+
|
|
3452
|
+
if (Math.hypot(vx, vy) > 0.5) {
|
|
3453
|
+
// The blur only samples inside its texture: give flat layers room
|
|
3454
|
+
// to streak past their own bounds instead of smearing edge pixels.
|
|
3455
|
+
if (!isTrue3D && !(is3D && quad3D)) {
|
|
3456
|
+
const reach = shutterAngle / 360;
|
|
3457
|
+
blurPadX = Math.ceil(Math.abs(vx) * reach);
|
|
3458
|
+
blurPadY = Math.ceil(Math.abs(vy) * reach);
|
|
3459
|
+
const paddedTex = ctx.renderer.getTemporaryTexture(
|
|
3460
|
+
texW + blurPadX * 2,
|
|
3461
|
+
texH + blurPadY * 2,
|
|
3462
|
+
[finalTex, activeTex, targetTexture, ...excludes],
|
|
3463
|
+
);
|
|
3464
|
+
const padPass = ctx.renderer.beginFrame(
|
|
3465
|
+
encoder,
|
|
3466
|
+
paddedTex.createView(),
|
|
3467
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3468
|
+
paddedTex.width,
|
|
3469
|
+
paddedTex.height,
|
|
3470
|
+
"clear",
|
|
3471
|
+
);
|
|
3472
|
+
ctx.renderer.drawTexture(
|
|
3473
|
+
padPass,
|
|
3474
|
+
finalTex,
|
|
3475
|
+
{ x: blurPadX, y: blurPadY, width: texW, height: texH },
|
|
3476
|
+
{ opacity: 1.0 },
|
|
3477
|
+
);
|
|
3478
|
+
padPass.end();
|
|
3479
|
+
finalTex = paddedTex;
|
|
3480
|
+
}
|
|
3481
|
+
// ~1 tap per 2 px of streak so fast moves smear instead of ghosting.
|
|
3482
|
+
const streakPx = Math.hypot(vx, vy) * (shutterAngle / 360) * 2;
|
|
3483
|
+
const samples = Math.min(128, Math.max(16, Math.ceil(streakPx / 2)));
|
|
3484
|
+
finalTex = ctx.renderer.applyMotionBlur(
|
|
3485
|
+
encoder,
|
|
3486
|
+
finalTex,
|
|
3487
|
+
vx,
|
|
3488
|
+
vy,
|
|
3489
|
+
shutterAngle,
|
|
3490
|
+
samples,
|
|
3491
|
+
[activeTex, targetTexture, ...excludes],
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
const borderExcludes = [
|
|
3497
|
+
localTex,
|
|
3498
|
+
finalTex,
|
|
3499
|
+
activeTex,
|
|
3500
|
+
targetTexture,
|
|
3501
|
+
...excludes,
|
|
3502
|
+
];
|
|
3503
|
+
const borderResult =
|
|
3504
|
+
!isTrue3D && lop.borderWidth !== undefined && lop.borderColor
|
|
3505
|
+
? createBorderTexture(
|
|
3506
|
+
ctx,
|
|
3507
|
+
encoder,
|
|
3508
|
+
layerW,
|
|
3509
|
+
layerH,
|
|
3510
|
+
br,
|
|
3511
|
+
lop.borderWidth,
|
|
3512
|
+
lop.borderColor,
|
|
3513
|
+
lop.strokeAlign ?? "inside",
|
|
3514
|
+
borderExcludes,
|
|
3515
|
+
)
|
|
3516
|
+
: null;
|
|
3517
|
+
|
|
3518
|
+
// Restore parent container scissors before compositing onto container/screen activeView
|
|
3519
|
+
if (savedScissors.length > 0) {
|
|
3520
|
+
ctx.renderer.resumeScissors?.(savedScissors);
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
if (isTrue3D) {
|
|
3524
|
+
const renderedTex = finalTex;
|
|
3525
|
+
if (borderResult) {
|
|
3526
|
+
const borderPass = ctx.renderer.beginFrame(
|
|
3527
|
+
encoder,
|
|
3528
|
+
renderedTex.createView(),
|
|
3529
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3530
|
+
texW,
|
|
3531
|
+
texH,
|
|
3532
|
+
"load",
|
|
3533
|
+
);
|
|
3534
|
+
ctx.renderer.drawTexture(
|
|
3535
|
+
borderPass,
|
|
3536
|
+
borderResult.borderTex,
|
|
3537
|
+
{
|
|
3538
|
+
x: borderResult.ox,
|
|
3539
|
+
y: borderResult.oy,
|
|
3540
|
+
width: borderResult.ow,
|
|
3541
|
+
height: borderResult.oh,
|
|
3542
|
+
},
|
|
3543
|
+
{ opacity: 1.0 },
|
|
3544
|
+
);
|
|
3545
|
+
borderPass.end();
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
let modelMatrix: Mat4 | undefined;
|
|
3549
|
+
if (parent3DMatrix) {
|
|
3550
|
+
let localM = Matrix4Math.identity();
|
|
3551
|
+
const childX = target?.x ?? lop.x ?? 0;
|
|
3552
|
+
const childY = target?.y ?? lop.y ?? 0;
|
|
3553
|
+
const childZ = target?.z ?? lop.z ?? 0;
|
|
3554
|
+
localM = Matrix4Math.multiply(
|
|
3555
|
+
localM,
|
|
3556
|
+
Matrix4Math.translate(childX, childY, childZ),
|
|
3557
|
+
);
|
|
3558
|
+
const rxRad = ((target?.rotateX ?? lop.rotateX ?? 0) * Math.PI) / 180;
|
|
3559
|
+
const ryRad = ((target?.rotateY ?? lop.rotateY ?? 0) * Math.PI) / 180;
|
|
3560
|
+
const rzRad =
|
|
3561
|
+
((target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
3562
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
3563
|
+
: rotation) *
|
|
3564
|
+
(Math.PI / 180);
|
|
3565
|
+
if (rzRad !== 0)
|
|
3566
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateZ(rzRad));
|
|
3567
|
+
if (ryRad !== 0)
|
|
3568
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateY(ryRad));
|
|
3569
|
+
if (rxRad !== 0)
|
|
3570
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.rotateX(rxRad));
|
|
3571
|
+
|
|
3572
|
+
const sx = layerW * scale;
|
|
3573
|
+
const sy = layerH * scale;
|
|
3574
|
+
const sz = target?.scaleZ ?? lop.scaleZ ?? 1;
|
|
3575
|
+
localM = Matrix4Math.multiply(localM, Matrix4Math.scale(sx, sy, sz));
|
|
3576
|
+
|
|
3577
|
+
const ax = lop.anchorX ?? 0.5;
|
|
3578
|
+
const ay = lop.anchorY ?? 0.5;
|
|
3579
|
+
const pivotX = 0.5 - ax;
|
|
3580
|
+
const pivotY = 0.5 - ay;
|
|
3581
|
+
if (pivotX !== 0 || pivotY !== 0) {
|
|
3582
|
+
localM = Matrix4Math.multiply(
|
|
3583
|
+
localM,
|
|
3584
|
+
Matrix4Math.translate(pivotX, pivotY, 0),
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
modelMatrix = Matrix4Math.multiply(parent3DMatrix, localM);
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
const quadOpts: DrawQuad3DOpts = {
|
|
3592
|
+
position: [
|
|
3593
|
+
localX + layerW * 0.5,
|
|
3594
|
+
localY + layerH * 0.5,
|
|
3595
|
+
target?.z ?? lop.z ?? 0,
|
|
3596
|
+
],
|
|
3597
|
+
width: layerW,
|
|
3598
|
+
height: layerH,
|
|
3599
|
+
textureExtent: [layerW / texW, layerH / texH],
|
|
3600
|
+
rotation: [
|
|
3601
|
+
target?.rotateX ?? lop.rotateX ?? 0,
|
|
3602
|
+
target?.rotateY ?? lop.rotateY ?? 0,
|
|
3603
|
+
(target?.rotateZ ?? lop.rotateZ ?? 0) !== 0
|
|
3604
|
+
? (target?.rotateZ ?? lop.rotateZ ?? 0)
|
|
3605
|
+
: rotation,
|
|
3606
|
+
],
|
|
3607
|
+
scale: [scale, scale, target?.scaleZ ?? lop.scaleZ ?? 1],
|
|
3608
|
+
anchor: [lop.anchorX ?? 0.5, lop.anchorY ?? 0.5],
|
|
3609
|
+
borderRadius: br,
|
|
3610
|
+
borderWidth: lop.borderWidth,
|
|
3611
|
+
borderColor: lop.borderColor,
|
|
3612
|
+
opacity: layerOpacity,
|
|
3613
|
+
twoSided: target?.twoSided ?? lop.twoSided ?? true,
|
|
3614
|
+
material: target?.material ?? lop.material,
|
|
3615
|
+
shininess: target?.shininess ?? lop.shininess,
|
|
3616
|
+
roughness: target?.roughness ?? lop.roughness,
|
|
3617
|
+
specularIntensity: target?.specularIntensity ?? lop.specularIntensity,
|
|
3618
|
+
ambientIntensity: target?.ambientIntensity ?? lop.ambientIntensity,
|
|
3619
|
+
metallic: target?.metallic ?? lop.metallic,
|
|
3620
|
+
...(modelMatrix && { modelMatrix }),
|
|
3621
|
+
};
|
|
3622
|
+
|
|
3623
|
+
// The texture quad carries the layer's backdrop and border; a
|
|
3624
|
+
// text layer that drew neither needs only its glyphs.
|
|
3625
|
+
const quadHasContent =
|
|
3626
|
+
layerHasRaster ||
|
|
3627
|
+
Boolean(
|
|
3628
|
+
quadOpts.borderWidth &&
|
|
3629
|
+
quadOpts.borderWidth > 0 &&
|
|
3630
|
+
quadOpts.borderColor,
|
|
3631
|
+
);
|
|
3632
|
+
if (!glyphBatches || quadHasContent) {
|
|
3633
|
+
pending3DItems.push({
|
|
3634
|
+
kind: "quad",
|
|
3635
|
+
texture: renderedTex,
|
|
3636
|
+
opts: quadOpts,
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
if (glyphBatches && glyphBatches.length > 0) {
|
|
3640
|
+
pending3DItems.push({
|
|
3641
|
+
kind: "slug",
|
|
3642
|
+
batches: glyphBatches,
|
|
3643
|
+
opts: quadOpts,
|
|
3644
|
+
stackBase: quadHasContent ? 1 : 0,
|
|
3645
|
+
});
|
|
3646
|
+
}
|
|
3647
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
if (is3D && quad3D) {
|
|
3651
|
+
const renderedTex = finalTex;
|
|
3652
|
+
if (borderResult) {
|
|
3653
|
+
const borderPass = ctx.renderer.beginFrame(
|
|
3654
|
+
encoder,
|
|
3655
|
+
renderedTex.createView(),
|
|
3656
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3657
|
+
texW,
|
|
3658
|
+
texH,
|
|
3659
|
+
"load",
|
|
3660
|
+
);
|
|
3661
|
+
ctx.renderer.drawTexture(
|
|
3662
|
+
borderPass,
|
|
3663
|
+
borderResult.borderTex,
|
|
3664
|
+
{
|
|
3665
|
+
x: borderResult.ox,
|
|
3666
|
+
y: borderResult.oy,
|
|
3667
|
+
width: borderResult.ow,
|
|
3668
|
+
height: borderResult.oh,
|
|
3669
|
+
},
|
|
3670
|
+
{ opacity: 1.0 },
|
|
3671
|
+
);
|
|
3672
|
+
borderPass.end();
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
if (blendMode === "normal" || blendMode === "source-over") {
|
|
3676
|
+
drawProjectedQuad(
|
|
3677
|
+
ctx,
|
|
3678
|
+
encoder,
|
|
3679
|
+
activeView,
|
|
3680
|
+
renderedTex,
|
|
3681
|
+
quad3D,
|
|
3682
|
+
targetW,
|
|
3683
|
+
targetH,
|
|
3684
|
+
layerOpacity,
|
|
3685
|
+
);
|
|
3686
|
+
} else {
|
|
3687
|
+
const layerExcludes = [
|
|
3688
|
+
activeTex,
|
|
3689
|
+
targetTexture,
|
|
3690
|
+
renderedTex,
|
|
3691
|
+
...excludes,
|
|
3692
|
+
];
|
|
3693
|
+
if (borderResult) layerExcludes.push(borderResult.borderTex);
|
|
3694
|
+
const layerTex = ctx.renderer.getTemporaryTexture(
|
|
3695
|
+
targetW,
|
|
3696
|
+
targetH,
|
|
3697
|
+
layerExcludes,
|
|
3698
|
+
);
|
|
3699
|
+
const layerView = layerTex.createView();
|
|
3700
|
+
const layerClearPass = ctx.renderer.beginFrame(
|
|
3701
|
+
encoder,
|
|
3702
|
+
layerView,
|
|
3703
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3704
|
+
targetW,
|
|
3705
|
+
targetH,
|
|
3706
|
+
"clear",
|
|
3707
|
+
);
|
|
3708
|
+
layerClearPass.end();
|
|
3709
|
+
|
|
3710
|
+
drawProjectedQuad(
|
|
3711
|
+
ctx,
|
|
3712
|
+
encoder,
|
|
3713
|
+
layerView,
|
|
3714
|
+
renderedTex,
|
|
3715
|
+
quad3D,
|
|
3716
|
+
targetW,
|
|
3717
|
+
targetH,
|
|
3718
|
+
layerOpacity,
|
|
3719
|
+
);
|
|
3720
|
+
|
|
3721
|
+
const nextCompTex = ctx.renderer.composite(
|
|
3722
|
+
encoder,
|
|
3723
|
+
activeTex,
|
|
3724
|
+
layerTex,
|
|
3725
|
+
blendMode,
|
|
3726
|
+
[targetTexture, ...excludes],
|
|
3727
|
+
);
|
|
3728
|
+
activeTex = nextCompTex;
|
|
3729
|
+
activeView = activeTex.createView();
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
if (blendMode === "normal" || blendMode === "source-over") {
|
|
3736
|
+
const drawPass = ctx.renderer.beginFrame(
|
|
3737
|
+
encoder,
|
|
3738
|
+
activeView,
|
|
3739
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3740
|
+
targetW,
|
|
3741
|
+
targetH,
|
|
3742
|
+
"load",
|
|
3743
|
+
);
|
|
3744
|
+
ctx.renderer.pushTransform(layerMatrix);
|
|
3745
|
+
ctx.renderer.drawTexture(
|
|
3746
|
+
drawPass,
|
|
3747
|
+
finalTex,
|
|
3748
|
+
{
|
|
3749
|
+
x: -blurPadX,
|
|
3750
|
+
y: -blurPadY,
|
|
3751
|
+
width: layerW + blurPadX * 2,
|
|
3752
|
+
height: layerH + blurPadY * 2,
|
|
3753
|
+
},
|
|
3754
|
+
{ opacity: layerOpacity },
|
|
3755
|
+
);
|
|
3756
|
+
|
|
3757
|
+
if (borderResult) {
|
|
3758
|
+
ctx.renderer.drawTexture(
|
|
3759
|
+
drawPass,
|
|
3760
|
+
borderResult.borderTex,
|
|
3761
|
+
{
|
|
3762
|
+
x: borderResult.ox,
|
|
3763
|
+
y: borderResult.oy,
|
|
3764
|
+
width: borderResult.ow,
|
|
3765
|
+
height: borderResult.oh,
|
|
3766
|
+
},
|
|
3767
|
+
{ opacity: layerOpacity },
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
ctx.renderer.popTransform();
|
|
3772
|
+
drawPass.end();
|
|
3773
|
+
} else {
|
|
3774
|
+
const layerExcludes = [finalTex, activeTex, targetTexture, ...excludes];
|
|
3775
|
+
if (borderResult) layerExcludes.push(borderResult.borderTex);
|
|
3776
|
+
const layerTex = ctx.renderer.getTemporaryTexture(
|
|
3777
|
+
targetW,
|
|
3778
|
+
targetH,
|
|
3779
|
+
layerExcludes,
|
|
3780
|
+
);
|
|
3781
|
+
const layerView = layerTex.createView();
|
|
3782
|
+
|
|
3783
|
+
const layerClearPass = ctx.renderer.beginFrame(
|
|
3784
|
+
encoder,
|
|
3785
|
+
layerView,
|
|
3786
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3787
|
+
targetW,
|
|
3788
|
+
targetH,
|
|
3789
|
+
"clear",
|
|
3790
|
+
);
|
|
3791
|
+
layerClearPass.end();
|
|
3792
|
+
|
|
3793
|
+
const drawPass = ctx.renderer.beginFrame(
|
|
3794
|
+
encoder,
|
|
3795
|
+
layerView,
|
|
3796
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3797
|
+
targetW,
|
|
3798
|
+
targetH,
|
|
3799
|
+
"load",
|
|
3800
|
+
);
|
|
3801
|
+
ctx.renderer.pushTransform(layerMatrix);
|
|
3802
|
+
ctx.renderer.drawTexture(
|
|
3803
|
+
drawPass,
|
|
3804
|
+
finalTex,
|
|
3805
|
+
{
|
|
3806
|
+
x: -blurPadX,
|
|
3807
|
+
y: -blurPadY,
|
|
3808
|
+
width: layerW + blurPadX * 2,
|
|
3809
|
+
height: layerH + blurPadY * 2,
|
|
3810
|
+
},
|
|
3811
|
+
{ opacity: layerOpacity },
|
|
3812
|
+
);
|
|
3813
|
+
|
|
3814
|
+
if (borderResult) {
|
|
3815
|
+
ctx.renderer.drawTexture(
|
|
3816
|
+
drawPass,
|
|
3817
|
+
borderResult.borderTex,
|
|
3818
|
+
{
|
|
3819
|
+
x: borderResult.ox,
|
|
3820
|
+
y: borderResult.oy,
|
|
3821
|
+
width: borderResult.ow,
|
|
3822
|
+
height: borderResult.oh,
|
|
3823
|
+
},
|
|
3824
|
+
{ opacity: layerOpacity },
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
ctx.renderer.popTransform();
|
|
3829
|
+
drawPass.end();
|
|
3830
|
+
|
|
3831
|
+
const nextCompTex = ctx.renderer.composite(
|
|
3832
|
+
encoder,
|
|
3833
|
+
activeTex,
|
|
3834
|
+
layerTex,
|
|
3835
|
+
blendMode,
|
|
3836
|
+
[targetTexture, ...excludes],
|
|
3837
|
+
);
|
|
3838
|
+
activeTex = nextCompTex;
|
|
3839
|
+
activeView = activeTex.createView();
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
return { finalTex: activeTex, finalView: activeView };
|
|
3843
|
+
};
|
|
3844
|
+
|
|
3845
|
+
const initialExcludes = [
|
|
3846
|
+
compTex,
|
|
3847
|
+
targetTexture,
|
|
3848
|
+
...(props.excludeTextures || []),
|
|
3849
|
+
];
|
|
3850
|
+
const renderResult = await renderNodeTree(
|
|
3851
|
+
virtualMedia.children ?? [],
|
|
3852
|
+
compTex,
|
|
3853
|
+
compView,
|
|
3854
|
+
initialExcludes,
|
|
3855
|
+
1.0,
|
|
3856
|
+
new DOMMatrix(),
|
|
3857
|
+
null,
|
|
3858
|
+
nativeWidth,
|
|
3859
|
+
nativeHeight,
|
|
3860
|
+
null,
|
|
3861
|
+
true,
|
|
3862
|
+
);
|
|
3863
|
+
currentCompTex = renderResult.finalTex;
|
|
3864
|
+
|
|
3865
|
+
ctx.renderer.popTransform();
|
|
3866
|
+
ctx.renderer.popScissor();
|
|
3867
|
+
|
|
3868
|
+
// 4. Composite the native texture back to targetView
|
|
3869
|
+
const finalPass = ctx.renderer.beginFrame(
|
|
3870
|
+
encoder,
|
|
3871
|
+
targetView,
|
|
3872
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3873
|
+
targetWidth,
|
|
3874
|
+
targetHeight,
|
|
3875
|
+
"load",
|
|
3876
|
+
);
|
|
3877
|
+
|
|
3878
|
+
ctx.renderer.drawTexture(
|
|
3879
|
+
finalPass,
|
|
3880
|
+
currentCompTex,
|
|
3881
|
+
{
|
|
3882
|
+
x: 0,
|
|
3883
|
+
y: 0,
|
|
3884
|
+
width: props.containerWidth ?? targetWidth,
|
|
3885
|
+
height: props.containerHeight ?? targetHeight,
|
|
3886
|
+
},
|
|
3887
|
+
{ opacity },
|
|
3888
|
+
);
|
|
3889
|
+
|
|
3890
|
+
args.pass = finalPass;
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
|
|
3894
|
+
function createBorderTexture(
|
|
3895
|
+
ctx: RenderContextValue,
|
|
3896
|
+
encoder: GPUCommandEncoder,
|
|
3897
|
+
w: number,
|
|
3898
|
+
h: number,
|
|
3899
|
+
br: number,
|
|
3900
|
+
borderWidth: number,
|
|
3901
|
+
borderColor: string | Color,
|
|
3902
|
+
strokeAlign: string,
|
|
3903
|
+
excludeTextures: GPUTexture[],
|
|
3904
|
+
): {
|
|
3905
|
+
borderTex: GPUTexture;
|
|
3906
|
+
ox: number;
|
|
3907
|
+
oy: number;
|
|
3908
|
+
ow: number;
|
|
3909
|
+
oh: number;
|
|
3910
|
+
} | null {
|
|
3911
|
+
if (w <= 0 || h <= 0 || !borderColor || !borderWidth || borderWidth <= 0) {
|
|
3912
|
+
return null;
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
let ox = 0;
|
|
3916
|
+
let oy = 0;
|
|
3917
|
+
let ow = w;
|
|
3918
|
+
let oh = h;
|
|
3919
|
+
let outerRadius = br;
|
|
3920
|
+
let innerRadius = br;
|
|
3921
|
+
|
|
3922
|
+
if (strokeAlign === "inside") {
|
|
3923
|
+
ox = 0;
|
|
3924
|
+
oy = 0;
|
|
3925
|
+
ow = w;
|
|
3926
|
+
oh = h;
|
|
3927
|
+
outerRadius = br;
|
|
3928
|
+
innerRadius = Math.max(0, br - borderWidth);
|
|
3929
|
+
} else if (strokeAlign === "outside") {
|
|
3930
|
+
ox = -borderWidth;
|
|
3931
|
+
oy = -borderWidth;
|
|
3932
|
+
ow = w + 2 * borderWidth;
|
|
3933
|
+
oh = h + 2 * borderWidth;
|
|
3934
|
+
outerRadius = br + borderWidth;
|
|
3935
|
+
innerRadius = br;
|
|
3936
|
+
} else {
|
|
3937
|
+
// center
|
|
3938
|
+
ox = -borderWidth / 2;
|
|
3939
|
+
oy = -borderWidth / 2;
|
|
3940
|
+
ow = w + borderWidth;
|
|
3941
|
+
oh = h + borderWidth;
|
|
3942
|
+
outerRadius = br + borderWidth / 2;
|
|
3943
|
+
innerRadius = Math.max(0, br - borderWidth / 2);
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
if (br <= 0) {
|
|
3947
|
+
outerRadius = 0;
|
|
3948
|
+
innerRadius = 0;
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
if (ow <= 0 || oh <= 0) {
|
|
3952
|
+
return null;
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
const borderTexW = Math.ceil(ow);
|
|
3956
|
+
const borderTexH = Math.ceil(oh);
|
|
3957
|
+
|
|
3958
|
+
const outerTex = ctx.renderer.getTemporaryTexture(
|
|
3959
|
+
borderTexW,
|
|
3960
|
+
borderTexH,
|
|
3961
|
+
excludeTextures,
|
|
3962
|
+
);
|
|
3963
|
+
const outerView = outerTex.createView();
|
|
3964
|
+
|
|
3965
|
+
const outerClearPass = ctx.renderer.beginFrame(
|
|
3966
|
+
encoder,
|
|
3967
|
+
outerView,
|
|
3968
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3969
|
+
borderTexW,
|
|
3970
|
+
borderTexH,
|
|
3971
|
+
"clear",
|
|
3972
|
+
);
|
|
3973
|
+
outerClearPass.end();
|
|
3974
|
+
|
|
3975
|
+
const outerDrawPass = ctx.renderer.beginFrame(
|
|
3976
|
+
encoder,
|
|
3977
|
+
outerView,
|
|
3978
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
3979
|
+
borderTexW,
|
|
3980
|
+
borderTexH,
|
|
3981
|
+
"load",
|
|
3982
|
+
);
|
|
3983
|
+
ctx.renderer.drawRect(
|
|
3984
|
+
outerDrawPass,
|
|
3985
|
+
{ x: 0, y: 0, width: ow, height: oh },
|
|
3986
|
+
borderColor,
|
|
3987
|
+
outerRadius,
|
|
3988
|
+
);
|
|
3989
|
+
outerDrawPass.end();
|
|
3990
|
+
|
|
3991
|
+
const innerW = ow - 2 * borderWidth;
|
|
3992
|
+
const innerH = oh - 2 * borderWidth;
|
|
3993
|
+
|
|
3994
|
+
if (innerW > 0 && innerH > 0) {
|
|
3995
|
+
const innerTex = ctx.renderer.getTemporaryTexture(borderTexW, borderTexH, [
|
|
3996
|
+
outerTex,
|
|
3997
|
+
...excludeTextures,
|
|
3998
|
+
]);
|
|
3999
|
+
const innerView = innerTex.createView();
|
|
4000
|
+
|
|
4001
|
+
const innerClearPass = ctx.renderer.beginFrame(
|
|
4002
|
+
encoder,
|
|
4003
|
+
innerView,
|
|
4004
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
4005
|
+
borderTexW,
|
|
4006
|
+
borderTexH,
|
|
4007
|
+
"clear",
|
|
4008
|
+
);
|
|
4009
|
+
innerClearPass.end();
|
|
4010
|
+
|
|
4011
|
+
const innerDrawPass = ctx.renderer.beginFrame(
|
|
4012
|
+
encoder,
|
|
4013
|
+
innerView,
|
|
4014
|
+
{ r: 0, g: 0, b: 0, a: 0 },
|
|
4015
|
+
borderTexW,
|
|
4016
|
+
borderTexH,
|
|
4017
|
+
"load",
|
|
4018
|
+
);
|
|
4019
|
+
ctx.renderer.drawRect(
|
|
4020
|
+
innerDrawPass,
|
|
4021
|
+
{ x: borderWidth, y: borderWidth, width: innerW, height: innerH },
|
|
4022
|
+
{ r: 1, g: 1, b: 1, a: 1 },
|
|
4023
|
+
innerRadius,
|
|
4024
|
+
);
|
|
4025
|
+
innerDrawPass.end();
|
|
4026
|
+
|
|
4027
|
+
const borderTex = ctx.renderer.composite(
|
|
4028
|
+
encoder,
|
|
4029
|
+
outerTex,
|
|
4030
|
+
innerTex,
|
|
4031
|
+
"mask-out",
|
|
4032
|
+
excludeTextures,
|
|
4033
|
+
);
|
|
4034
|
+
return { borderTex, ox, oy, ow, oh };
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
return { borderTex: outerTex, ox, oy, ow, oh };
|
|
4038
|
+
}
|