@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.
Files changed (41) 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/orbit/index.ts +1 -1
  8. package/src/extras/text/index.ts +10 -65
  9. package/src/extras/{water.ts → water/index.ts} +59 -4
  10. package/src/standard/raster/batch.ts +149 -0
  11. package/src/standard/raster/forward.ts +832 -0
  12. package/src/standard/raster/index.ts +146 -472
  13. package/src/standard/raster/shadow.ts +408 -0
  14. package/src/standard/raytracing/bvh/blas.ts +335 -87
  15. package/src/standard/raytracing/bvh/radix.ts +225 -228
  16. package/src/standard/raytracing/bvh/refit.ts +711 -0
  17. package/src/standard/raytracing/bvh/structs.ts +0 -55
  18. package/src/standard/raytracing/bvh/tlas.ts +153 -141
  19. package/src/standard/raytracing/bvh/traverse.ts +72 -64
  20. package/src/standard/raytracing/index.ts +233 -204
  21. package/src/standard/raytracing/instance.ts +30 -18
  22. package/src/standard/raytracing/ray.ts +1 -1
  23. package/src/standard/raytracing/shaders.ts +23 -40
  24. package/src/standard/render/camera.ts +10 -28
  25. package/src/standard/render/data.ts +1 -1
  26. package/src/standard/render/index.ts +68 -12
  27. package/src/standard/render/light.ts +2 -2
  28. package/src/standard/render/mesh.ts +404 -0
  29. package/src/standard/render/overlay.ts +5 -2
  30. package/src/standard/render/postprocess.ts +263 -267
  31. package/src/standard/render/surface/index.ts +81 -12
  32. package/src/standard/render/surface/shaders.ts +265 -11
  33. package/src/standard/render/surface/structs.ts +10 -0
  34. package/src/standard/tween/tween.ts +44 -115
  35. package/src/standard/render/mesh/box.ts +0 -20
  36. package/src/standard/render/mesh/index.ts +0 -315
  37. package/src/standard/render/mesh/plane.ts +0 -11
  38. package/src/standard/render/mesh/sphere.ts +0 -40
  39. package/src/standard/render/mesh/unified.ts +0 -96
  40. package/src/standard/render/surface/compile.ts +0 -65
  41. 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
+ }