@multiplekex/shallot 0.2.5 → 0.3.0
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/package.json +1 -1
- package/src/core/component.ts +1 -1
- package/src/core/index.ts +1 -13
- package/src/core/math.ts +186 -0
- package/src/core/state.ts +1 -1
- package/src/core/xml.ts +56 -41
- package/src/extras/orbit/index.ts +1 -1
- package/src/extras/text/index.ts +10 -65
- package/src/extras/{water.ts → water/index.ts} +59 -4
- package/src/standard/raster/batch.ts +149 -0
- package/src/standard/raster/forward.ts +832 -0
- package/src/standard/raster/index.ts +146 -472
- package/src/standard/raster/shadow.ts +408 -0
- package/src/standard/raytracing/bvh/blas.ts +335 -87
- package/src/standard/raytracing/bvh/radix.ts +225 -228
- package/src/standard/raytracing/bvh/refit.ts +711 -0
- package/src/standard/raytracing/bvh/structs.ts +0 -55
- package/src/standard/raytracing/bvh/tlas.ts +153 -141
- package/src/standard/raytracing/bvh/traverse.ts +72 -64
- package/src/standard/raytracing/index.ts +233 -204
- package/src/standard/raytracing/instance.ts +30 -18
- package/src/standard/raytracing/ray.ts +1 -1
- package/src/standard/raytracing/shaders.ts +23 -40
- package/src/standard/render/camera.ts +10 -28
- package/src/standard/render/data.ts +1 -1
- package/src/standard/render/index.ts +68 -12
- package/src/standard/render/light.ts +2 -2
- package/src/standard/render/mesh.ts +404 -0
- package/src/standard/render/overlay.ts +5 -2
- package/src/standard/render/postprocess.ts +263 -267
- package/src/standard/render/surface/index.ts +81 -12
- package/src/standard/render/surface/shaders.ts +265 -11
- package/src/standard/render/surface/structs.ts +10 -0
- package/src/standard/tween/tween.ts +44 -115
- package/src/standard/render/mesh/box.ts +0 -20
- package/src/standard/render/mesh/index.ts +0 -315
- package/src/standard/render/mesh/plane.ts +0 -11
- package/src/standard/render/mesh/sphere.ts +0 -40
- package/src/standard/render/mesh/unified.ts +0 -96
- package/src/standard/render/surface/compile.ts +0 -65
- package/src/standard/render/surface/noise.ts +0 -58
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import type { ComputeNode, ExecutionContext } from "../compute";
|
|
2
|
+
import {
|
|
3
|
+
perspective,
|
|
4
|
+
multiply,
|
|
5
|
+
invert,
|
|
6
|
+
lookAtMatrix,
|
|
7
|
+
orthographicBounds,
|
|
8
|
+
extractFrustumCorners,
|
|
9
|
+
extractFrustumPlanes,
|
|
10
|
+
invertMatrix,
|
|
11
|
+
} from "../../core/math";
|
|
12
|
+
import { SHADOW_STRUCT_WGSL } from "../render/surface/structs";
|
|
13
|
+
import { VERTEX_BUFFERS, drawSlots } from "./forward";
|
|
14
|
+
import type { Batch } from "./batch";
|
|
15
|
+
|
|
16
|
+
const SHADOW_CASCADE_COUNT = 4;
|
|
17
|
+
const SHADOW_ATLAS_SIZE = 2048;
|
|
18
|
+
const SHADOW_CASCADE_SIZE = SHADOW_ATLAS_SIZE / 2;
|
|
19
|
+
const SHADOW_BUFFER_SIZE = 288;
|
|
20
|
+
|
|
21
|
+
export function computeCascadeSplits(
|
|
22
|
+
near: number,
|
|
23
|
+
far: number,
|
|
24
|
+
cascadeCount: number,
|
|
25
|
+
lambda = 0.5
|
|
26
|
+
): Float32Array {
|
|
27
|
+
const splits = new Float32Array(cascadeCount);
|
|
28
|
+
const ratio = far / near;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < cascadeCount; i++) {
|
|
31
|
+
const p = (i + 1) / cascadeCount;
|
|
32
|
+
const log = near * Math.pow(ratio, p);
|
|
33
|
+
const uniform = near + (far - near) * p;
|
|
34
|
+
splits[i] = lambda * log + (1 - lambda) * uniform;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return splits;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CascadeData {
|
|
41
|
+
viewProj: Float32Array;
|
|
42
|
+
frustumPlanes: Float32Array;
|
|
43
|
+
texelSize: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function computeCascadeMatrix(
|
|
47
|
+
cameraWorld: Float32Array,
|
|
48
|
+
fov: number,
|
|
49
|
+
aspect: number,
|
|
50
|
+
nearSplit: number,
|
|
51
|
+
farSplit: number,
|
|
52
|
+
lightDir: [number, number, number],
|
|
53
|
+
shadowMapSize: number
|
|
54
|
+
): CascadeData {
|
|
55
|
+
const proj = perspective(fov, aspect, nearSplit, farSplit);
|
|
56
|
+
const view = invert(cameraWorld);
|
|
57
|
+
const viewProj = multiply(proj, view);
|
|
58
|
+
const invViewProj = invertMatrix(viewProj);
|
|
59
|
+
|
|
60
|
+
const corners = extractFrustumCorners(invViewProj, 0, 1);
|
|
61
|
+
|
|
62
|
+
let centerX = 0,
|
|
63
|
+
centerY = 0,
|
|
64
|
+
centerZ = 0;
|
|
65
|
+
for (let i = 0; i < 8; i++) {
|
|
66
|
+
centerX += corners[i * 3];
|
|
67
|
+
centerY += corners[i * 3 + 1];
|
|
68
|
+
centerZ += corners[i * 3 + 2];
|
|
69
|
+
}
|
|
70
|
+
centerX /= 8;
|
|
71
|
+
centerY /= 8;
|
|
72
|
+
centerZ /= 8;
|
|
73
|
+
|
|
74
|
+
const [lightDirX, lightDirY, lightDirZ] = lightDir;
|
|
75
|
+
const len = Math.sqrt(lightDirX * lightDirX + lightDirY * lightDirY + lightDirZ * lightDirZ);
|
|
76
|
+
const normLightX = lightDirX / len;
|
|
77
|
+
const normLightY = lightDirY / len;
|
|
78
|
+
const normLightZ = lightDirZ / len;
|
|
79
|
+
|
|
80
|
+
let maxRadius = 0;
|
|
81
|
+
for (let i = 0; i < 8; i++) {
|
|
82
|
+
const dx = corners[i * 3] - centerX;
|
|
83
|
+
const dy = corners[i * 3 + 1] - centerY;
|
|
84
|
+
const dz = corners[i * 3 + 2] - centerZ;
|
|
85
|
+
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
86
|
+
maxRadius = Math.max(maxRadius, dist);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const shadowDistance = maxRadius * 2;
|
|
90
|
+
const eyeX = centerX - normLightX * shadowDistance;
|
|
91
|
+
const eyeY = centerY - normLightY * shadowDistance;
|
|
92
|
+
const eyeZ = centerZ - normLightZ * shadowDistance;
|
|
93
|
+
|
|
94
|
+
const lightView = lookAtMatrix(eyeX, eyeY, eyeZ, centerX, centerY, centerZ);
|
|
95
|
+
|
|
96
|
+
let minX = Infinity,
|
|
97
|
+
maxX = -Infinity;
|
|
98
|
+
let minY = Infinity,
|
|
99
|
+
maxY = -Infinity;
|
|
100
|
+
let minZ = Infinity,
|
|
101
|
+
maxZ = -Infinity;
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < 8; i++) {
|
|
104
|
+
const wx = corners[i * 3];
|
|
105
|
+
const wy = corners[i * 3 + 1];
|
|
106
|
+
const wz = corners[i * 3 + 2];
|
|
107
|
+
|
|
108
|
+
const lx = lightView[0] * wx + lightView[4] * wy + lightView[8] * wz + lightView[12];
|
|
109
|
+
const ly = lightView[1] * wx + lightView[5] * wy + lightView[9] * wz + lightView[13];
|
|
110
|
+
const lz = lightView[2] * wx + lightView[6] * wy + lightView[10] * wz + lightView[14];
|
|
111
|
+
|
|
112
|
+
minX = Math.min(minX, lx);
|
|
113
|
+
maxX = Math.max(maxX, lx);
|
|
114
|
+
minY = Math.min(minY, ly);
|
|
115
|
+
maxY = Math.max(maxY, ly);
|
|
116
|
+
minZ = Math.min(minZ, lz);
|
|
117
|
+
maxZ = Math.max(maxZ, lz);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const extendBack = shadowDistance;
|
|
121
|
+
minZ -= extendBack;
|
|
122
|
+
maxZ += maxRadius;
|
|
123
|
+
|
|
124
|
+
const texelSize = (maxX - minX) / shadowMapSize;
|
|
125
|
+
minX = Math.floor(minX / texelSize) * texelSize;
|
|
126
|
+
maxX = Math.ceil(maxX / texelSize) * texelSize;
|
|
127
|
+
minY = Math.floor(minY / texelSize) * texelSize;
|
|
128
|
+
maxY = Math.ceil(maxY / texelSize) * texelSize;
|
|
129
|
+
|
|
130
|
+
const lightProj = orthographicBounds(minX, maxX, minY, maxY, -maxZ, -minZ);
|
|
131
|
+
const cascadeViewProj = multiply(lightProj, lightView);
|
|
132
|
+
const frustumPlanes = extractFrustumPlanes(cascadeViewProj);
|
|
133
|
+
|
|
134
|
+
return { viewProj: cascadeViewProj, frustumPlanes, texelSize };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const shadowBuffer = new ArrayBuffer(SHADOW_BUFFER_SIZE);
|
|
138
|
+
const shadowF32 = new Float32Array(shadowBuffer);
|
|
139
|
+
|
|
140
|
+
function uploadShadowData(
|
|
141
|
+
device: GPUDevice,
|
|
142
|
+
buffer: GPUBuffer,
|
|
143
|
+
cascades: CascadeData[],
|
|
144
|
+
splits: Float32Array
|
|
145
|
+
): void {
|
|
146
|
+
for (let i = 0; i < SHADOW_CASCADE_COUNT; i++) {
|
|
147
|
+
shadowF32.set(cascades[i].viewProj, i * 16);
|
|
148
|
+
}
|
|
149
|
+
shadowF32[64] = splits[0];
|
|
150
|
+
shadowF32[65] = splits[1];
|
|
151
|
+
shadowF32[66] = splits[2];
|
|
152
|
+
shadowF32[67] = splits[3];
|
|
153
|
+
|
|
154
|
+
shadowF32[68] = cascades[0].texelSize;
|
|
155
|
+
shadowF32[69] = cascades[1].texelSize;
|
|
156
|
+
shadowF32[70] = cascades[2].texelSize;
|
|
157
|
+
shadowF32[71] = cascades[3].texelSize;
|
|
158
|
+
|
|
159
|
+
device.queue.writeBuffer(buffer, 0, shadowBuffer);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createShadowBuffer(device: GPUDevice): GPUBuffer {
|
|
163
|
+
return device.createBuffer({
|
|
164
|
+
label: "shadow",
|
|
165
|
+
size: SHADOW_BUFFER_SIZE,
|
|
166
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createShadowAtlas(device: GPUDevice): GPUTexture {
|
|
171
|
+
return device.createTexture({
|
|
172
|
+
label: "shadow-atlas",
|
|
173
|
+
size: [SHADOW_ATLAS_SIZE, SHADOW_ATLAS_SIZE, 1],
|
|
174
|
+
format: "depth32float",
|
|
175
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface ShadowUploadConfig {
|
|
180
|
+
shadow: GPUBuffer;
|
|
181
|
+
getCameraData: () => {
|
|
182
|
+
world: Float32Array;
|
|
183
|
+
fov: number;
|
|
184
|
+
near: number;
|
|
185
|
+
far: number;
|
|
186
|
+
width: number;
|
|
187
|
+
height: number;
|
|
188
|
+
} | null;
|
|
189
|
+
getLightDir: () => [number, number, number];
|
|
190
|
+
shadowsEnabled: () => boolean;
|
|
191
|
+
getShadowDistance: () => number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createShadowUploadNode(config: ShadowUploadConfig): ComputeNode {
|
|
195
|
+
let cascades: CascadeData[] = [];
|
|
196
|
+
let splits: Float32Array = new Float32Array(SHADOW_CASCADE_COUNT);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
id: "shadow-cascade-upload",
|
|
200
|
+
inputs: [{ id: "data", access: "read" }],
|
|
201
|
+
outputs: [{ id: "shadow-cascades", access: "write" }],
|
|
202
|
+
|
|
203
|
+
execute(ctx: ExecutionContext) {
|
|
204
|
+
if (!config.shadowsEnabled()) return;
|
|
205
|
+
|
|
206
|
+
const camera = config.getCameraData();
|
|
207
|
+
if (!camera) return;
|
|
208
|
+
|
|
209
|
+
const { world, fov, near, far, width, height } = camera;
|
|
210
|
+
const aspect = width / height;
|
|
211
|
+
const lightDir = config.getLightDir();
|
|
212
|
+
|
|
213
|
+
const shadowDistance = config.getShadowDistance();
|
|
214
|
+
const effectiveFar = Math.min(far, shadowDistance);
|
|
215
|
+
|
|
216
|
+
splits = computeCascadeSplits(near, effectiveFar, SHADOW_CASCADE_COUNT);
|
|
217
|
+
cascades = [];
|
|
218
|
+
|
|
219
|
+
let prevSplit = near;
|
|
220
|
+
for (let i = 0; i < SHADOW_CASCADE_COUNT; i++) {
|
|
221
|
+
const cascade = computeCascadeMatrix(
|
|
222
|
+
world,
|
|
223
|
+
fov,
|
|
224
|
+
aspect,
|
|
225
|
+
prevSplit,
|
|
226
|
+
splits[i],
|
|
227
|
+
lightDir,
|
|
228
|
+
SHADOW_CASCADE_SIZE
|
|
229
|
+
);
|
|
230
|
+
cascades.push(cascade);
|
|
231
|
+
prevSplit = splits[i];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
uploadShadowData(ctx.device, config.shadow, cascades, splits);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
interface ShadowForwardGPU {
|
|
240
|
+
pipeline: GPURenderPipeline;
|
|
241
|
+
cascadeIndexBuffers: GPUBuffer[];
|
|
242
|
+
cascadeBindGroups: GPUBindGroup[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const shadowDepthShader = /* wgsl */ `
|
|
246
|
+
struct VertexInput {
|
|
247
|
+
@location(0) position: vec3<f32>,
|
|
248
|
+
@location(1) normal: vec3<f32>,
|
|
249
|
+
@builtin(instance_index) instance: u32,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
struct VertexOutput {
|
|
253
|
+
@builtin(position) position: vec4<f32>,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
${SHADOW_STRUCT_WGSL}
|
|
257
|
+
|
|
258
|
+
@group(0) @binding(0) var<uniform> shadow: Shadow;
|
|
259
|
+
@group(0) @binding(1) var<storage, read> entityIds: array<u32>;
|
|
260
|
+
@group(0) @binding(2) var<storage, read> matrices: array<mat4x4<f32>>;
|
|
261
|
+
@group(0) @binding(3) var<storage, read> sizes: array<vec4<f32>>;
|
|
262
|
+
@group(0) @binding(4) var<uniform> cascadeIndex: u32;
|
|
263
|
+
|
|
264
|
+
fn getCascadeViewProj(cascade: u32) -> mat4x4<f32> {
|
|
265
|
+
switch cascade {
|
|
266
|
+
case 0u: { return shadow.cascade0ViewProj; }
|
|
267
|
+
case 1u: { return shadow.cascade1ViewProj; }
|
|
268
|
+
case 2u: { return shadow.cascade2ViewProj; }
|
|
269
|
+
default: { return shadow.cascade3ViewProj; }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@vertex
|
|
274
|
+
fn vs(input: VertexInput) -> VertexOutput {
|
|
275
|
+
let eid = entityIds[input.instance];
|
|
276
|
+
let world = matrices[eid];
|
|
277
|
+
let scaledPos = input.position * sizes[eid].xyz;
|
|
278
|
+
let worldPos = (world * vec4<f32>(scaledPos, 1.0)).xyz;
|
|
279
|
+
let viewProj = getCascadeViewProj(cascadeIndex);
|
|
280
|
+
|
|
281
|
+
var output: VertexOutput;
|
|
282
|
+
output.position = viewProj * vec4<f32>(worldPos, 1.0);
|
|
283
|
+
return output;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@fragment
|
|
287
|
+
fn fs() {}
|
|
288
|
+
`;
|
|
289
|
+
|
|
290
|
+
async function prepareShadowForwardGPU(
|
|
291
|
+
device: GPUDevice,
|
|
292
|
+
config: ShadowForwardConfig
|
|
293
|
+
): Promise<ShadowForwardGPU> {
|
|
294
|
+
const module = device.createShaderModule({ code: shadowDepthShader });
|
|
295
|
+
const pipeline = await device.createRenderPipelineAsync({
|
|
296
|
+
layout: "auto",
|
|
297
|
+
vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
|
|
298
|
+
fragment: { module, entryPoint: "fs", targets: [] },
|
|
299
|
+
depthStencil: {
|
|
300
|
+
format: "depth32float",
|
|
301
|
+
depthWriteEnabled: true,
|
|
302
|
+
depthCompare: "less",
|
|
303
|
+
},
|
|
304
|
+
primitive: { topology: "triangle-list", cullMode: "back" },
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const cascadeIndexBuffers: GPUBuffer[] = [];
|
|
308
|
+
for (let i = 0; i < 4; i++) {
|
|
309
|
+
const buf = device.createBuffer({
|
|
310
|
+
label: `cascade-index-${i}`,
|
|
311
|
+
size: 4,
|
|
312
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
313
|
+
});
|
|
314
|
+
device.queue.writeBuffer(buf, 0, new Uint32Array([i]));
|
|
315
|
+
cascadeIndexBuffers.push(buf);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const layout = pipeline.getBindGroupLayout(0);
|
|
319
|
+
const cascadeBindGroups = cascadeIndexBuffers.map((indexBuf) =>
|
|
320
|
+
device.createBindGroup({
|
|
321
|
+
layout,
|
|
322
|
+
entries: [
|
|
323
|
+
{ binding: 0, resource: { buffer: config.shadow } },
|
|
324
|
+
{ binding: 1, resource: { buffer: config.batching.entityIds } },
|
|
325
|
+
{ binding: 2, resource: { buffer: config.matrices } },
|
|
326
|
+
{ binding: 3, resource: { buffer: config.sizes } },
|
|
327
|
+
{ binding: 4, resource: { buffer: indexBuf } },
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
return { pipeline, cascadeIndexBuffers, cascadeBindGroups };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface ShadowForwardConfig {
|
|
336
|
+
shadow: GPUBuffer;
|
|
337
|
+
matrices: GPUBuffer;
|
|
338
|
+
sizes: GPUBuffer;
|
|
339
|
+
batching: Batch;
|
|
340
|
+
atlas: GPUTexture;
|
|
341
|
+
shadowsEnabled: () => boolean;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function createShadowForwardNode(config: ShadowForwardConfig): ComputeNode[] {
|
|
345
|
+
let gpu: ShadowForwardGPU | null = null;
|
|
346
|
+
|
|
347
|
+
const nodes: ComputeNode[] = [];
|
|
348
|
+
|
|
349
|
+
for (let cascade = 0; cascade < 4; cascade++) {
|
|
350
|
+
const inputId = cascade === 0 ? "shadow-cascades" : `shadow-cascade-${cascade - 1}`;
|
|
351
|
+
|
|
352
|
+
nodes.push({
|
|
353
|
+
id: `shadow-render-${cascade}`,
|
|
354
|
+
inputs: [
|
|
355
|
+
{ id: inputId, access: "read" },
|
|
356
|
+
{ id: "batched", access: "read" },
|
|
357
|
+
],
|
|
358
|
+
outputs: [{ id: `shadow-cascade-${cascade}`, access: "write" }],
|
|
359
|
+
|
|
360
|
+
async prepare(device: GPUDevice) {
|
|
361
|
+
if (!gpu) {
|
|
362
|
+
gpu = await prepareShadowForwardGPU(device, config);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
execute(ctx: ExecutionContext) {
|
|
367
|
+
if (!gpu || !config.shadowsEnabled()) return;
|
|
368
|
+
|
|
369
|
+
const buffers = config.batching.buffers;
|
|
370
|
+
const offsetX = (cascade % 2) * SHADOW_CASCADE_SIZE;
|
|
371
|
+
const offsetY = Math.floor(cascade / 2) * SHADOW_CASCADE_SIZE;
|
|
372
|
+
|
|
373
|
+
const pass = ctx.encoder.beginRenderPass({
|
|
374
|
+
colorAttachments: [],
|
|
375
|
+
depthStencilAttachment: {
|
|
376
|
+
view: config.atlas.createView(),
|
|
377
|
+
depthClearValue: 1.0,
|
|
378
|
+
depthLoadOp: cascade === 0 ? "clear" : "load",
|
|
379
|
+
depthStoreOp: "store",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
pass.setViewport(offsetX, offsetY, SHADOW_CASCADE_SIZE, SHADOW_CASCADE_SIZE, 0, 1);
|
|
384
|
+
pass.setScissorRect(offsetX, offsetY, SHADOW_CASCADE_SIZE, SHADOW_CASCADE_SIZE);
|
|
385
|
+
pass.setPipeline(gpu.pipeline);
|
|
386
|
+
pass.setBindGroup(0, gpu.cascadeBindGroups[cascade]);
|
|
387
|
+
|
|
388
|
+
drawSlots(pass, config.batching.opaque, buffers, ctx.device);
|
|
389
|
+
|
|
390
|
+
pass.end();
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const doneNode: ComputeNode = {
|
|
396
|
+
id: "shadow-done",
|
|
397
|
+
inputs: [
|
|
398
|
+
{ id: "shadow-cascade-0", access: "read" },
|
|
399
|
+
{ id: "shadow-cascade-1", access: "read" },
|
|
400
|
+
{ id: "shadow-cascade-2", access: "read" },
|
|
401
|
+
{ id: "shadow-cascade-3", access: "read" },
|
|
402
|
+
],
|
|
403
|
+
outputs: [{ id: "shadow-atlas", access: "write" }],
|
|
404
|
+
execute() {},
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return [...nodes, doneNode];
|
|
408
|
+
}
|