@aethervtt/shard-render 0.0.1

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 (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/assets.d.ts +104 -0
  4. package/dist/assets.js +213 -0
  5. package/dist/atmosphere-model.d.ts +96 -0
  6. package/dist/atmosphere-model.js +370 -0
  7. package/dist/atmosphere-nodes.d.ts +55 -0
  8. package/dist/atmosphere-nodes.js +780 -0
  9. package/dist/atmosphere-plugin.d.ts +5 -0
  10. package/dist/atmosphere-plugin.js +42 -0
  11. package/dist/atmosphere-shaders.d.ts +7 -0
  12. package/dist/atmosphere-shaders.js +595 -0
  13. package/dist/atmosphere-state.d.ts +72 -0
  14. package/dist/atmosphere-state.js +18 -0
  15. package/dist/atmosphere.d.ts +191 -0
  16. package/dist/atmosphere.js +950 -0
  17. package/dist/camera.d.ts +83 -0
  18. package/dist/camera.js +163 -0
  19. package/dist/clusters.d.ts +68 -0
  20. package/dist/clusters.js +288 -0
  21. package/dist/culling-shaders.d.ts +2 -0
  22. package/dist/culling-shaders.js +203 -0
  23. package/dist/culling.d.ts +84 -0
  24. package/dist/culling.js +498 -0
  25. package/dist/debug-shaders.d.ts +3 -0
  26. package/dist/debug-shaders.js +207 -0
  27. package/dist/debug-views.d.ts +60 -0
  28. package/dist/debug-views.js +197 -0
  29. package/dist/deferred-plugin.d.ts +5 -0
  30. package/dist/deferred-plugin.js +20 -0
  31. package/dist/deferred-shaders.d.ts +2 -0
  32. package/dist/deferred-shaders.js +151 -0
  33. package/dist/deferred.d.ts +22 -0
  34. package/dist/deferred.js +296 -0
  35. package/dist/deform.d.ts +80 -0
  36. package/dist/deform.js +213 -0
  37. package/dist/describe-plugin.d.ts +5 -0
  38. package/dist/describe-plugin.js +19 -0
  39. package/dist/display-nodes.d.ts +5 -0
  40. package/dist/display-nodes.js +189 -0
  41. package/dist/dynamic-resolution.d.ts +47 -0
  42. package/dist/dynamic-resolution.js +191 -0
  43. package/dist/environment-plugin.d.ts +6 -0
  44. package/dist/environment-plugin.js +44 -0
  45. package/dist/environment-shaders.d.ts +2 -0
  46. package/dist/environment-shaders.js +260 -0
  47. package/dist/environment-state.d.ts +71 -0
  48. package/dist/environment-state.js +144 -0
  49. package/dist/environment.d.ts +70 -0
  50. package/dist/environment.js +529 -0
  51. package/dist/forward.d.ts +123 -0
  52. package/dist/forward.js +1216 -0
  53. package/dist/fxaa.d.ts +2 -0
  54. package/dist/fxaa.js +65 -0
  55. package/dist/gizmo-font.d.ts +21 -0
  56. package/dist/gizmo-font.js +132 -0
  57. package/dist/gizmos-plugin.d.ts +5 -0
  58. package/dist/gizmos-plugin.js +34 -0
  59. package/dist/gizmos.d.ts +156 -0
  60. package/dist/gizmos.js +772 -0
  61. package/dist/gpu-assets.d.ts +114 -0
  62. package/dist/gpu-assets.js +494 -0
  63. package/dist/graph.d.ts +204 -0
  64. package/dist/graph.js +417 -0
  65. package/dist/index.d.ts +53 -0
  66. package/dist/index.js +540 -0
  67. package/dist/instances.d.ts +349 -0
  68. package/dist/instances.js +1335 -0
  69. package/dist/large-world.d.ts +13 -0
  70. package/dist/large-world.js +20 -0
  71. package/dist/lights.d.ts +160 -0
  72. package/dist/lights.js +468 -0
  73. package/dist/material-noise.d.ts +19 -0
  74. package/dist/material-noise.js +7 -0
  75. package/dist/material-pipelines.d.ts +46 -0
  76. package/dist/material-pipelines.js +166 -0
  77. package/dist/materials.d.ts +108 -0
  78. package/dist/materials.js +219 -0
  79. package/dist/noise.d.ts +76 -0
  80. package/dist/noise.js +286 -0
  81. package/dist/overlay-registry.d.ts +18 -0
  82. package/dist/overlay-registry.js +30 -0
  83. package/dist/overlays.d.ts +35 -0
  84. package/dist/overlays.js +344 -0
  85. package/dist/paths.d.ts +20 -0
  86. package/dist/paths.js +46 -0
  87. package/dist/picking-plugin.d.ts +2 -0
  88. package/dist/picking-plugin.js +20 -0
  89. package/dist/picking.d.ts +94 -0
  90. package/dist/picking.js +456 -0
  91. package/dist/pixel-perfect-plugin.d.ts +9 -0
  92. package/dist/pixel-perfect-plugin.js +135 -0
  93. package/dist/pixel-perfect.d.ts +27 -0
  94. package/dist/pixel-perfect.js +70 -0
  95. package/dist/plugin.d.ts +111 -0
  96. package/dist/plugin.js +182 -0
  97. package/dist/pool.d.ts +19 -0
  98. package/dist/pool.js +43 -0
  99. package/dist/post-common.d.ts +51 -0
  100. package/dist/post-common.js +148 -0
  101. package/dist/post-nodes.d.ts +39 -0
  102. package/dist/post-nodes.js +849 -0
  103. package/dist/post-plugin.d.ts +5 -0
  104. package/dist/post-plugin.js +26 -0
  105. package/dist/post-shaders.d.ts +8 -0
  106. package/dist/post-shaders.js +698 -0
  107. package/dist/post.d.ts +175 -0
  108. package/dist/post.js +453 -0
  109. package/dist/readback.d.ts +4 -0
  110. package/dist/readback.js +95 -0
  111. package/dist/render-scale.d.ts +53 -0
  112. package/dist/render-scale.js +63 -0
  113. package/dist/shaders.d.ts +122 -0
  114. package/dist/shaders.js +1168 -0
  115. package/dist/shadows.d.ts +119 -0
  116. package/dist/shadows.js +562 -0
  117. package/dist/skin-asset.d.ts +26 -0
  118. package/dist/skin-asset.js +25 -0
  119. package/dist/skinning-plugin.d.ts +5 -0
  120. package/dist/skinning-plugin.js +16 -0
  121. package/dist/skinning.d.ts +19 -0
  122. package/dist/skinning.js +343 -0
  123. package/dist/standard-fields.d.ts +67 -0
  124. package/dist/standard-fields.js +73 -0
  125. package/dist/standard.d.ts +7 -0
  126. package/dist/standard.js +37 -0
  127. package/dist/stats.d.ts +25 -0
  128. package/dist/stats.js +18 -0
  129. package/dist/target.d.ts +55 -0
  130. package/dist/target.js +70 -0
  131. package/dist/testing.d.ts +34 -0
  132. package/dist/testing.js +126 -0
  133. package/dist/timer.d.ts +28 -0
  134. package/dist/timer.js +136 -0
  135. package/dist/view.d.ts +117 -0
  136. package/dist/view.js +389 -0
  137. package/dist/visibility.d.ts +10 -0
  138. package/dist/visibility.js +84 -0
  139. package/package.json +49 -0
package/dist/gizmos.js ADDED
@@ -0,0 +1,772 @@
1
+ import { defineResource, defineSystem, mat4 } from "@aethervtt/shard-core";
2
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
3
+ import { Time } from "@aethervtt/shard-runtime";
4
+ import { ForwardStateResource } from "./forward.js";
5
+ import { LABEL_FONT, labelAtlas, labelWidth } from "./gizmo-font.js";
6
+ import { RenderPhase } from "./graph.js";
7
+ import { Gpu, Shaders } from "./plugin.js";
8
+ import { cameraOf } from "./view.js";
9
+ const GIZMO_LINE_FLOATS = 8;
10
+ const GLYPH_FLOATS = 12;
11
+ const SOLID = 4294967295;
12
+ const NO_DEPTH = 1;
13
+ const LINE_META = 12 << 8;
14
+ const LABEL_META = NO_DEPTH | 12 << 8;
15
+ const packScratch = new Uint8Array(4);
16
+ const packView = new Uint32Array(packScratch.buffer);
17
+ function packGizmoColor(c) {
18
+ packScratch[0] = Math.round(Math.min(1, Math.max(0, c[0] ?? 1)) * 255);
19
+ packScratch[1] = Math.round(Math.min(1, Math.max(0, c[1] ?? 1)) * 255);
20
+ packScratch[2] = Math.round(Math.min(1, Math.max(0, c[2] ?? 1)) * 255);
21
+ packScratch[3] = Math.round(Math.min(1, Math.max(0, c[3] ?? 1)) * 255);
22
+ return packView[0];
23
+ }
24
+ function unpackColor(v) {
25
+ packView[0] = v;
26
+ return [
27
+ packScratch[0] / 255,
28
+ packScratch[1] / 255,
29
+ packScratch[2] / 255,
30
+ packScratch[3] / 255
31
+ ];
32
+ }
33
+ class GizmoList {
34
+ lines = new Float32Array(GIZMO_LINE_FLOATS * 256);
35
+ lineBits = new Uint32Array(this.lines.buffer);
36
+ lineCount = 0;
37
+ /** Timed lists: when each line stops drawing (Time.elapsed). */
38
+ lineExpiry = new Float64Array(256);
39
+ labelPos = new Float32Array(3 * 16);
40
+ labelColor = new Uint32Array(16);
41
+ labelMeta = new Uint32Array(16);
42
+ labelText = [];
43
+ labelExpiry = new Float64Array(16);
44
+ labelCount = 0;
45
+ /** Appends a line; returns its index. */
46
+ line(ax, ay, az, bx, by, bz, color, meta) {
47
+ if (this.lineCount * GIZMO_LINE_FLOATS >= this.lines.length) {
48
+ const grown = new Float32Array(this.lines.length * 2);
49
+ grown.set(this.lines);
50
+ this.lines = grown;
51
+ this.lineBits = new Uint32Array(grown.buffer);
52
+ const e = new Float64Array(this.lineExpiry.length * 2);
53
+ e.set(this.lineExpiry);
54
+ this.lineExpiry = e;
55
+ }
56
+ const o = this.lineCount * GIZMO_LINE_FLOATS;
57
+ const f = this.lines;
58
+ f[o] = ax;
59
+ f[o + 1] = ay;
60
+ f[o + 2] = az;
61
+ f[o + 3] = bx;
62
+ f[o + 4] = by;
63
+ f[o + 5] = bz;
64
+ this.lineBits[o + 6] = color;
65
+ this.lineBits[o + 7] = meta;
66
+ return this.lineCount++;
67
+ }
68
+ label(x, y, z, text, color, meta, expiry) {
69
+ const n = this.labelCount;
70
+ if (n >= this.labelColor.length) {
71
+ const pos = new Float32Array(this.labelPos.length * 2);
72
+ pos.set(this.labelPos);
73
+ this.labelPos = pos;
74
+ const col = new Uint32Array(this.labelColor.length * 2);
75
+ col.set(this.labelColor);
76
+ this.labelColor = col;
77
+ const meta2 = new Uint32Array(this.labelMeta.length * 2);
78
+ meta2.set(this.labelMeta);
79
+ this.labelMeta = meta2;
80
+ const e = new Float64Array(this.labelExpiry.length * 2);
81
+ e.set(this.labelExpiry);
82
+ this.labelExpiry = e;
83
+ }
84
+ this.labelPos[n * 3] = x;
85
+ this.labelPos[n * 3 + 1] = y;
86
+ this.labelPos[n * 3 + 2] = z;
87
+ this.labelColor[n] = color;
88
+ this.labelMeta[n] = meta;
89
+ this.labelText[n] = text;
90
+ this.labelExpiry[n] = expiry;
91
+ this.labelCount++;
92
+ }
93
+ /** Drops entries whose expiry has passed, keeping the order of the rest. */
94
+ expire(now) {
95
+ let w = 0;
96
+ for (let r = 0; r < this.lineCount; r++) {
97
+ if (this.lineExpiry[r] <= now) continue;
98
+ if (w !== r) {
99
+ this.lines.copyWithin(
100
+ w * GIZMO_LINE_FLOATS,
101
+ r * GIZMO_LINE_FLOATS,
102
+ (r + 1) * GIZMO_LINE_FLOATS
103
+ );
104
+ this.lineExpiry[w] = this.lineExpiry[r];
105
+ }
106
+ w++;
107
+ }
108
+ this.lineCount = w;
109
+ w = 0;
110
+ for (let r = 0; r < this.labelCount; r++) {
111
+ if (this.labelExpiry[r] <= now) continue;
112
+ if (w !== r) {
113
+ this.labelPos.copyWithin(w * 3, r * 3, r * 3 + 3);
114
+ this.labelColor[w] = this.labelColor[r];
115
+ this.labelMeta[w] = this.labelMeta[r];
116
+ this.labelText[w] = this.labelText[r];
117
+ this.labelExpiry[w] = this.labelExpiry[r];
118
+ }
119
+ w++;
120
+ }
121
+ this.labelCount = w;
122
+ this.labelText.length = w;
123
+ }
124
+ clear() {
125
+ this.lineCount = 0;
126
+ this.labelCount = 0;
127
+ }
128
+ /** Adds an offset to every line end and label anchor (a floating-origin shift). */
129
+ shift(x, y, z) {
130
+ const f = this.lines;
131
+ for (let i = 0; i < this.lineCount; i++) {
132
+ const o = i * GIZMO_LINE_FLOATS;
133
+ f[o] = f[o] + x;
134
+ f[o + 1] = f[o + 1] + y;
135
+ f[o + 2] = f[o + 2] + z;
136
+ f[o + 3] = f[o + 3] + x;
137
+ f[o + 4] = f[o + 4] + y;
138
+ f[o + 5] = f[o + 5] + z;
139
+ }
140
+ const p = this.labelPos;
141
+ for (let i = 0; i < this.labelCount; i++) {
142
+ p[i * 3] = p[i * 3] + x;
143
+ p[i * 3 + 1] = p[i * 3 + 1] + y;
144
+ p[i * 3 + 2] = p[i * 3 + 2] + z;
145
+ }
146
+ }
147
+ }
148
+ const m4 = new Float32Array(16);
149
+ const rot = new Float32Array(9);
150
+ const corners = new Float32Array(24);
151
+ const BOX_EDGES = [0, 1, 1, 3, 3, 2, 2, 0, 4, 5, 5, 7, 7, 6, 6, 4, 0, 4, 1, 5, 2, 6, 3, 7];
152
+ const CIRCLE_SEGMENTS = 32;
153
+ class GizmoStore {
154
+ /** Drawn this frame; cleared at the start of the next. */
155
+ frame = new GizmoList();
156
+ /** Drawn with a duration; kept until they expire. */
157
+ timed = new GizmoList();
158
+ /**
159
+ * Built-in overlays (overlays.ts), redrawn by their system each render. Their own list, so a
160
+ * render without a full frame (a capture of a paused game) doesn't draw them twice.
161
+ */
162
+ overlay = new GizmoList();
163
+ /** Where frame-lifetime drawings go: `frame`, or `overlay` while overlays draw. */
164
+ current = this.frame;
165
+ /** Routes drawings to the overlay list until `endOverlays`, clearing it first. */
166
+ beginOverlays() {
167
+ this.overlay.clear();
168
+ this.current = this.overlay;
169
+ }
170
+ endOverlays() {
171
+ this.current = this.frame;
172
+ }
173
+ /** Time.elapsed at the start of this frame. */
174
+ now = 0;
175
+ /** Every list, in draw order. */
176
+ lists = [this.frame, this.overlay, this.timed];
177
+ // The last color packed, so a run of lines in one color packs it once.
178
+ lr = -1;
179
+ lg = -1;
180
+ lb = -1;
181
+ la = -1;
182
+ lp = 0;
183
+ pack(c) {
184
+ const r = c[0];
185
+ const g = c[1];
186
+ const b = c[2];
187
+ const a = c[3] ?? 1;
188
+ if (r !== this.lr || g !== this.lg || b !== this.lb || a !== this.la) {
189
+ this.lr = r;
190
+ this.lg = g;
191
+ this.lb = b;
192
+ this.la = a;
193
+ this.lp = packGizmoColor(c);
194
+ }
195
+ return this.lp;
196
+ }
197
+ meta(options, labels) {
198
+ if (options === void 0) return labels ? LABEL_META : LINE_META;
199
+ const depthTest = options.depthTest ?? !labels;
200
+ const width = options.width ?? 1.5;
201
+ return (depthTest ? 0 : NO_DEPTH) | Math.min(255, Math.round(width * 8)) << 8;
202
+ }
203
+ list(options) {
204
+ return options?.duration ? this.timed : this.current;
205
+ }
206
+ /** Timed lines from `first` on get their expiry. */
207
+ expire(options, list, first) {
208
+ if (list !== this.timed) return;
209
+ const until = this.now + options.duration;
210
+ for (let i = first; i < list.lineCount; i++) list.lineExpiry[i] = until;
211
+ }
212
+ /** A segment from a to b. Colors are linear RGBA from 0 to 1. */
213
+ line(a, b, color, options) {
214
+ if (options === void 0) {
215
+ this.current.line(a[0], a[1], a[2], b[0], b[1], b[2], this.pack(color), LINE_META);
216
+ return;
217
+ }
218
+ const list = this.list(options);
219
+ const i = list.line(
220
+ a[0],
221
+ a[1],
222
+ a[2],
223
+ b[0],
224
+ b[1],
225
+ b[2],
226
+ this.pack(color),
227
+ this.meta(options, false)
228
+ );
229
+ this.expire(options, list, i);
230
+ }
231
+ /** A line with an arrowhead at `to`. */
232
+ arrow(from, to, color, options) {
233
+ const list = this.list(options);
234
+ const c = this.pack(color);
235
+ const meta = this.meta(options, false);
236
+ const first = list.lineCount;
237
+ const tx = to[0];
238
+ const ty = to[1];
239
+ const tz = to[2];
240
+ const dx = tx - from[0];
241
+ const dy = ty - from[1];
242
+ const dz = tz - from[2];
243
+ list.line(from[0], from[1], from[2], tx, ty, tz, c, meta);
244
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
245
+ if (len < 1e-9) {
246
+ this.expire(options, list, first);
247
+ return;
248
+ }
249
+ const head = Math.min(len * 0.25, Math.max(len * 0.1, 0.1));
250
+ const fx = dx / len;
251
+ const fy = dy / len;
252
+ const fz = dz / len;
253
+ let ux = Math.abs(fy) < 0.9 ? 0 : 1;
254
+ let uy = Math.abs(fy) < 0.9 ? 1 : 0;
255
+ let uz = 0;
256
+ const rx = fy * uz - fz * uy;
257
+ const ry = fz * ux - fx * uz;
258
+ const rz = fx * uy - fy * ux;
259
+ const rl = Math.sqrt(rx * rx + ry * ry + rz * rz);
260
+ const sx = rx / rl;
261
+ const sy = ry / rl;
262
+ const sz = rz / rl;
263
+ ux = sy * fz - sz * fy;
264
+ uy = sz * fx - sx * fz;
265
+ uz = sx * fy - sy * fx;
266
+ const bx = tx - fx * head;
267
+ const by = ty - fy * head;
268
+ const bz = tz - fz * head;
269
+ const w = head * 0.4;
270
+ list.line(tx, ty, tz, bx + sx * w, by + sy * w, bz + sz * w, c, meta);
271
+ list.line(tx, ty, tz, bx - sx * w, by - sy * w, bz - sz * w, c, meta);
272
+ list.line(tx, ty, tz, bx + ux * w, by + uy * w, bz + uz * w, c, meta);
273
+ list.line(tx, ty, tz, bx - ux * w, by - uy * w, bz - uz * w, c, meta);
274
+ this.expire(options, list, first);
275
+ }
276
+ /** A box: center, full size, rotation (quaternion, default none). */
277
+ box(center, size, rotation, color, options) {
278
+ const qx = rotation?.[0] ?? 0;
279
+ const qy = rotation?.[1] ?? 0;
280
+ const qz = rotation?.[2] ?? 0;
281
+ const qw = rotation?.[3] ?? 1;
282
+ rot[0] = 1 - 2 * (qy * qy + qz * qz);
283
+ rot[1] = 2 * (qx * qy + qz * qw);
284
+ rot[2] = 2 * (qx * qz - qy * qw);
285
+ rot[3] = 2 * (qx * qy - qz * qw);
286
+ rot[4] = 1 - 2 * (qx * qx + qz * qz);
287
+ rot[5] = 2 * (qy * qz + qx * qw);
288
+ rot[6] = 2 * (qx * qz + qy * qw);
289
+ rot[7] = 2 * (qy * qz - qx * qw);
290
+ rot[8] = 1 - 2 * (qx * qx + qy * qy);
291
+ for (let k = 0; k < 8; k++) {
292
+ const x = (k & 1 ? 0.5 : -0.5) * size[0];
293
+ const y = (k & 2 ? 0.5 : -0.5) * size[1];
294
+ const z = (k & 4 ? 0.5 : -0.5) * size[2];
295
+ corners[k * 3] = center[0] + rot[0] * x + rot[3] * y + rot[6] * z;
296
+ corners[k * 3 + 1] = center[1] + rot[1] * x + rot[4] * y + rot[7] * z;
297
+ corners[k * 3 + 2] = center[2] + rot[2] * x + rot[5] * y + rot[8] * z;
298
+ }
299
+ this.edges(color, options);
300
+ }
301
+ /** A box from 8 corners in `corners` (bit 0: +x, bit 1: +y, bit 2: +z). */
302
+ edges(color, options) {
303
+ const list = this.list(options);
304
+ const c = this.pack(color);
305
+ const meta = this.meta(options, false);
306
+ const first = list.lineCount;
307
+ for (let i = 0; i < 24; i += 2) {
308
+ const a = BOX_EDGES[i] * 3;
309
+ const b = BOX_EDGES[i + 1] * 3;
310
+ list.line(
311
+ corners[a],
312
+ corners[a + 1],
313
+ corners[a + 2],
314
+ corners[b],
315
+ corners[b + 1],
316
+ corners[b + 2],
317
+ c,
318
+ meta
319
+ );
320
+ }
321
+ this.expire(options, list, first);
322
+ }
323
+ /**
324
+ * The edges of a box given as a local AABB (`[minX, minY, minZ, maxX, maxY, maxZ]`) through an
325
+ * affine transform (three rows of four, at `offset`): an oriented bounding box.
326
+ */
327
+ bounds(aabb, rows, offset, color, options) {
328
+ for (let k = 0; k < 8; k++) {
329
+ const x = k & 1 ? aabb[3] : aabb[0];
330
+ const y = k & 2 ? aabb[4] : aabb[1];
331
+ const z = k & 4 ? aabb[5] : aabb[2];
332
+ for (let r = 0; r < 3; r++) {
333
+ const o = offset + r * 4;
334
+ corners[k * 3 + r] = rows[o] * x + rows[o + 1] * y + rows[o + 2] * z + rows[o + 3];
335
+ }
336
+ }
337
+ this.edges(color, options);
338
+ }
339
+ /** Three great circles. */
340
+ sphere(center, radius, color, options) {
341
+ this.circle(center, radius, 0, color, options);
342
+ this.circle(center, radius, 1, color, options);
343
+ this.circle(center, radius, 2, color, options);
344
+ }
345
+ /** A circle around axis 0 (x), 1 (y), or 2 (z). */
346
+ circle(center, radius, axis, color, options) {
347
+ const list = this.list(options);
348
+ const c = this.pack(color);
349
+ const meta = this.meta(options, false);
350
+ const first = list.lineCount;
351
+ const cx = center[0];
352
+ const cy = center[1];
353
+ const cz = center[2];
354
+ let px = 0;
355
+ let py = 0;
356
+ let pz = 0;
357
+ for (let i = 0; i <= CIRCLE_SEGMENTS; i++) {
358
+ const t = i / CIRCLE_SEGMENTS * Math.PI * 2;
359
+ const u = Math.cos(t) * radius;
360
+ const v = Math.sin(t) * radius;
361
+ const x = cx + (axis === 0 ? 0 : u);
362
+ const y = cy + (axis === 0 ? u : axis === 1 ? 0 : v);
363
+ const z = cz + (axis === 2 ? 0 : v);
364
+ if (i > 0) list.line(px, py, pz, x, y, z, c, meta);
365
+ px = x;
366
+ py = y;
367
+ pz = z;
368
+ }
369
+ this.expire(options, list, first);
370
+ }
371
+ /**
372
+ * A view frustum from its view-projection (reversed-Z). An infinite far plane draws at 20×
373
+ * the near distance.
374
+ */
375
+ frustum(viewProj, color, options) {
376
+ if (!mat4.invert(m4, viewProj)) return;
377
+ for (let k = 0; k < 8; k++) {
378
+ const x = k & 1 ? 1 : -1;
379
+ const y = k & 2 ? 1 : -1;
380
+ let z = k & 4 ? 0 : 1;
381
+ let w = m4[3] * x + m4[7] * y + m4[11] * z + m4[15];
382
+ if (z === 0 && Math.abs(w) < 1e-6) {
383
+ z = 0.05;
384
+ w = m4[3] * x + m4[7] * y + m4[11] * z + m4[15];
385
+ }
386
+ for (let r = 0; r < 3; r++) {
387
+ corners[k * 3 + r] = (m4[r] * x + m4[4 + r] * y + m4[8 + r] * z + m4[12 + r]) / w;
388
+ }
389
+ }
390
+ this.edges(color, options);
391
+ }
392
+ /** A square grid of `cells` × `cells` cells, `spacing` apart, facing `normal`. */
393
+ grid(center, normal, cells, spacing, color, options) {
394
+ const list = this.list(options);
395
+ const c = this.pack(color);
396
+ const meta = this.meta(options, false);
397
+ const first = list.lineCount;
398
+ let nx = normal[0];
399
+ let ny = normal[1];
400
+ let nz = normal[2];
401
+ const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
402
+ nx /= nl;
403
+ ny /= nl;
404
+ nz /= nl;
405
+ const hx = Math.abs(ny) < 0.9 ? 0 : 1;
406
+ const hy = Math.abs(ny) < 0.9 ? 1 : 0;
407
+ let ux = hy * nz;
408
+ let uy = -hx * nz;
409
+ let uz = hx * ny - hy * nx;
410
+ const ul = Math.sqrt(ux * ux + uy * uy + uz * uz);
411
+ ux /= ul;
412
+ uy /= ul;
413
+ uz /= ul;
414
+ const vx = ny * uz - nz * uy;
415
+ const vy = nz * ux - nx * uz;
416
+ const vz = nx * uy - ny * ux;
417
+ const half = cells * spacing / 2;
418
+ const cx = center[0];
419
+ const cy = center[1];
420
+ const cz = center[2];
421
+ for (let i = 0; i <= cells; i++) {
422
+ const t = -half + i * spacing;
423
+ list.line(
424
+ cx + ux * t - vx * half,
425
+ cy + uy * t - vy * half,
426
+ cz + uz * t - vz * half,
427
+ cx + ux * t + vx * half,
428
+ cy + uy * t + vy * half,
429
+ cz + uz * t + vz * half,
430
+ c,
431
+ meta
432
+ );
433
+ list.line(
434
+ cx + vx * t - ux * half,
435
+ cy + vy * t - uy * half,
436
+ cz + vz * t - uz * half,
437
+ cx + vx * t + ux * half,
438
+ cy + vy * t + uy * half,
439
+ cz + vz * t + uz * half,
440
+ c,
441
+ meta
442
+ );
443
+ }
444
+ this.expire(options, list, first);
445
+ }
446
+ /** Text just above a world position, facing the screen at a fixed pixel size, on a dark backing. */
447
+ label(position, text, color, options) {
448
+ const until = options?.duration ? this.now + options.duration : Number.POSITIVE_INFINITY;
449
+ this.list(options).label(
450
+ position[0],
451
+ position[1],
452
+ position[2],
453
+ text,
454
+ this.pack(color),
455
+ this.meta(options, true),
456
+ until
457
+ );
458
+ }
459
+ /**
460
+ * The floating origin moved (spec 0040): retained (timed) drawings, and this frame's drawings made
461
+ * before the shift, move by `offset` so they stay put in the world. Overlays redraw each render.
462
+ */
463
+ shiftOrigin(x, y, z) {
464
+ this.timed.shift(x, y, z);
465
+ this.frame.shift(x, y, z);
466
+ }
467
+ /** Lines and labels this frame, including timed ones still showing. */
468
+ get lineCount() {
469
+ return this.frame.lineCount + this.overlay.lineCount + this.timed.lineCount;
470
+ }
471
+ get labelCount() {
472
+ return this.frame.labelCount + this.overlay.labelCount + this.timed.labelCount;
473
+ }
474
+ /** What's drawn, as data (`debug.gizmos`): up to `limit` lines, and every label. */
475
+ describe(limit = 1e3) {
476
+ const lines = [];
477
+ const labels = [];
478
+ for (const list of [this.frame, this.overlay, this.timed]) {
479
+ for (let i = 0; i < list.lineCount && lines.length < limit; i++) {
480
+ const o = i * GIZMO_LINE_FLOATS;
481
+ const f = list.lines;
482
+ const meta = list.lineBits[o + 7];
483
+ lines.push({
484
+ from: [f[o], f[o + 1], f[o + 2]],
485
+ to: [f[o + 3], f[o + 4], f[o + 5]],
486
+ color: unpackColor(list.lineBits[o + 6]),
487
+ depthTest: (meta & NO_DEPTH) === 0,
488
+ width: (meta >>> 8) / 8
489
+ });
490
+ }
491
+ for (let i = 0; i < list.labelCount; i++) {
492
+ labels.push({
493
+ position: [list.labelPos[i * 3], list.labelPos[i * 3 + 1], list.labelPos[i * 3 + 2]],
494
+ text: list.labelText[i],
495
+ color: unpackColor(list.labelColor[i]),
496
+ depthTest: (list.labelMeta[i] & NO_DEPTH) === 0
497
+ });
498
+ }
499
+ }
500
+ return { lineCount: this.lineCount, labelCount: this.labelCount, lines, labels };
501
+ }
502
+ }
503
+ const Gizmos = defineResource("render/Gizmos", {
504
+ description: "Immediate-mode debug drawing: lines, shapes, and labels for this frame or a duration.",
505
+ init: () => new GizmoStore()
506
+ });
507
+ const beginGizmos = defineSystem({
508
+ name: "render/gizmos-begin",
509
+ run: (_, world) => {
510
+ const g = world.resource(Gizmos);
511
+ g.now = world.resource(Time).elapsed;
512
+ g.frame.clear();
513
+ g.timed.expire(g.now);
514
+ }
515
+ });
516
+ const GizmoGpuResource = defineResource("render/GizmoGpu", {
517
+ description: "GPU buffers of the gizmo pass.",
518
+ init: () => ({ gpu: void 0 })
519
+ });
520
+ function gizmoGpu(world, gpu) {
521
+ const r = world.initResource(GizmoGpuResource);
522
+ if (r.gpu && r.gpu.generation === gpu.generation) return r.gpu;
523
+ const V = GPUShaderStage.VERTEX;
524
+ const F = GPUShaderStage.FRAGMENT;
525
+ const atlas = gpu.device.createTexture({
526
+ label: "gizmos/label-font",
527
+ size: [LABEL_FONT.width, LABEL_FONT.height],
528
+ format: "r8unorm",
529
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
530
+ });
531
+ gpu.device.queue.writeTexture(
532
+ { texture: atlas },
533
+ labelAtlas(),
534
+ { bytesPerRow: LABEL_FONT.width },
535
+ [LABEL_FONT.width, LABEL_FONT.height]
536
+ );
537
+ const glyphData = new Float32Array(GLYPH_FLOATS * 256);
538
+ r.gpu = {
539
+ generation: gpu.generation,
540
+ lines: new GpuBuffer(gpu, { label: "gizmos/lines", usage: GPUBufferUsage.STORAGE, size: 4096 }),
541
+ glyphs: new GpuBuffer(gpu, {
542
+ label: "gizmos/glyphs",
543
+ usage: GPUBufferUsage.STORAGE,
544
+ size: 4096
545
+ }),
546
+ atlas,
547
+ layouts: {
548
+ view: gpu.layouts.bindGroupLayout({
549
+ label: "gizmos/view",
550
+ entries: [{ binding: 0, visibility: V | F, buffer: { type: "uniform" } }]
551
+ }),
552
+ data: gpu.layouts.bindGroupLayout({
553
+ label: "gizmos/data",
554
+ entries: [
555
+ { binding: 0, visibility: V, buffer: { type: "read-only-storage" } },
556
+ { binding: 1, visibility: V, buffer: { type: "read-only-storage" } },
557
+ { binding: 2, visibility: F, texture: { sampleType: "depth" } },
558
+ { binding: 3, visibility: F, texture: { sampleType: "float" } }
559
+ ]
560
+ })
561
+ },
562
+ pipelines: /* @__PURE__ */ new Map(),
563
+ groups: /* @__PURE__ */ new Map(),
564
+ glyphData,
565
+ glyphBits: new Uint32Array(glyphData.buffer),
566
+ glyphCount: 0
567
+ };
568
+ return r.gpu;
569
+ }
570
+ function pushGlyph(g, x, y, z, ox, oy, w, h, glyph, color, meta) {
571
+ if ((g.glyphCount + 1) * GLYPH_FLOATS > g.glyphData.length) {
572
+ const grown = new Float32Array(g.glyphData.length * 2);
573
+ grown.set(g.glyphData);
574
+ g.glyphData = grown;
575
+ g.glyphBits = new Uint32Array(grown.buffer);
576
+ }
577
+ const o = g.glyphCount++ * GLYPH_FLOATS;
578
+ const f = g.glyphData;
579
+ f[o] = x;
580
+ f[o + 1] = y;
581
+ f[o + 2] = z;
582
+ f[o + 3] = ox;
583
+ f[o + 4] = oy;
584
+ f[o + 6] = w;
585
+ f[o + 7] = h;
586
+ g.glyphBits[o + 8] = glyph;
587
+ g.glyphBits[o + 9] = color;
588
+ g.glyphBits[o + 10] = meta;
589
+ }
590
+ const LABEL_BACKING = packGizmoColor([0, 0, 0, 0.65]);
591
+ const PAD_X = 3;
592
+ function layoutLabels(g, list) {
593
+ for (let i = 0; i < list.labelCount; i++) {
594
+ const text = list.labelText[i];
595
+ const x = list.labelPos[i * 3];
596
+ const y = list.labelPos[i * 3 + 1];
597
+ const z = list.labelPos[i * 3 + 2];
598
+ const meta = list.labelMeta[i];
599
+ const color = list.labelColor[i];
600
+ const width = Math.ceil(labelWidth(text));
601
+ const left = -Math.round(width / 2) - PAD_X;
602
+ const top = -LABEL_FONT.cellHeight - 4;
603
+ pushGlyph(
604
+ g,
605
+ x,
606
+ y,
607
+ z,
608
+ left,
609
+ top,
610
+ width + PAD_X * 2,
611
+ LABEL_FONT.cellHeight + 2,
612
+ SOLID,
613
+ LABEL_BACKING,
614
+ meta
615
+ );
616
+ let pen = left + PAD_X;
617
+ for (let k = 0; k < text.length; k++) {
618
+ const code = text.charCodeAt(k);
619
+ const index = code >= 32 && code < 127 ? code - 32 : 31;
620
+ if (index !== 0) {
621
+ pushGlyph(
622
+ g,
623
+ x,
624
+ y,
625
+ z,
626
+ Math.round(pen),
627
+ top + 1,
628
+ LABEL_FONT.cellWidth,
629
+ LABEL_FONT.cellHeight,
630
+ index,
631
+ color,
632
+ meta
633
+ );
634
+ }
635
+ pen += LABEL_FONT.advances[index];
636
+ }
637
+ }
638
+ }
639
+ const uploadGizmos = defineSystem({
640
+ name: "render/gizmos-upload",
641
+ run: (_, world) => {
642
+ const gpu = world.tryResource(Gpu);
643
+ if (!gpu) return;
644
+ const store = world.resource(Gizmos);
645
+ const g = gizmoGpu(world, gpu);
646
+ const bytes = GIZMO_LINE_FLOATS * 4;
647
+ g.lines.ensureCapacity(store.lineCount * bytes);
648
+ let offset = 0;
649
+ for (const list of store.lists) {
650
+ if (list.lineCount === 0) continue;
651
+ g.lines.write(list.lines, offset, 0, list.lineCount * GIZMO_LINE_FLOATS);
652
+ offset += list.lineCount * bytes;
653
+ }
654
+ g.glyphCount = 0;
655
+ for (const list of store.lists) layoutLabels(g, list);
656
+ if (g.glyphCount > 0) g.glyphs.write(g.glyphData, 0, 0, g.glyphCount * GLYPH_FLOATS);
657
+ }
658
+ });
659
+ const PREMULTIPLIED = {
660
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
661
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
662
+ };
663
+ function pipeline(ctx, g, kind) {
664
+ const cached = g.pipelines.get(kind);
665
+ if (cached) return cached;
666
+ const gpu = ctx.gpu;
667
+ const module = ctx.world.resource(Shaders).module(gpu, { root: `shard::gizmos::${kind}` });
668
+ if (!module) {
669
+ gpu.pipelines.skipped++;
670
+ return void 0;
671
+ }
672
+ const p = gpu.pipelines.render({
673
+ label: `gizmos/${kind}`,
674
+ layout: gpu.layouts.pipelineLayout({
675
+ label: "gizmos",
676
+ bindGroupLayouts: [g.layouts.view, g.layouts.data]
677
+ }),
678
+ vertex: { module, entryPoint: "vs" },
679
+ fragment: {
680
+ module,
681
+ entryPoint: "fs",
682
+ targets: [{ format: "rgba16float", blend: PREMULTIPLIED }]
683
+ },
684
+ primitive: { topology: "triangle-list" }
685
+ });
686
+ if (p) g.pipelines.set(kind, p);
687
+ return p;
688
+ }
689
+ const ids = /* @__PURE__ */ new WeakMap();
690
+ let nextId = 1;
691
+ function idOf(o) {
692
+ let id = ids.get(o);
693
+ if (id === void 0) {
694
+ id = nextId++;
695
+ ids.set(o, id);
696
+ }
697
+ return id;
698
+ }
699
+ function group(gpu, g, slot, key, layout, entries) {
700
+ let entry = g.groups.get(slot);
701
+ if (!entry || entry.key !== key) {
702
+ entry = { key, group: gpu.device.createBindGroup({ label: slot, layout, entries: entries() }) };
703
+ g.groups.set(slot, entry);
704
+ }
705
+ return entry.group;
706
+ }
707
+ function gizmoNode(world) {
708
+ const store = world.resource(Gizmos);
709
+ return {
710
+ kind: "render",
711
+ phase: RenderPhase.Resolve + 30,
712
+ enabled: (view) => cameraOf(view) !== void 0 && store.lineCount + store.labelCount > 0,
713
+ reads: ["depth"],
714
+ writes: ["hdr"],
715
+ color: [{ resource: "hdr" }],
716
+ run: (ctx) => {
717
+ const lines = store.lineCount;
718
+ const r = ctx.world.resource(GizmoGpuResource).gpu;
719
+ const pv = ctx.world.resource(ForwardStateResource).views.get(ctx.view.name);
720
+ if (!r || !pv || lines === 0 && r.glyphCount === 0) return;
721
+ const gpu = ctx.gpu;
722
+ const pass = ctx.renderPass;
723
+ const depth = ctx.texture("depth");
724
+ pass.setBindGroup(
725
+ 0,
726
+ group(gpu, r, `view/${ctx.view.name}`, `${idOf(pv.uniform.buffer)}`, r.layouts.view, () => [
727
+ { binding: 0, resource: { buffer: pv.uniform.buffer } }
728
+ ])
729
+ );
730
+ pass.setBindGroup(
731
+ 1,
732
+ group(
733
+ gpu,
734
+ r,
735
+ `data/${ctx.view.name}`,
736
+ `${idOf(r.lines.buffer)}/${idOf(r.glyphs.buffer)}/${idOf(depth)}/${idOf(r.atlas)}`,
737
+ r.layouts.data,
738
+ () => [
739
+ { binding: 0, resource: { buffer: r.lines.buffer } },
740
+ { binding: 1, resource: { buffer: r.glyphs.buffer } },
741
+ { binding: 2, resource: depth.createView() },
742
+ { binding: 3, resource: r.atlas.createView() }
743
+ ]
744
+ )
745
+ );
746
+ if (lines > 0) {
747
+ const p = pipeline(ctx, r, "lines");
748
+ if (p) {
749
+ pass.setPipeline(p);
750
+ pass.draw(6, lines);
751
+ }
752
+ }
753
+ if (r.glyphCount > 0) {
754
+ const p = pipeline(ctx, r, "labels");
755
+ if (p) {
756
+ pass.setPipeline(p);
757
+ pass.draw(6, r.glyphCount);
758
+ }
759
+ }
760
+ }
761
+ };
762
+ }
763
+ export {
764
+ GIZMO_LINE_FLOATS,
765
+ GizmoGpuResource,
766
+ GizmoStore,
767
+ Gizmos,
768
+ beginGizmos,
769
+ gizmoNode,
770
+ packGizmoColor,
771
+ uploadGizmos
772
+ };