@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
@@ -82,7 +82,10 @@ function parseHead(r: Reader, table: TableEntry): { unitsPerEm: number; indexToL
82
82
  return { unitsPerEm, indexToLocFormat };
83
83
  }
84
84
 
85
- function parseHhea(r: Reader, table: TableEntry): { ascender: number; descender: number; lineGap: number; numHMetrics: number } {
85
+ function parseHhea(
86
+ r: Reader,
87
+ table: TableEntry
88
+ ): { ascender: number; descender: number; lineGap: number; numHMetrics: number } {
86
89
  seek(r, table.offset + 4);
87
90
  const ascender = i16(r);
88
91
  const descender = i16(r);
@@ -92,7 +95,12 @@ function parseHhea(r: Reader, table: TableEntry): { ascender: number; descender:
92
95
  return { ascender, descender, lineGap, numHMetrics };
93
96
  }
94
97
 
95
- function parseHmtx(r: Reader, table: TableEntry, numHMetrics: number, numGlyphs: number): { advances: Uint16Array } {
98
+ function parseHmtx(
99
+ r: Reader,
100
+ table: TableEntry,
101
+ numHMetrics: number,
102
+ numGlyphs: number
103
+ ): { advances: Uint16Array } {
96
104
  const advances = new Uint16Array(numGlyphs);
97
105
  seek(r, table.offset);
98
106
 
@@ -114,7 +122,12 @@ function parseMaxp(r: Reader, table: TableEntry): number {
114
122
  return u16(r);
115
123
  }
116
124
 
117
- function parseLoca(r: Reader, table: TableEntry, numGlyphs: number, indexToLocFormat: number): Uint32Array {
125
+ function parseLoca(
126
+ r: Reader,
127
+ table: TableEntry,
128
+ numGlyphs: number,
129
+ indexToLocFormat: number
130
+ ): Uint32Array {
118
131
  const offsets = new Uint32Array(numGlyphs + 1);
119
132
  seek(r, table.offset);
120
133
 
@@ -210,7 +223,8 @@ function parseCmap(r: Reader, table: TableEntry): Map<number, number> {
210
223
  if (rangeOffset === 0) {
211
224
  glyphId = (c + delta) & 0xffff;
212
225
  } else {
213
- const glyphIdOffset = idRangeOffsetPos + i * 2 + rangeOffset + (c - start) * 2;
226
+ const glyphIdOffset =
227
+ idRangeOffsetPos + i * 2 + rangeOffset + (c - start) * 2;
214
228
  seek(r, glyphIdOffset);
215
229
  glyphId = u16(r);
216
230
  if (glyphId !== 0) {
@@ -291,7 +305,12 @@ const REPEAT = 8;
291
305
  const X_SAME = 16;
292
306
  const Y_SAME = 32;
293
307
 
294
- function parseGlyph(r: Reader, glyfOffset: number, loca: Uint32Array, glyphId: number): { path: string; bounds: [number, number, number, number] } | null {
308
+ function parseGlyph(
309
+ r: Reader,
310
+ glyfOffset: number,
311
+ loca: Uint32Array,
312
+ glyphId: number
313
+ ): { path: string; bounds: [number, number, number, number] } | null {
295
314
  const start = loca[glyphId];
296
315
  const end = loca[glyphId + 1];
297
316
  if (start === end) return null;
@@ -372,7 +391,11 @@ function parseGlyph(r: Reader, glyfOffset: number, loca: Uint32Array, glyphId: n
372
391
  while (firstOn < points.length && !points[firstOn].on) firstOn++;
373
392
 
374
393
  if (firstOn === points.length) {
375
- const mid = { x: (points[0].x + points[1].x) / 2, y: (points[0].y + points[1].y) / 2, on: true };
394
+ const mid = {
395
+ x: (points[0].x + points[1].x) / 2,
396
+ y: (points[0].y + points[1].y) / 2,
397
+ on: true,
398
+ };
376
399
  points.unshift(mid);
377
400
  firstOn = 0;
378
401
  }
@@ -412,17 +435,28 @@ function parseGlyph(r: Reader, glyfOffset: number, loca: Uint32Array, glyphId: n
412
435
  return { path, bounds: [xMin, yMin, xMax, yMax] };
413
436
  }
414
437
 
415
- function parseCompositeGlyph(r: Reader, glyfOffset: number, loca: Uint32Array): { path: string; bounds: [number, number, number, number] } | null {
438
+ function parseCompositeGlyph(
439
+ r: Reader,
440
+ glyfOffset: number,
441
+ loca: Uint32Array
442
+ ): { path: string; bounds: [number, number, number, number] } | null {
416
443
  let path = "";
417
- let xMin = Infinity, yMin = Infinity, xMax = -Infinity, yMax = -Infinity;
444
+ let xMin = Infinity,
445
+ yMin = Infinity,
446
+ xMax = -Infinity,
447
+ yMax = -Infinity;
418
448
  let hasMore = true;
419
449
 
420
450
  while (hasMore) {
421
451
  const flags = u16(r);
422
452
  const glyphIndex = u16(r);
423
453
 
424
- let dx = 0, dy = 0;
425
- let a = 1, b = 0, c = 0, d = 1;
454
+ let dx = 0,
455
+ dy = 0;
456
+ let a = 1,
457
+ b = 0,
458
+ c = 0,
459
+ d = 1;
426
460
 
427
461
  if (flags & 1) {
428
462
  dx = i16(r);
@@ -464,7 +498,15 @@ function parseCompositeGlyph(r: Reader, glyfOffset: number, loca: Uint32Array):
464
498
  return { path, bounds: [xMin, yMin, xMax, yMax] };
465
499
  }
466
500
 
467
- function transformPath(path: string, a: number, b: number, c: number, d: number, dx: number, dy: number): string {
501
+ function transformPath(
502
+ path: string,
503
+ a: number,
504
+ b: number,
505
+ c: number,
506
+ d: number,
507
+ dx: number,
508
+ dy: number
509
+ ): string {
468
510
  return path.replace(/(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g, (_, x, y) => {
469
511
  const nx = parseFloat(x) * a + parseFloat(y) * b + dx;
470
512
  const ny = parseFloat(x) * c + parseFloat(y) * d + dy;
@@ -485,7 +527,15 @@ export function parseFont(buffer: ArrayBuffer): Font {
485
527
  const glyfTable = tables.get("glyf");
486
528
  const kernTable = tables.get("kern");
487
529
 
488
- if (!headTable || !hheaTable || !hmtxTable || !maxpTable || !cmapTable || !locaTable || !glyfTable) {
530
+ if (
531
+ !headTable ||
532
+ !hheaTable ||
533
+ !hmtxTable ||
534
+ !maxpTable ||
535
+ !cmapTable ||
536
+ !locaTable ||
537
+ !glyfTable
538
+ ) {
489
539
  throw new Error("Missing required font tables");
490
540
  }
491
541
 
@@ -497,14 +547,19 @@ export function parseFont(buffer: ArrayBuffer): Font {
497
547
  const cmap = parseCmap(r, cmapTable);
498
548
  const kern = kernTable ? parseKern(r, kernTable) : new Map<number, number>();
499
549
 
500
- const glyphCache = new Map<number, { path: string; bounds: [number, number, number, number] } | null>();
550
+ const glyphCache = new Map<
551
+ number,
552
+ { path: string; bounds: [number, number, number, number] } | null
553
+ >();
501
554
  const glyfOffset = glyfTable.offset;
502
555
 
503
556
  function getGlyphId(char: string): number {
504
557
  return cmap.get(char.codePointAt(0) ?? 0) ?? 0;
505
558
  }
506
559
 
507
- function getGlyph(glyphId: number): { path: string; bounds: [number, number, number, number] } | null {
560
+ function getGlyph(
561
+ glyphId: number
562
+ ): { path: string; bounds: [number, number, number, number] } | null {
508
563
  if (glyphCache.has(glyphId)) return glyphCache.get(glyphId)!;
509
564
  const glyph = parseGlyph(r, glyfOffset, loca, glyphId);
510
565
  glyphCache.set(glyphId, glyph);
@@ -3,12 +3,10 @@ import { SDFGenerator } from "./sdf";
3
3
  import {
4
4
  MAX_ENTITIES,
5
5
  resource,
6
- registerPostLoadHook,
7
6
  createFieldProxy,
8
7
  type Plugin,
9
8
  type State,
10
9
  type System,
11
- type PostLoadContext,
12
10
  type FieldProxy,
13
11
  } from "../../core";
14
12
  import { setTraits } from "../../core/component";
@@ -21,8 +19,8 @@ import {
21
19
  type Draw,
22
20
  type SharedPassContext,
23
21
  } from "../../standard/render";
24
- import { DEPTH_FORMAT } from "../../standard/render/scene";
25
- import { SCENE_STRUCT_WGSL } from "../../standard/render/shaders";
22
+ import { Z_FORMAT } from "../../standard/render/scene";
23
+ import { SCENE_STRUCT_WGSL } from "../../standard/render/surface/structs";
26
24
  import { Transform } from "../../standard/transforms";
27
25
 
28
26
  const MAX_GLYPHS = 50000;
@@ -31,14 +29,16 @@ const SDF_SIZE = 96;
31
29
  const SDF_EXPONENT = 9;
32
30
  const fontUrls: string[] = [];
33
31
  const loadedFonts: (Font | null)[] = [];
32
+ const fontNames = new Map<string, number>();
34
33
 
35
34
  export const DEFAULT_FONT =
36
35
  "https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf";
37
36
 
38
- export function font(url: string): number {
37
+ export function font(url: string, name?: string): number {
39
38
  const id = fontUrls.length;
40
39
  fontUrls.push(url);
41
40
  loadedFonts.push(null);
41
+ if (name) fontNames.set(name, id);
42
42
  return id;
43
43
  }
44
44
 
@@ -46,9 +46,14 @@ export function getFont(id: number): Font | null {
46
46
  return loadedFonts[id] ?? null;
47
47
  }
48
48
 
49
+ export function getFontByName(name: string): number | undefined {
50
+ return fontNames.get(name);
51
+ }
52
+
49
53
  export function resetFonts(): void {
50
54
  fontUrls.length = 0;
51
55
  loadedFonts.length = 0;
56
+ fontNames.clear();
52
57
  }
53
58
 
54
59
  async function loadFonts(): Promise<void> {
@@ -141,35 +146,6 @@ export const Text = {
141
146
  colorB: createFieldProxy(data, 12, 10),
142
147
  };
143
148
 
144
- interface PendingText {
145
- readonly eid: number;
146
- readonly content: string;
147
- }
148
-
149
- let pendingTextContent: PendingText[] = [];
150
-
151
- function parseTextAttrs(attrs: Record<string, string>): Record<string, string> {
152
- if (attrs._value) {
153
- const parsed: Record<string, string> = {};
154
- for (const part of attrs._value.split(";")) {
155
- const colonIdx = part.indexOf(":");
156
- if (colonIdx === -1) continue;
157
- const key = part.slice(0, colonIdx).trim();
158
- const value = part.slice(colonIdx + 1).trim();
159
- if (key && value) parsed[key] = value;
160
- }
161
- return parsed;
162
- }
163
- return attrs;
164
- }
165
-
166
- function finalizePendingText(_state: State, _context: PostLoadContext): void {
167
- for (const pending of pendingTextContent) {
168
- Text.content[pending.eid] = pending.content;
169
- }
170
- pendingTextContent = [];
171
- }
172
-
173
149
  setTraits(Text, {
174
150
  defaults: () => ({
175
151
  font: 0,
@@ -180,36 +156,7 @@ setTraits(Text, {
180
156
  anchorY: 0,
181
157
  color: 0xffffff,
182
158
  }),
183
- adapter: (attrs: Record<string, string>, eid: number) => {
184
- const parsed = parseTextAttrs(attrs);
185
- const result: Record<string, number> = {};
186
-
187
- if (parsed.content) {
188
- pendingTextContent.push({ eid, content: parsed.content });
189
- }
190
-
191
- if (parsed.font) result.font = parseInt(parsed.font, 10);
192
- if (parsed["font-size"]) result.fontSize = parseFloat(parsed["font-size"]);
193
- if (parsed.fontSize) result.fontSize = parseFloat(parsed.fontSize);
194
- if (parsed.opacity) result.opacity = parseFloat(parsed.opacity);
195
- if (parsed.visible) result.visible = parseFloat(parsed.visible);
196
- if (parsed["anchor-x"]) result.anchorX = parseFloat(parsed["anchor-x"]);
197
- if (parsed.anchorX) result.anchorX = parseFloat(parsed.anchorX);
198
- if (parsed["anchor-y"]) result.anchorY = parseFloat(parsed["anchor-y"]);
199
- if (parsed.anchorY) result.anchorY = parseFloat(parsed.anchorY);
200
- if (parsed.color) {
201
- const colorStr = parsed.color;
202
- if (colorStr.startsWith("0x") || colorStr.startsWith("0X")) {
203
- result.color = parseInt(colorStr, 16);
204
- } else if (colorStr.startsWith("#")) {
205
- result.color = parseInt(colorStr.slice(1), 16);
206
- } else {
207
- result.color = parseInt(colorStr, 10);
208
- }
209
- }
210
-
211
- return result;
212
- },
159
+ parse: { font: getFontByName },
213
160
  });
214
161
 
215
162
  interface GlyphMetrics {
@@ -366,7 +313,7 @@ function layoutText(text: string, atlas: GlyphAtlas, fontSize: number): LayoutRe
366
313
  if (!metrics) continue;
367
314
 
368
315
  if (prevChar) {
369
- cursorX += atlas.font.kerning(prevChar, char) / atlas.font.unitsPerEm * scale;
316
+ cursorX += (atlas.font.kerning(prevChar, char) / atlas.font.unitsPerEm) * scale;
370
317
  }
371
318
 
372
319
  const glyphW = metrics.glyphWidth * scale;
@@ -579,7 +526,7 @@ function createTextPipeline(
579
526
  cullMode: "none",
580
527
  },
581
528
  depthStencil: {
582
- format: DEPTH_FORMAT,
529
+ format: Z_FORMAT,
583
530
  depthCompare: "less",
584
531
  depthWriteEnabled: false,
585
532
  },
@@ -669,6 +616,8 @@ interface PendingGlyph {
669
616
  a: number;
670
617
  }
671
618
 
619
+ const glyphsByFont: PendingGlyph[][] = [];
620
+
672
621
  const TextSystem: System = {
673
622
  group: "draw",
674
623
 
@@ -681,7 +630,8 @@ const TextSystem: System = {
681
630
  const { atlases, staging, ranges } = text;
682
631
  const stagingU32 = new Uint32Array(staging.buffer);
683
632
 
684
- const glyphsByFont: PendingGlyph[][] = atlases.map(() => []);
633
+ while (glyphsByFont.length < atlases.length) glyphsByFont.push([]);
634
+ for (let i = 0; i < atlases.length; i++) glyphsByFont[i].length = 0;
685
635
 
686
636
  for (const eid of state.query([Text, Transform])) {
687
637
  if (!Text.visible[eid]) continue;
@@ -785,8 +735,6 @@ export const TextPlugin: Plugin = {
785
735
  dependencies: [ComputePlugin, RenderPlugin],
786
736
 
787
737
  async initialize(state: State) {
788
- registerPostLoadHook(finalizePendingText);
789
-
790
738
  const compute = Compute.from(state);
791
739
  const render = Render.from(state);
792
740
  if (!compute || !render) return;
@@ -340,14 +340,25 @@ export class SDFGenerator {
340
340
  const segments = segmentPath(path, this.curveSubdivisions);
341
341
  if (segments.length === 0) return;
342
342
  if (segments.length > this.maxSegments) {
343
- console.warn(`Too many segments (${segments.length}), truncating to ${this.maxSegments}`);
343
+ console.warn(
344
+ `Too many segments (${segments.length}), truncating to ${this.maxSegments}`
345
+ );
344
346
  segments.length = this.maxSegments;
345
347
  }
346
348
 
347
349
  const [xMin, yMin, xMax, yMax] = bounds;
348
350
  const maxDist = Math.max(xMax - xMin, yMax - yMin) / 2;
349
351
 
350
- const uniformData = new Float32Array([xMin, yMin, xMax, yMax, maxDist, this.exponent, 0, 0]);
352
+ const uniformData = new Float32Array([
353
+ xMin,
354
+ yMin,
355
+ xMax,
356
+ yMax,
357
+ maxDist,
358
+ this.exponent,
359
+ 0,
360
+ 0,
361
+ ]);
351
362
  this.device.queue.writeBuffer(this.uniformBuffer, 0, uniformData);
352
363
 
353
364
  const segmentData = new Float32Array(segments.length * 4);
@@ -0,0 +1,119 @@
1
+ import type { Plugin, State, System } from "../../core";
2
+ import { setTraits } from "../../core/component";
3
+ import { Transform } from "../../standard/transforms";
4
+ import { Mesh, Volume, mesh, Surface, surface, RenderPlugin } from "../../standard/render";
5
+ import type { MeshData } from "../../standard/render";
6
+
7
+ const SEED1 = "vec2(127.1, 311.7)";
8
+ const SEED2 = "vec2(269.5, 183.3)";
9
+
10
+ const waterSurface = surface({
11
+ fragment: `
12
+ let p = (*surface).worldPos.xz * 3.0;
13
+ let t = scene.time;
14
+ let t1 = vec2(t * 0.6, t * 0.4);
15
+ let t2 = vec2(t * 0.5, t * 0.7);
16
+ let eps = 0.1;
17
+
18
+ let h = (value2d(p + t1, ${SEED1}) + value2d(p - t2, ${SEED2})) * 0.5;
19
+ let hx = (value2d(p + vec2(eps, 0.0) + t1, ${SEED1}) + value2d(p + vec2(eps, 0.0) - t2, ${SEED2})) * 0.5;
20
+ let hz = (value2d(p + vec2(0.0, eps) + t1, ${SEED1}) + value2d(p + vec2(0.0, eps) - t2, ${SEED2})) * 0.5;
21
+
22
+ let dx = (hx - h) / eps;
23
+ let dz = (hz - h) / eps;
24
+ (*surface).normal = normalize(vec3(-dx * 0.015, 1.0, -dz * 0.015));`,
25
+ });
26
+
27
+ function createSubdividedPlane(subdivisions: number = 32): MeshData {
28
+ const segments = subdivisions;
29
+ const vertexCount = (segments + 1) * (segments + 1);
30
+ const indexCount = segments * segments * 6;
31
+
32
+ const vertices = new Float32Array(vertexCount * 6);
33
+ const indices = new Uint16Array(indexCount);
34
+
35
+ let vi = 0;
36
+ for (let z = 0; z <= segments; z++) {
37
+ for (let x = 0; x <= segments; x++) {
38
+ const u = x / segments;
39
+ const v = z / segments;
40
+ vertices[vi++] = u - 0.5;
41
+ vertices[vi++] = 0;
42
+ vertices[vi++] = v - 0.5;
43
+ vertices[vi++] = 0;
44
+ vertices[vi++] = 1;
45
+ vertices[vi++] = 0;
46
+ }
47
+ }
48
+
49
+ let ii = 0;
50
+ for (let z = 0; z < segments; z++) {
51
+ for (let x = 0; x < segments; x++) {
52
+ const topLeft = z * (segments + 1) + x;
53
+ const topRight = topLeft + 1;
54
+ const bottomLeft = (z + 1) * (segments + 1) + x;
55
+ const bottomRight = bottomLeft + 1;
56
+
57
+ indices[ii++] = topLeft;
58
+ indices[ii++] = bottomLeft;
59
+ indices[ii++] = topRight;
60
+ indices[ii++] = topRight;
61
+ indices[ii++] = bottomLeft;
62
+ indices[ii++] = bottomRight;
63
+ }
64
+ }
65
+
66
+ return { vertices, indices, vertexCount, indexCount };
67
+ }
68
+
69
+ const waterMesh = mesh(createSubdividedPlane(64));
70
+
71
+ export const Water = {
72
+ color: [] as number[],
73
+ opacity: [] as number[],
74
+ roughness: [] as number[],
75
+ metallic: [] as number[],
76
+ ior: [] as number[],
77
+ level: [] as number[],
78
+ };
79
+ setTraits(Water, {
80
+ defaults: () => ({
81
+ color: 0x4090a0,
82
+ opacity: 0.5,
83
+ roughness: 0.05,
84
+ metallic: 0.8,
85
+ ior: 1.33,
86
+ level: 0,
87
+ }),
88
+ });
89
+
90
+ const WaterSystem: System = {
91
+ group: "simulation",
92
+
93
+ update(state: State) {
94
+ for (const eid of state.query([Water])) {
95
+ if (!state.hasComponent(eid, Transform)) state.addComponent(eid, Transform);
96
+ if (!state.hasComponent(eid, Mesh)) state.addComponent(eid, Mesh);
97
+ if (!state.hasComponent(eid, Surface)) state.addComponent(eid, Surface);
98
+
99
+ Transform.posY[eid] = Water.level[eid];
100
+ Mesh.shape[eid] = waterMesh;
101
+ Mesh.color[eid] = Water.color[eid];
102
+ Mesh.opacity[eid] = Water.opacity[eid];
103
+ Mesh.roughness[eid] = Water.roughness[eid];
104
+ Mesh.metallic[eid] = Water.metallic[eid];
105
+ Mesh.ior[eid] = Water.ior[eid];
106
+ Mesh.sizeX[eid] = 40;
107
+ Mesh.sizeY[eid] = 1;
108
+ Mesh.sizeZ[eid] = 40;
109
+ Mesh.volume[eid] = Volume.HalfSpace;
110
+ Surface.type[eid] = waterSurface;
111
+ }
112
+ },
113
+ };
114
+
115
+ export const WaterPlugin: Plugin = {
116
+ systems: [WaterSystem],
117
+ components: { Water },
118
+ dependencies: [RenderPlugin],
119
+ };
@@ -3,6 +3,7 @@ import { TransformsPlugin } from "./transforms";
3
3
  import { InputPlugin } from "./input";
4
4
  import { ComputePlugin, initCanvas } from "./compute";
5
5
  import { RenderPlugin } from "./render";
6
+ import { RasterPlugin } from "./raster";
6
7
  import { ActivityPlugin, spinnerDark } from "./activity";
7
8
  import { shallotDark } from "./loading";
8
9
 
@@ -12,6 +13,7 @@ export const DEFAULT_PLUGINS: readonly Plugin[] = [
12
13
  InputPlugin,
13
14
  ComputePlugin,
14
15
  RenderPlugin,
16
+ RasterPlugin,
15
17
  ];
16
18
 
17
19
  StateBuilder.defaultPlugins = DEFAULT_PLUGINS;
@@ -4,5 +4,7 @@ export * from "./input";
4
4
  export * from "./tween";
5
5
  export * from "./transforms";
6
6
  export * from "./render";
7
+ export * from "./raster";
8
+ export * from "./raytracing";
7
9
  export * from "./loading";
8
10
  export * from "./defaults";
@@ -0,0 +1,149 @@
1
+ import { MAX_ENTITIES } from "../../core";
2
+ import { MAX_SURFACES, MAX_BATCH_SLOTS, Mesh, getMesh, createMeshBuffers } from "../render/mesh";
3
+ import type { MeshBuffers } from "../render/mesh";
4
+
5
+ const INVALID_SHAPE = 0xffffffff;
6
+ const SLOT_STRIDE = 4;
7
+
8
+ export interface Batch {
9
+ entityIds: GPUBuffer;
10
+ buffers: Map<number, MeshBuffers>;
11
+ opaque: Uint32Array;
12
+ transparent: Uint32Array;
13
+ }
14
+
15
+ export function createBatch(device: GPUDevice): Batch {
16
+ return {
17
+ entityIds: device.createBuffer({
18
+ label: "raster-entity-ids",
19
+ size: MAX_ENTITIES * 4,
20
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
21
+ }),
22
+ buffers: new Map(),
23
+ opaque: new Uint32Array(MAX_BATCH_SLOTS * SLOT_STRIDE),
24
+ transparent: new Uint32Array(MAX_BATCH_SLOTS * SLOT_STRIDE),
25
+ };
26
+ }
27
+
28
+ export function slotInstanceCount(slots: Uint32Array, i: number): number {
29
+ return slots[i * SLOT_STRIDE + 1];
30
+ }
31
+
32
+ export function slotIndexCount(slots: Uint32Array, i: number): number {
33
+ return slots[i * SLOT_STRIDE];
34
+ }
35
+
36
+ export function slotFirstInstance(slots: Uint32Array, i: number): number {
37
+ return slots[i * SLOT_STRIDE + 2];
38
+ }
39
+
40
+ export function slotShapeId(slots: Uint32Array, i: number): number {
41
+ return slots[i * SLOT_STRIDE + 3];
42
+ }
43
+
44
+ const entityIdsData = new Uint32Array(MAX_ENTITIES);
45
+ const opaqueCounts = new Uint32Array(MAX_BATCH_SLOTS);
46
+ const transparentCounts = new Uint32Array(MAX_BATCH_SLOTS);
47
+ const opaqueOffsets = new Uint32Array(MAX_BATCH_SLOTS);
48
+ const transparentOffsets = new Uint32Array(MAX_BATCH_SLOTS);
49
+ const opaqueWriteOffsets = new Uint32Array(MAX_BATCH_SLOTS);
50
+ const transparentWriteOffsets = new Uint32Array(MAX_BATCH_SLOTS);
51
+
52
+ const opaqueEids = new Uint32Array(MAX_ENTITIES);
53
+ const opaqueBatchIds = new Uint32Array(MAX_ENTITIES);
54
+ const transparentEids = new Uint32Array(MAX_ENTITIES);
55
+ const transparentBatchIds = new Uint32Array(MAX_ENTITIES);
56
+
57
+ export function uploadBatch(
58
+ device: GPUDevice,
59
+ entities: Iterable<number>,
60
+ getSurface: (eid: number) => number,
61
+ getOpacity: (eid: number) => number,
62
+ state: Batch
63
+ ): void {
64
+ opaqueCounts.fill(0);
65
+ transparentCounts.fill(0);
66
+ let opaqueLen = 0;
67
+ let transparentLen = 0;
68
+
69
+ for (const eid of entities) {
70
+ const shape = Mesh.shape[eid];
71
+ if (shape === INVALID_SHAPE) continue;
72
+
73
+ const surface = getSurface(eid);
74
+ const batchIndex = shape * MAX_SURFACES + surface;
75
+ if (batchIndex >= MAX_BATCH_SLOTS) continue;
76
+
77
+ if (getOpacity(eid) < 1.0) {
78
+ transparentEids[transparentLen] = eid;
79
+ transparentBatchIds[transparentLen] = batchIndex;
80
+ transparentLen++;
81
+ transparentCounts[batchIndex]++;
82
+ } else {
83
+ opaqueEids[opaqueLen] = eid;
84
+ opaqueBatchIds[opaqueLen] = batchIndex;
85
+ opaqueLen++;
86
+ opaqueCounts[batchIndex]++;
87
+ }
88
+ }
89
+
90
+ let offset = 0;
91
+ for (let i = 0; i < MAX_BATCH_SLOTS; i++) {
92
+ opaqueOffsets[i] = offset;
93
+ offset += opaqueCounts[i];
94
+ }
95
+ for (let i = 0; i < MAX_BATCH_SLOTS; i++) {
96
+ transparentOffsets[i] = offset;
97
+ offset += transparentCounts[i];
98
+ }
99
+
100
+ opaqueWriteOffsets.fill(0);
101
+ for (let i = 0; i < opaqueLen; i++) {
102
+ const batchIndex = opaqueBatchIds[i];
103
+ const idx = opaqueOffsets[batchIndex] + opaqueWriteOffsets[batchIndex];
104
+ entityIdsData[idx] = opaqueEids[i];
105
+ opaqueWriteOffsets[batchIndex]++;
106
+ }
107
+
108
+ transparentWriteOffsets.fill(0);
109
+ for (let i = 0; i < transparentLen; i++) {
110
+ const batchIndex = transparentBatchIds[i];
111
+ const idx = transparentOffsets[batchIndex] + transparentWriteOffsets[batchIndex];
112
+ entityIdsData[idx] = transparentEids[i];
113
+ transparentWriteOffsets[batchIndex]++;
114
+ }
115
+
116
+ const totalEntities = offset;
117
+ if (totalEntities > 0) {
118
+ device.queue.writeBuffer(state.entityIds, 0, entityIdsData, 0, totalEntities);
119
+ }
120
+
121
+ for (let batchIndex = 0; batchIndex < MAX_BATCH_SLOTS; batchIndex++) {
122
+ const shapeId = Math.floor(batchIndex / MAX_SURFACES);
123
+ const o = batchIndex * SLOT_STRIDE;
124
+
125
+ const opaqueCount = opaqueCounts[batchIndex];
126
+ const transparentCount = transparentCounts[batchIndex];
127
+
128
+ let indexCount = 0;
129
+ if (opaqueCount > 0 || transparentCount > 0) {
130
+ const mesh = getMesh(shapeId);
131
+ if (mesh) {
132
+ indexCount = mesh.indexCount;
133
+ if (!state.buffers.has(shapeId)) {
134
+ state.buffers.set(shapeId, createMeshBuffers(device, mesh));
135
+ }
136
+ }
137
+ }
138
+
139
+ state.opaque[o] = opaqueCount > 0 ? indexCount : 0;
140
+ state.opaque[o + 1] = opaqueCount;
141
+ state.opaque[o + 2] = opaqueOffsets[batchIndex];
142
+ state.opaque[o + 3] = shapeId;
143
+
144
+ state.transparent[o] = transparentCount > 0 ? indexCount : 0;
145
+ state.transparent[o + 1] = transparentCount;
146
+ state.transparent[o + 2] = transparentOffsets[batchIndex];
147
+ state.transparent[o + 3] = shapeId;
148
+ }
149
+ }