@multiplekex/shallot 0.2.4 → 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.
Files changed (62) hide show
  1. package/package.json +1 -1
  2. package/src/core/component.ts +1 -1
  3. package/src/core/index.ts +1 -13
  4. package/src/core/math.ts +186 -0
  5. package/src/core/state.ts +1 -1
  6. package/src/core/xml.ts +56 -41
  7. package/src/extras/arrows/index.ts +3 -3
  8. package/src/extras/caustic.ts +37 -0
  9. package/src/extras/gradient/index.ts +63 -69
  10. package/src/extras/index.ts +3 -0
  11. package/src/extras/lines/index.ts +3 -3
  12. package/src/extras/orbit/index.ts +1 -1
  13. package/src/extras/skylab/index.ts +314 -0
  14. package/src/extras/text/font.ts +69 -14
  15. package/src/extras/text/index.ts +17 -69
  16. package/src/extras/text/sdf.ts +13 -2
  17. package/src/extras/water/index.ts +119 -0
  18. package/src/standard/defaults.ts +2 -0
  19. package/src/standard/index.ts +2 -0
  20. package/src/standard/raster/batch.ts +149 -0
  21. package/src/standard/raster/forward.ts +832 -0
  22. package/src/standard/raster/index.ts +191 -0
  23. package/src/standard/raster/shadow.ts +408 -0
  24. package/src/standard/{render → raytracing}/bvh/blas.ts +336 -88
  25. package/src/standard/raytracing/bvh/radix.ts +473 -0
  26. package/src/standard/raytracing/bvh/refit.ts +711 -0
  27. package/src/standard/{render → raytracing}/bvh/structs.ts +0 -55
  28. package/src/standard/{render → raytracing}/bvh/tlas.ts +155 -140
  29. package/src/standard/{render → raytracing}/bvh/traverse.ts +72 -64
  30. package/src/standard/{render → raytracing}/depth.ts +9 -9
  31. package/src/standard/raytracing/index.ts +409 -0
  32. package/src/standard/{render → raytracing}/instance.ts +31 -16
  33. package/src/standard/{render → raytracing}/ray.ts +1 -1
  34. package/src/standard/raytracing/shaders.ts +798 -0
  35. package/src/standard/{render → raytracing}/triangle.ts +1 -1
  36. package/src/standard/render/camera.ts +96 -106
  37. package/src/standard/render/data.ts +1 -1
  38. package/src/standard/render/index.ts +136 -220
  39. package/src/standard/render/indirect.ts +9 -10
  40. package/src/standard/render/light.ts +2 -2
  41. package/src/standard/render/mesh.ts +404 -0
  42. package/src/standard/render/overlay.ts +8 -5
  43. package/src/standard/render/pass.ts +1 -1
  44. package/src/standard/render/postprocess.ts +263 -242
  45. package/src/standard/render/scene.ts +28 -16
  46. package/src/standard/render/surface/index.ts +81 -12
  47. package/src/standard/render/surface/shaders.ts +511 -0
  48. package/src/standard/render/surface/structs.ts +23 -6
  49. package/src/standard/tween/tween.ts +44 -115
  50. package/src/standard/render/bvh/radix.ts +0 -476
  51. package/src/standard/render/forward/index.ts +0 -259
  52. package/src/standard/render/forward/raster.ts +0 -228
  53. package/src/standard/render/mesh/box.ts +0 -20
  54. package/src/standard/render/mesh/index.ts +0 -446
  55. package/src/standard/render/mesh/plane.ts +0 -11
  56. package/src/standard/render/mesh/sphere.ts +0 -40
  57. package/src/standard/render/mesh/unified.ts +0 -96
  58. package/src/standard/render/shaders.ts +0 -484
  59. package/src/standard/render/surface/compile.ts +0 -67
  60. package/src/standard/render/surface/noise.ts +0 -45
  61. package/src/standard/render/surface/wgsl.ts +0 -573
  62. /package/src/standard/{render → raytracing}/intersection.ts +0 -0
@@ -0,0 +1,191 @@
1
+ import type { Plugin, State } from "../../core";
2
+ import { Compute } from "../compute";
3
+ import type { ComputeNode } from "../compute";
4
+ import { WorldTransform } from "../transforms";
5
+ import { Camera, Shadows, getClearColor, hasSkyComponent } from "../render/camera";
6
+ import { DirectionalLight, normalizeDirection } from "../render/light";
7
+ import { Mesh, MeshColors } from "../render/mesh";
8
+ import { Surface, SurfaceType, getDefaultAllSurfaces } from "../render/surface";
9
+ import { Render, RenderPlugin, registerShadowSettings } from "../render";
10
+ import { setTraits } from "../../core/component";
11
+ import {
12
+ createShadowAtlas,
13
+ createShadowBuffer,
14
+ createShadowUploadNode,
15
+ createShadowForwardNode,
16
+ } from "./shadow";
17
+ import { createBatch, uploadBatch, type Batch } from "./batch";
18
+ import { createRasterForwardNode } from "./forward";
19
+
20
+ export { compileRasterShader, compileCompositeShader } from "./forward";
21
+
22
+ export const RasterShadowSettings = {
23
+ softness: [] as number[],
24
+ samples: [] as number[],
25
+ distance: [] as number[],
26
+ };
27
+
28
+ setTraits(RasterShadowSettings, {
29
+ defaults: () => ({
30
+ softness: 0,
31
+ samples: 1,
32
+ distance: 100,
33
+ }),
34
+ });
35
+
36
+ import { resource } from "../../core";
37
+
38
+ export const RasterResource = resource<{
39
+ batching: Batch;
40
+ shadowAtlas: GPUTexture;
41
+ shadowBuffer: GPUBuffer;
42
+ rtRendered: boolean;
43
+ }>("raster");
44
+
45
+ export const RasterPlugin: Plugin = {
46
+ systems: [],
47
+ components: {
48
+ RasterShadowSettings,
49
+ },
50
+ dependencies: [RenderPlugin],
51
+
52
+ async initialize(state: State) {
53
+ const compute = Compute.from(state);
54
+ const render = Render.from(state);
55
+ if (!compute || !render) return;
56
+
57
+ registerShadowSettings({
58
+ softness: RasterShadowSettings.softness,
59
+ samples: RasterShadowSettings.samples,
60
+ isActive: (s, eid) => s.hasComponent(eid, RasterShadowSettings),
61
+ });
62
+
63
+ const { device } = compute;
64
+
65
+ const shadowAtlas = createShadowAtlas(device);
66
+ const shadowBuffer = createShadowBuffer(device);
67
+ const batching = createBatch(device);
68
+
69
+ const rasterState = {
70
+ batching,
71
+ shadowAtlas,
72
+ shadowBuffer,
73
+ rtRendered: false,
74
+ };
75
+
76
+ state.setResource(RasterResource, rasterState);
77
+
78
+ const getActiveCamera = () => {
79
+ for (const eid of state.query([Camera])) {
80
+ if (Camera.active[eid]) return eid;
81
+ }
82
+ return -1;
83
+ };
84
+
85
+ const getActiveClearColor = () => {
86
+ const eid = getActiveCamera();
87
+ if (eid >= 0) return getClearColor(eid);
88
+ return { r: 0, g: 0, b: 0 };
89
+ };
90
+
91
+ const getSky = () => {
92
+ const eid = getActiveCamera();
93
+ return eid >= 0 && hasSkyComponent(state, eid);
94
+ };
95
+
96
+ const getShadowsEnabled = () => {
97
+ const eid = getActiveCamera();
98
+ return eid >= 0 && state.hasComponent(eid, Shadows);
99
+ };
100
+
101
+ const getShadowDistance = () => {
102
+ const eid = getActiveCamera();
103
+ if (eid >= 0 && state.hasComponent(eid, RasterShadowSettings)) {
104
+ return RasterShadowSettings.distance[eid];
105
+ }
106
+ return 100;
107
+ };
108
+
109
+ const getCameraData = () => {
110
+ const eid = getActiveCamera();
111
+ if (eid >= 0) {
112
+ return {
113
+ world: WorldTransform.data.subarray(eid * 16, eid * 16 + 16),
114
+ fov: Camera.fov[eid],
115
+ near: Camera.near[eid],
116
+ far: Camera.far[eid],
117
+ width: render.width,
118
+ height: render.height,
119
+ };
120
+ }
121
+ return null;
122
+ };
123
+
124
+ const getLightDir = (): [number, number, number] => {
125
+ for (const eid of state.query([DirectionalLight])) {
126
+ const [dx, dy, dz] = normalizeDirection(
127
+ DirectionalLight.directionX[eid],
128
+ DirectionalLight.directionY[eid],
129
+ DirectionalLight.directionZ[eid]
130
+ );
131
+ return [dx, dy, dz];
132
+ }
133
+ return [-0.5, -1.0, -0.5];
134
+ };
135
+
136
+ const getOpacity = (eid: number) => MeshColors.data[eid * 4 + 3];
137
+
138
+ const batchUploadNode: ComputeNode = {
139
+ id: "batch-upload",
140
+ inputs: [{ id: "data", access: "read" }],
141
+ outputs: [{ id: "batched", access: "write" }],
142
+ execute() {
143
+ const meshEntities = state.query([Mesh, WorldTransform]);
144
+ uploadBatch(
145
+ device,
146
+ meshEntities,
147
+ (eid) => Surface.type[eid] ?? SurfaceType.Default,
148
+ getOpacity,
149
+ rasterState.batching
150
+ );
151
+ },
152
+ };
153
+ compute.graph.add(batchUploadNode);
154
+
155
+ const shadowUploadNode = createShadowUploadNode({
156
+ shadow: shadowBuffer,
157
+ getCameraData,
158
+ getLightDir,
159
+ shadowsEnabled: getShadowsEnabled,
160
+ getShadowDistance,
161
+ });
162
+ compute.graph.add(shadowUploadNode);
163
+
164
+ const shadowNodes = createShadowForwardNode({
165
+ shadow: shadowBuffer,
166
+ matrices: render.matrices,
167
+ sizes: render.sizes,
168
+ batching: rasterState.batching,
169
+ atlas: shadowAtlas,
170
+ shadowsEnabled: getShadowsEnabled,
171
+ });
172
+ for (const node of shadowNodes) compute.graph.add(node);
173
+
174
+ const forwardNode = createRasterForwardNode({
175
+ scene: render.scene,
176
+ sky: render.sky,
177
+ data: render.data,
178
+ matrices: render.matrices,
179
+ sizes: render.sizes,
180
+ batching: rasterState.batching,
181
+ shadowBuffer,
182
+ shadowAtlas,
183
+ getSurfaces: getDefaultAllSurfaces,
184
+ getClearColor: getActiveClearColor,
185
+ getSky,
186
+ getShadowsEnabled,
187
+ getRTRendered: () => rasterState.rtRendered,
188
+ });
189
+ compute.graph.add(forwardNode);
190
+ },
191
+ };
@@ -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
+ }