@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,832 @@
1
+ import type { ComputeNode, ExecutionContext } from "../compute";
2
+ import { getMesh, createMeshBuffers, type MeshBuffers } from "../render/mesh";
3
+ import type { SurfaceData } from "../render/surface";
4
+ import { COLOR_FORMAT } from "../render/scene";
5
+ import {
6
+ SCENE_STRUCT_WGSL,
7
+ SKY_STRUCT_WGSL,
8
+ SHADOW_STRUCT_WGSL,
9
+ WGSL_STRUCTS,
10
+ } from "../render/surface/structs";
11
+ import {
12
+ compileVertexBody,
13
+ WGSL_LIGHTING_CALC,
14
+ SPECULAR_WGSL,
15
+ SHADOW_SAMPLE_WGSL,
16
+ SKY_WGSL,
17
+ SKY_DIR_WGSL,
18
+ NOISE_WGSL,
19
+ HAZE_WGSL,
20
+ } from "../render/surface/shaders";
21
+ import { MAX_BATCH_SLOTS } from "../render/mesh";
22
+ import type { Batch } from "./batch";
23
+ import { slotInstanceCount, slotIndexCount, slotFirstInstance, slotShapeId } from "./batch";
24
+
25
+ const ACCUM_FORMAT: GPUTextureFormat = "rgba16float";
26
+ const REVEAL_FORMAT: GPUTextureFormat = "r8unorm";
27
+
28
+ function compileSurfaceVariant(id: number, data: SurfaceData): string {
29
+ const vertexBody = compileVertexBody(data.vertex);
30
+ const fragmentBody = data.fragment ?? "";
31
+
32
+ return `
33
+ fn userVertexTransform_${id}(worldPos: vec3<f32>, normal: vec3<f32>, eid: u32) -> vec3<f32> {
34
+ ${vertexBody}
35
+ }
36
+
37
+ fn userFragment_${id}(surface: ptr<function, SurfaceData>, position: vec4<f32>) {
38
+ ${fragmentBody}
39
+ }
40
+
41
+ fn applyLighting_${id}(surface: SurfaceData, shadowFactor: f32) -> vec3<f32> {
42
+ ${WGSL_LIGHTING_CALC}
43
+ return litColor;
44
+ }
45
+ `;
46
+ }
47
+
48
+ function compileDispatchFunctions(surfaceCount: number): string {
49
+ const vertexCases = Array.from(
50
+ { length: surfaceCount },
51
+ (_, i) => ` case ${i}u: { return userVertexTransform_${i}(worldPos, normal, eid); }`
52
+ ).join("\n");
53
+
54
+ const fragmentCases = Array.from(
55
+ { length: surfaceCount },
56
+ (_, i) => ` case ${i}u: { userFragment_${i}(surface, position); }`
57
+ ).join("\n");
58
+
59
+ const lightingCases = Array.from(
60
+ { length: surfaceCount },
61
+ (_, i) => ` case ${i}u: { return applyLighting_${i}(surface, shadowFactor); }`
62
+ ).join("\n");
63
+
64
+ return `
65
+ fn dispatchVertexTransform(surfaceId: u32, worldPos: vec3<f32>, normal: vec3<f32>, eid: u32) -> vec3<f32> {
66
+ switch surfaceId {
67
+ ${vertexCases}
68
+ default: { return userVertexTransform_0(worldPos, normal, eid); }
69
+ }
70
+ }
71
+
72
+ fn dispatchFragment(surfaceId: u32, surface: ptr<function, SurfaceData>, position: vec4<f32>) {
73
+ switch surfaceId {
74
+ ${fragmentCases}
75
+ default: { userFragment_0(surface, position); }
76
+ }
77
+ }
78
+
79
+ fn dispatchLighting(surfaceId: u32, surface: SurfaceData, shadowFactor: f32) -> vec3<f32> {
80
+ switch surfaceId {
81
+ ${lightingCases}
82
+ default: { return applyLighting_0(surface, shadowFactor); }
83
+ }
84
+ }
85
+ `;
86
+ }
87
+
88
+ export function compileRasterShader(surfaces: SurfaceData[], useShadows: boolean): string {
89
+ const surfaceVariants = surfaces.map((s, i) => compileSurfaceVariant(i, s)).join("\n");
90
+ const dispatchFunctions = compileDispatchFunctions(surfaces.length);
91
+
92
+ const shadowBindings = useShadows
93
+ ? /* wgsl */ `
94
+ ${SHADOW_STRUCT_WGSL}
95
+
96
+ @group(1) @binding(0) var<uniform> shadow: Shadow;
97
+ @group(1) @binding(1) var shadowMap: texture_depth_2d;
98
+ @group(1) @binding(2) var shadowSampler: sampler_comparison;
99
+
100
+ ${SHADOW_SAMPLE_WGSL}
101
+ `
102
+ : "";
103
+
104
+ const shadowCompute = useShadows
105
+ ? /* wgsl */ `
106
+ let viewZ = -dot(scene.cameraWorld[2].xyz, surface.worldPos - scene.cameraWorld[3].xyz);
107
+ let shadowFactor = sampleShadowPCF(surface.worldPos, surface.normal, viewZ, scene.shadowSoftness, scene.shadowSamples);
108
+ `
109
+ : /* wgsl */ `
110
+ let shadowFactor = 1.0;
111
+ `;
112
+
113
+ return /* wgsl */ `
114
+ ${WGSL_STRUCTS}
115
+ ${SKY_STRUCT_WGSL}
116
+
117
+ @group(0) @binding(5) var<uniform> sky: Sky;
118
+
119
+ ${HAZE_WGSL}
120
+ ${NOISE_WGSL}
121
+ ${SPECULAR_WGSL}
122
+ ${shadowBindings}
123
+
124
+ ${surfaceVariants}
125
+ ${dispatchFunctions}
126
+
127
+ @vertex
128
+ fn vs(input: VertexInput) -> VertexOutput {
129
+ let eid = entityIds[input.instance];
130
+ let world = matrices[eid];
131
+ let scaledPos = input.position * sizes[eid].xyz;
132
+ let baseWorldPos = (world * vec4<f32>(scaledPos, 1.0)).xyz;
133
+ let worldNormal = normalize((world * vec4<f32>(input.normal, 0.0)).xyz);
134
+ let d = data[eid];
135
+ let surfaceId = d.flags & 0xFFu;
136
+ let finalWorldPos = dispatchVertexTransform(surfaceId, baseWorldPos, worldNormal, eid);
137
+
138
+ var output: VertexOutput;
139
+ output.position = scene.viewProj * vec4<f32>(finalWorldPos, 1.0);
140
+ output.color = d.baseColor;
141
+ output.worldNormal = worldNormal;
142
+ output.entityId = eid;
143
+ output.worldPos = finalWorldPos;
144
+ return output;
145
+ }
146
+
147
+ @fragment
148
+ fn fs(input: VertexOutput) -> FragmentOutput {
149
+ let eid = input.entityId;
150
+ let d = data[eid];
151
+ let surfaceId = d.flags & 0xFFu;
152
+
153
+ var surface: SurfaceData;
154
+ surface.baseColor = input.color.rgb;
155
+ surface.roughness = d.pbr.x;
156
+ surface.metallic = d.pbr.y;
157
+ surface.emission = d.emission.rgb * d.emission.a;
158
+ surface.normal = normalize(input.worldNormal);
159
+ surface.worldPos = input.worldPos;
160
+
161
+ dispatchFragment(surfaceId, &surface, input.position);
162
+
163
+ ${shadowCompute}
164
+ let litColor = dispatchLighting(surfaceId, surface, shadowFactor);
165
+ let dist = length(input.worldPos - scene.cameraWorld[3].xyz);
166
+
167
+ var output: FragmentOutput;
168
+ output.color = vec4<f32>(applyHaze(litColor, dist), input.color.a);
169
+ output.entityId = input.entityId;
170
+ return output;
171
+ }
172
+
173
+ struct TransparentOutput {
174
+ @location(0) accum: vec4<f32>,
175
+ @location(1) reveal: f32,
176
+ }
177
+
178
+ @fragment
179
+ fn fs_transparent(input: VertexOutput) -> TransparentOutput {
180
+ let eid = input.entityId;
181
+ let d = data[eid];
182
+ let surfaceId = d.flags & 0xFFu;
183
+
184
+ var surface: SurfaceData;
185
+ surface.baseColor = input.color.rgb;
186
+ surface.roughness = d.pbr.x;
187
+ surface.metallic = d.pbr.y;
188
+ surface.opacity = input.color.a;
189
+ surface.emission = d.emission.rgb * d.emission.a;
190
+ surface.normal = normalize(input.worldNormal);
191
+ surface.worldPos = input.worldPos;
192
+
193
+ dispatchFragment(surfaceId, &surface, input.position);
194
+
195
+ let alpha = clamp(surface.opacity, 0.0, 1.0);
196
+
197
+ ${shadowCompute}
198
+ let litColor = dispatchLighting(surfaceId, surface, shadowFactor);
199
+ let dist = length(input.worldPos - scene.cameraWorld[3].xyz);
200
+ let color = applyHaze(litColor, dist);
201
+
202
+ let z = input.position.z;
203
+ let w = clamp(alpha * max(1e-2, 3e3 * pow(1.0 - z, 3.0)), 1e-2, 3e3);
204
+
205
+ var output: TransparentOutput;
206
+ output.accum = vec4<f32>(color * alpha * w, alpha * w);
207
+ output.reveal = alpha;
208
+ return output;
209
+ }
210
+ `;
211
+ }
212
+
213
+ export function compileCompositeShader(): string {
214
+ return /* wgsl */ `
215
+ @group(0) @binding(0) var accumTexture: texture_2d<f32>;
216
+ @group(0) @binding(1) var revealTexture: texture_2d<f32>;
217
+
218
+ struct VertexOutput {
219
+ @builtin(position) position: vec4<f32>,
220
+ }
221
+
222
+ @vertex
223
+ fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
224
+ var positions = array<vec2<f32>, 3>(
225
+ vec2(-1.0, -1.0),
226
+ vec2(3.0, -1.0),
227
+ vec2(-1.0, 3.0)
228
+ );
229
+ var output: VertexOutput;
230
+ output.position = vec4(positions[vertexIndex], 0.0, 1.0);
231
+ return output;
232
+ }
233
+
234
+ @fragment
235
+ fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
236
+ let coords = vec2<i32>(input.position.xy);
237
+ let accum = textureLoad(accumTexture, coords, 0);
238
+ let reveal = textureLoad(revealTexture, coords, 0).r;
239
+
240
+ if (accum.a <= 1e-5) {
241
+ discard;
242
+ }
243
+
244
+ let avgColor = accum.rgb / max(accum.a, 1e-5);
245
+ return vec4<f32>(avgColor, 1.0 - reveal);
246
+ }
247
+ `;
248
+ }
249
+
250
+ function compileSkyShader(): string {
251
+ return /* wgsl */ `
252
+ ${SCENE_STRUCT_WGSL}
253
+ ${SKY_STRUCT_WGSL}
254
+
255
+ @group(0) @binding(0) var<uniform> scene: Scene;
256
+ @group(0) @binding(1) var<uniform> sky: Sky;
257
+
258
+ ${SKY_DIR_WGSL}
259
+ ${NOISE_WGSL}
260
+ ${SKY_WGSL}
261
+
262
+ struct VertexOutput {
263
+ @builtin(position) position: vec4<f32>,
264
+ @location(0) uv: vec2<f32>,
265
+ }
266
+
267
+ @vertex
268
+ fn vs(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
269
+ var positions = array<vec2<f32>, 3>(
270
+ vec2(-1.0, -1.0),
271
+ vec2(3.0, -1.0),
272
+ vec2(-1.0, 3.0)
273
+ );
274
+ var output: VertexOutput;
275
+ output.position = vec4(positions[vertexIndex], 0.0, 1.0);
276
+ output.uv = (positions[vertexIndex] + 1.0) * 0.5;
277
+ output.uv.y = 1.0 - output.uv.y;
278
+ return output;
279
+ }
280
+
281
+ @fragment
282
+ fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
283
+ let dir = computeSkyDir(input.uv.x, input.uv.y);
284
+ let color = sampleSky(dir);
285
+ return vec4(color, 1.0);
286
+ }
287
+ `;
288
+ }
289
+
290
+ export const VERTEX_BUFFERS: GPUVertexBufferLayout[] = [
291
+ {
292
+ arrayStride: 24,
293
+ attributes: [
294
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
295
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
296
+ ],
297
+ },
298
+ ];
299
+
300
+ interface ForwardGPU {
301
+ opaque: { pipeline: GPURenderPipeline; module: GPUShaderModule };
302
+ opaqueShadowed: { pipeline: GPURenderPipeline; module: GPUShaderModule };
303
+ transparent: GPURenderPipeline;
304
+ transparentShadowed: GPURenderPipeline;
305
+ composite: GPURenderPipeline;
306
+ sky: GPURenderPipeline;
307
+ }
308
+
309
+ interface ForwardBindGroups {
310
+ main: GPUBindGroup;
311
+ mainShadowed: GPUBindGroup;
312
+ transparent: GPUBindGroup;
313
+ transparentShadowed: GPUBindGroup;
314
+ shadow: GPUBindGroup;
315
+ transparentShadow: GPUBindGroup;
316
+ sky: GPUBindGroup;
317
+ composite: GPUBindGroup | null;
318
+ }
319
+
320
+ interface TransparentState {
321
+ accumTexture: GPUTexture;
322
+ accumView: GPUTextureView;
323
+ revealTexture: GPUTexture;
324
+ revealView: GPUTextureView;
325
+ width: number;
326
+ height: number;
327
+ }
328
+
329
+ async function createRasterPipeline(
330
+ device: GPUDevice,
331
+ surfaces: SurfaceData[],
332
+ colorFormat: GPUTextureFormat,
333
+ useShadows: boolean
334
+ ): Promise<{ pipeline: GPURenderPipeline; module: GPUShaderModule }> {
335
+ const code = compileRasterShader(surfaces, useShadows);
336
+ const module = device.createShaderModule({ code });
337
+
338
+ const pipeline = await device.createRenderPipelineAsync({
339
+ layout: "auto",
340
+ vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
341
+ fragment: {
342
+ module,
343
+ entryPoint: "fs",
344
+ targets: [{ format: colorFormat }, { format: "r32uint" }],
345
+ },
346
+ depthStencil: {
347
+ format: "depth24plus",
348
+ depthWriteEnabled: true,
349
+ depthCompare: "less",
350
+ },
351
+ primitive: { topology: "triangle-list", cullMode: "back" },
352
+ });
353
+
354
+ return { pipeline, module };
355
+ }
356
+
357
+ async function createTransparentPipeline(
358
+ device: GPUDevice,
359
+ module: GPUShaderModule
360
+ ): Promise<GPURenderPipeline> {
361
+ return device.createRenderPipelineAsync({
362
+ layout: "auto",
363
+ vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
364
+ fragment: {
365
+ module,
366
+ entryPoint: "fs_transparent",
367
+ targets: [
368
+ {
369
+ format: ACCUM_FORMAT,
370
+ blend: {
371
+ color: { srcFactor: "one", dstFactor: "one" },
372
+ alpha: { srcFactor: "one", dstFactor: "one" },
373
+ },
374
+ },
375
+ {
376
+ format: REVEAL_FORMAT,
377
+ blend: {
378
+ color: { srcFactor: "zero", dstFactor: "one-minus-src" },
379
+ alpha: { srcFactor: "zero", dstFactor: "one-minus-src" },
380
+ },
381
+ },
382
+ ],
383
+ },
384
+ depthStencil: {
385
+ format: "depth24plus",
386
+ depthWriteEnabled: false,
387
+ depthCompare: "less-equal",
388
+ },
389
+ primitive: { topology: "triangle-list", cullMode: "none" },
390
+ });
391
+ }
392
+
393
+ async function createSkyPipeline(
394
+ device: GPUDevice,
395
+ colorFormat: GPUTextureFormat
396
+ ): Promise<GPURenderPipeline> {
397
+ const code = compileSkyShader();
398
+ const module = device.createShaderModule({ code });
399
+
400
+ return device.createRenderPipelineAsync({
401
+ layout: "auto",
402
+ vertex: { module, entryPoint: "vs" },
403
+ fragment: {
404
+ module,
405
+ entryPoint: "fs",
406
+ targets: [{ format: colorFormat }],
407
+ },
408
+ depthStencil: {
409
+ format: "depth24plus",
410
+ depthWriteEnabled: false,
411
+ depthCompare: "always",
412
+ },
413
+ primitive: { topology: "triangle-list" },
414
+ });
415
+ }
416
+
417
+ async function prepareForwardGPU(device: GPUDevice, surfaces: SurfaceData[]): Promise<ForwardGPU> {
418
+ const [opaque, opaqueShadowed, sky] = await Promise.all([
419
+ createRasterPipeline(device, surfaces, COLOR_FORMAT, false),
420
+ createRasterPipeline(device, surfaces, COLOR_FORMAT, true),
421
+ createSkyPipeline(device, COLOR_FORMAT),
422
+ ]);
423
+
424
+ const [transparent, transparentShadowed, composite] = await Promise.all([
425
+ createTransparentPipeline(device, opaque.module),
426
+ createTransparentPipeline(device, opaqueShadowed.module),
427
+ createCompositePipeline(device),
428
+ ]);
429
+
430
+ return { opaque, opaqueShadowed, transparent, transparentShadowed, composite, sky };
431
+ }
432
+
433
+ async function createCompositePipeline(device: GPUDevice): Promise<GPURenderPipeline> {
434
+ const compositeModule = device.createShaderModule({ code: compileCompositeShader() });
435
+ return device.createRenderPipelineAsync({
436
+ layout: "auto",
437
+ vertex: { module: compositeModule, entryPoint: "vs" },
438
+ fragment: {
439
+ module: compositeModule,
440
+ entryPoint: "fs",
441
+ targets: [
442
+ {
443
+ format: COLOR_FORMAT,
444
+ blend: {
445
+ color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
446
+ alpha: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
447
+ },
448
+ },
449
+ ],
450
+ },
451
+ primitive: { topology: "triangle-list" },
452
+ });
453
+ }
454
+
455
+ function createForwardBindGroups(
456
+ device: GPUDevice,
457
+ gpu: ForwardGPU,
458
+ config: ForwardConfig
459
+ ): ForwardBindGroups {
460
+ const sceneEntries: GPUBindGroupEntry[] = [
461
+ { binding: 0, resource: { buffer: config.scene } },
462
+ { binding: 1, resource: { buffer: config.batching.entityIds } },
463
+ { binding: 2, resource: { buffer: config.matrices } },
464
+ { binding: 3, resource: { buffer: config.sizes } },
465
+ { binding: 4, resource: { buffer: config.data } },
466
+ { binding: 5, resource: { buffer: config.sky } },
467
+ ];
468
+
469
+ const bindScene = (pipeline: GPURenderPipeline) =>
470
+ device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: sceneEntries });
471
+
472
+ const shadowSampler = device.createSampler({
473
+ compare: "less",
474
+ magFilter: "linear",
475
+ minFilter: "linear",
476
+ });
477
+ const shadowEntries: GPUBindGroupEntry[] = [
478
+ { binding: 0, resource: { buffer: config.shadowBuffer } },
479
+ { binding: 1, resource: config.shadowAtlas.createView() },
480
+ { binding: 2, resource: shadowSampler },
481
+ ];
482
+
483
+ const bindShadow = (pipeline: GPURenderPipeline) =>
484
+ device.createBindGroup({ layout: pipeline.getBindGroupLayout(1), entries: shadowEntries });
485
+
486
+ return {
487
+ main: bindScene(gpu.opaque.pipeline),
488
+ mainShadowed: bindScene(gpu.opaqueShadowed.pipeline),
489
+ transparent: bindScene(gpu.transparent),
490
+ transparentShadowed: bindScene(gpu.transparentShadowed),
491
+ shadow: bindShadow(gpu.opaqueShadowed.pipeline),
492
+ transparentShadow: bindShadow(gpu.transparentShadowed),
493
+ sky: device.createBindGroup({
494
+ layout: gpu.sky.getBindGroupLayout(0),
495
+ entries: [
496
+ { binding: 0, resource: { buffer: config.scene } },
497
+ { binding: 1, resource: { buffer: config.sky } },
498
+ ],
499
+ }),
500
+ composite: null,
501
+ };
502
+ }
503
+
504
+ export function drawSlots(
505
+ pass: GPURenderPassEncoder,
506
+ slots: Uint32Array,
507
+ buffers: Map<number, MeshBuffers>,
508
+ device: GPUDevice
509
+ ): void {
510
+ for (let i = 0; i < MAX_BATCH_SLOTS; i++) {
511
+ const instances = slotInstanceCount(slots, i);
512
+ if (instances === 0) continue;
513
+
514
+ const shapeId = slotShapeId(slots, i);
515
+ let shapeBuffers = buffers.get(shapeId);
516
+ if (!shapeBuffers) {
517
+ const meshData = getMesh(shapeId);
518
+ if (!meshData) continue;
519
+ shapeBuffers = createMeshBuffers(device, meshData);
520
+ buffers.set(shapeId, shapeBuffers);
521
+ }
522
+
523
+ pass.setVertexBuffer(0, shapeBuffers.vertex);
524
+ pass.setIndexBuffer(shapeBuffers.index, "uint16");
525
+ pass.drawIndexed(slotIndexCount(slots, i), instances, 0, 0, slotFirstInstance(slots, i));
526
+ }
527
+ }
528
+
529
+ function hasAnyTransparent(slots: Uint32Array): boolean {
530
+ for (let i = 0; i < MAX_BATCH_SLOTS; i++) {
531
+ if (slotInstanceCount(slots, i) > 0) return true;
532
+ }
533
+ return false;
534
+ }
535
+
536
+ function ensureTransparentTextures(
537
+ device: GPUDevice,
538
+ width: number,
539
+ height: number,
540
+ state: TransparentState | null
541
+ ): TransparentState {
542
+ if (state && state.width === width && state.height === height) return state;
543
+
544
+ state?.accumTexture.destroy();
545
+ state?.revealTexture.destroy();
546
+
547
+ const accumTexture = device.createTexture({
548
+ label: "wboit-accum",
549
+ size: { width, height },
550
+ format: ACCUM_FORMAT,
551
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
552
+ });
553
+ const revealTexture = device.createTexture({
554
+ label: "wboit-reveal",
555
+ size: { width, height },
556
+ format: REVEAL_FORMAT,
557
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
558
+ });
559
+
560
+ return {
561
+ accumTexture,
562
+ accumView: accumTexture.createView(),
563
+ revealTexture,
564
+ revealView: revealTexture.createView(),
565
+ width,
566
+ height,
567
+ };
568
+ }
569
+
570
+ function renderSky(
571
+ encoder: GPUCommandEncoder,
572
+ gpu: ForwardGPU,
573
+ bindGroups: ForwardBindGroups,
574
+ colorView: GPUTextureView,
575
+ zView: GPUTextureView,
576
+ clearColor: { r: number; g: number; b: number }
577
+ ): void {
578
+ const skyPass = encoder.beginRenderPass({
579
+ colorAttachments: [
580
+ {
581
+ view: colorView,
582
+ clearValue: { r: clearColor.r, g: clearColor.g, b: clearColor.b, a: 1 },
583
+ loadOp: "clear",
584
+ storeOp: "store",
585
+ },
586
+ ],
587
+ depthStencilAttachment: {
588
+ view: zView,
589
+ depthClearValue: 1.0,
590
+ depthLoadOp: "clear",
591
+ depthStoreOp: "store",
592
+ },
593
+ });
594
+ skyPass.setPipeline(gpu.sky);
595
+ skyPass.setBindGroup(0, bindGroups.sky);
596
+ skyPass.draw(3);
597
+ skyPass.end();
598
+ }
599
+
600
+ function renderOpaque(
601
+ ctx: ExecutionContext,
602
+ gpu: ForwardGPU,
603
+ bindGroups: ForwardBindGroups,
604
+ config: ForwardConfig,
605
+ colorView: GPUTextureView,
606
+ eidView: GPUTextureView,
607
+ zView: GPUTextureView,
608
+ clearColor: { r: number; g: number; b: number },
609
+ hasSky: boolean,
610
+ useShadows: boolean
611
+ ): void {
612
+ const pass = ctx.encoder.beginRenderPass({
613
+ colorAttachments: [
614
+ {
615
+ view: colorView,
616
+ clearValue: { r: clearColor.r, g: clearColor.g, b: clearColor.b, a: 1 },
617
+ loadOp: hasSky ? "load" : "clear",
618
+ storeOp: "store",
619
+ },
620
+ {
621
+ view: eidView,
622
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
623
+ loadOp: "clear",
624
+ storeOp: "store",
625
+ },
626
+ ],
627
+ depthStencilAttachment: {
628
+ view: zView,
629
+ depthClearValue: 1.0,
630
+ depthLoadOp: hasSky ? "load" : "clear",
631
+ depthStoreOp: "store",
632
+ },
633
+ });
634
+
635
+ const activePipeline = useShadows ? gpu.opaqueShadowed.pipeline : gpu.opaque.pipeline;
636
+ const activeBindGroup = useShadows ? bindGroups.mainShadowed : bindGroups.main;
637
+
638
+ pass.setPipeline(activePipeline);
639
+ pass.setBindGroup(0, activeBindGroup);
640
+
641
+ if (useShadows) {
642
+ pass.setBindGroup(1, bindGroups.shadow);
643
+ }
644
+
645
+ drawSlots(pass, config.batching.opaque, config.batching.buffers, ctx.device);
646
+ pass.end();
647
+ }
648
+
649
+ function renderTransparent(
650
+ ctx: ExecutionContext,
651
+ gpu: ForwardGPU,
652
+ bindGroups: ForwardBindGroups,
653
+ config: ForwardConfig,
654
+ transparent: TransparentState,
655
+ zView: GPUTextureView,
656
+ useShadows: boolean
657
+ ): void {
658
+ const activePipeline = useShadows ? gpu.transparentShadowed : gpu.transparent;
659
+ const activeBindGroup = useShadows ? bindGroups.transparentShadowed : bindGroups.transparent;
660
+
661
+ const transparentPass = ctx.encoder.beginRenderPass({
662
+ colorAttachments: [
663
+ {
664
+ view: transparent.accumView,
665
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
666
+ loadOp: "clear",
667
+ storeOp: "store",
668
+ },
669
+ {
670
+ view: transparent.revealView,
671
+ clearValue: { r: 1, g: 1, b: 1, a: 1 },
672
+ loadOp: "clear",
673
+ storeOp: "store",
674
+ },
675
+ ],
676
+ depthStencilAttachment: {
677
+ view: zView,
678
+ depthReadOnly: true,
679
+ },
680
+ });
681
+
682
+ transparentPass.setPipeline(activePipeline);
683
+ transparentPass.setBindGroup(0, activeBindGroup);
684
+
685
+ if (useShadows) {
686
+ transparentPass.setBindGroup(1, bindGroups.transparentShadow);
687
+ }
688
+
689
+ drawSlots(transparentPass, config.batching.transparent, config.batching.buffers, ctx.device);
690
+ transparentPass.end();
691
+ }
692
+
693
+ function compositeTransparency(
694
+ encoder: GPUCommandEncoder,
695
+ gpu: ForwardGPU,
696
+ compositeBindGroup: GPUBindGroup,
697
+ colorView: GPUTextureView
698
+ ): void {
699
+ const compositePass = encoder.beginRenderPass({
700
+ colorAttachments: [
701
+ {
702
+ view: colorView,
703
+ loadOp: "load",
704
+ storeOp: "store",
705
+ },
706
+ ],
707
+ });
708
+
709
+ compositePass.setPipeline(gpu.composite);
710
+ compositePass.setBindGroup(0, compositeBindGroup);
711
+ compositePass.draw(3);
712
+ compositePass.end();
713
+ }
714
+
715
+ export interface ForwardConfig {
716
+ scene: GPUBuffer;
717
+ sky: GPUBuffer;
718
+ data: GPUBuffer;
719
+ matrices: GPUBuffer;
720
+ sizes: GPUBuffer;
721
+ batching: Batch;
722
+ shadowBuffer: GPUBuffer;
723
+ shadowAtlas: GPUTexture;
724
+ getSurfaces: () => SurfaceData[];
725
+ getClearColor: () => { r: number; g: number; b: number };
726
+ getSky: () => boolean;
727
+ getShadowsEnabled: () => boolean;
728
+ getRTRendered?: () => boolean;
729
+ }
730
+
731
+ export function createRasterForwardNode(config: ForwardConfig): ComputeNode {
732
+ let gpu: ForwardGPU | null = null;
733
+ let bindGroups: ForwardBindGroups | null = null;
734
+ let transparent: TransparentState | null = null;
735
+ let cachedWidth = 0;
736
+ let cachedHeight = 0;
737
+
738
+ return {
739
+ id: "forward",
740
+ inputs: [
741
+ { id: "batched", access: "read" },
742
+ { id: "shadow-atlas", access: "read" },
743
+ ],
744
+ outputs: [
745
+ { id: "color", access: "write" },
746
+ { id: "eid", access: "write" },
747
+ { id: "z", access: "write" },
748
+ ],
749
+
750
+ async prepare(device: GPUDevice) {
751
+ const surfaces = config.getSurfaces();
752
+ gpu = await prepareForwardGPU(device, surfaces);
753
+ bindGroups = createForwardBindGroups(device, gpu, config);
754
+ },
755
+
756
+ execute(ctx: ExecutionContext) {
757
+ if (config.getRTRendered?.()) return;
758
+ if (!gpu || !bindGroups) return;
759
+
760
+ const colorView = ctx.getTextureView("color");
761
+ const eidView = ctx.getTextureView("eid");
762
+ const zView = ctx.getTextureView("z");
763
+ if (!colorView || !eidView || !zView) return;
764
+
765
+ const colorTexture = ctx.getTexture("color");
766
+ if (!colorTexture) return;
767
+ const width = colorTexture.width;
768
+ const height = colorTexture.height;
769
+
770
+ const useShadows = config.getShadowsEnabled();
771
+ const clearColor = config.getClearColor();
772
+ const hasSky = config.getSky();
773
+
774
+ if (hasSky) {
775
+ renderSky(
776
+ ctx.encoder,
777
+ gpu,
778
+ bindGroups,
779
+ colorView as GPUTextureView,
780
+ zView as GPUTextureView,
781
+ clearColor
782
+ );
783
+ }
784
+
785
+ renderOpaque(
786
+ ctx,
787
+ gpu,
788
+ bindGroups,
789
+ config,
790
+ colorView as GPUTextureView,
791
+ eidView as GPUTextureView,
792
+ zView as GPUTextureView,
793
+ clearColor,
794
+ hasSky,
795
+ useShadows
796
+ );
797
+
798
+ const transparentSlots = config.batching.transparent;
799
+ if (!hasAnyTransparent(transparentSlots)) return;
800
+
801
+ if (width !== cachedWidth || height !== cachedHeight) {
802
+ transparent = ensureTransparentTextures(ctx.device, width, height, transparent);
803
+ bindGroups.composite = ctx.device.createBindGroup({
804
+ layout: gpu.composite.getBindGroupLayout(0),
805
+ entries: [
806
+ { binding: 0, resource: transparent.accumView },
807
+ { binding: 1, resource: transparent.revealView },
808
+ ],
809
+ });
810
+ cachedWidth = width;
811
+ cachedHeight = height;
812
+ }
813
+
814
+ renderTransparent(
815
+ ctx,
816
+ gpu,
817
+ bindGroups,
818
+ config,
819
+ transparent!,
820
+ zView as GPUTextureView,
821
+ useShadows
822
+ );
823
+
824
+ compositeTransparency(
825
+ ctx.encoder,
826
+ gpu,
827
+ bindGroups.composite!,
828
+ colorView as GPUTextureView
829
+ );
830
+ },
831
+ };
832
+ }