@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
@@ -0,0 +1,344 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ findComponent
5
+ } from "@aethervtt/shard-core";
6
+ import { GlobalTransform, GridFramesResource } from "@aethervtt/shard-transform";
7
+ import { ForwardStateResource } from "./forward.js";
8
+ import { Gizmos } from "./gizmos.js";
9
+ import {
10
+ INSTANCE_FLOATS,
11
+ InstanceFlags,
12
+ InstanceSlot,
13
+ Instances
14
+ } from "./instances.js";
15
+ import { DirectionalLight, Lights } from "./lights.js";
16
+ import { defineOverlay, OVERLAYS, registeredOverlays } from "./overlay-registry.js";
17
+ const extraOverlays = registeredOverlays();
18
+ import { Views } from "./plugin.js";
19
+ import { Cameras, cameraOf } from "./view.js";
20
+ import {
21
+ allOverlays,
22
+ defineOverlay as defineOverlay2,
23
+ OVERLAYS as OVERLAYS2,
24
+ overlayNames
25
+ } from "./overlay-registry.js";
26
+ function isOverlayOn(o, name) {
27
+ return OVERLAYS.includes(name) ? o[name] : o.extra[name] === true;
28
+ }
29
+ let memberDef;
30
+ function scenePath(world, entity) {
31
+ if (!memberDef) memberDef = findComponent("scene/SceneMember") ?? null;
32
+ if (!memberDef) return void 0;
33
+ const v = world.tryGet(entity, memberDef);
34
+ return v?.path || void 0;
35
+ }
36
+ const DebugOverlays = defineResource("render/DebugOverlays", {
37
+ description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, and entity labels.",
38
+ init: () => ({
39
+ bounds: false,
40
+ lights: false,
41
+ cameras: false,
42
+ cascades: false,
43
+ normals: false,
44
+ axes: false,
45
+ labels: false,
46
+ extra: {},
47
+ filter: { components: [], path: "" },
48
+ name: scenePath
49
+ })
50
+ });
51
+ function setOverlays(world, on, filter) {
52
+ const o = world.resource(DebugOverlays);
53
+ for (const name of OVERLAYS) if (on[name] !== void 0) o[name] = on[name];
54
+ for (const name of extraOverlays.keys()) if (on[name] !== void 0) o.extra[name] = on[name];
55
+ if (filter) {
56
+ if (filter.components) o.filter.components = filter.components;
57
+ if (filter.path !== void 0) o.filter.path = filter.path;
58
+ }
59
+ return o;
60
+ }
61
+ const BOUNDS = [0.2, 0.9, 1, 1];
62
+ const LIGHT = [1, 0.85, 0.3, 1];
63
+ const CAMERA = [0.9, 0.9, 0.9, 1];
64
+ const CASCADE = [
65
+ [1, 0.3, 0.3, 1],
66
+ [0.3, 1, 0.3, 1],
67
+ [0.3, 0.5, 1, 1],
68
+ [1, 1, 0.3, 1]
69
+ ];
70
+ const NORMAL = [0.4, 0.6, 1, 1];
71
+ const AXES = [
72
+ [1, 0.2, 0.2, 1],
73
+ [0.2, 1, 0.2, 1],
74
+ [0.3, 0.5, 1, 1]
75
+ ];
76
+ const LABEL = [1, 1, 1, 1];
77
+ const AXES_WIDTH = { width: 2 };
78
+ const NORMALS_PER_MESH = 1500;
79
+ const a = new Float32Array(3);
80
+ const b = new Float32Array(3);
81
+ function passes(world, o, entity, defs) {
82
+ for (const def of defs) if (!world.has(entity, def)) return false;
83
+ if (o.filter.path) {
84
+ const name = o.name(world, entity);
85
+ if (!name?.startsWith(o.filter.path)) return false;
86
+ }
87
+ return true;
88
+ }
89
+ function slotMesh(store, slot) {
90
+ const batch = store.batchOf[slot];
91
+ if (batch >= 0) return store.batches[batch].mesh;
92
+ if (batch <= -2) {
93
+ const set = store.lodSets[-2 - batch];
94
+ return store.batches[set.batches[0]]?.mesh;
95
+ }
96
+ return void 0;
97
+ }
98
+ function drawInstances(world, o, g, defs) {
99
+ const store = world.tryResource(Instances);
100
+ if (!store) return;
101
+ const f = store.f32;
102
+ const u = store.u32;
103
+ for (let slot = 0; slot < store.high; slot++) {
104
+ if ((store.flags[slot] & InstanceFlags.Visible) === 0) continue;
105
+ const mesh = slotMesh(store, slot);
106
+ if (!mesh) continue;
107
+ const entity = u[slot * INSTANCE_FLOATS + 15];
108
+ if (!passes(world, o, entity, defs)) continue;
109
+ const off = slot * INSTANCE_FLOATS;
110
+ const bb = mesh.bounds;
111
+ if (o.bounds) g.bounds(bb, f, off, BOUNDS);
112
+ if (o.labels) {
113
+ const cx = (bb[0] + bb[3]) / 2;
114
+ const cy = (bb[1] + bb[4]) / 2;
115
+ const cz = (bb[2] + bb[5]) / 2;
116
+ const hy = (Math.abs(f[off + 4]) * (bb[3] - bb[0]) + Math.abs(f[off + 5]) * (bb[4] - bb[1]) + Math.abs(f[off + 6]) * (bb[5] - bb[2])) / 2;
117
+ a[0] = f[off] * cx + f[off + 1] * cy + f[off + 2] * cz + f[off + 3];
118
+ a[1] = f[off + 4] * cx + f[off + 5] * cy + f[off + 6] * cz + f[off + 7] + hy;
119
+ a[2] = f[off + 8] * cx + f[off + 9] * cy + f[off + 10] * cz + f[off + 11];
120
+ g.label(a, o.name(world, entity) ?? `#${entity}`, LABEL);
121
+ }
122
+ if (o.normals && mesh.normals && mesh.normals.length === mesh.positions.length) {
123
+ const n = mesh.positions.length / 3;
124
+ const step = Math.max(1, Math.ceil(n / NORMALS_PER_MESH));
125
+ const p = mesh.positions;
126
+ const nn = mesh.normals;
127
+ const len = Math.max(bb[3] - bb[0], bb[4] - bb[1], bb[5] - bb[2]) * 0.08;
128
+ for (let v = 0; v < n; v += step) {
129
+ const x = p[v * 3];
130
+ const y = p[v * 3 + 1];
131
+ const z = p[v * 3 + 2];
132
+ for (let r = 0; r < 3; r++) {
133
+ const ro = off + r * 4;
134
+ a[r] = f[ro] * x + f[ro + 1] * y + f[ro + 2] * z + f[ro + 3];
135
+ b[r] = f[ro] * nn[v * 3] + f[ro + 1] * nn[v * 3 + 1] + f[ro + 2] * nn[v * 3 + 2];
136
+ }
137
+ const bl = Math.sqrt(b[0] * b[0] + b[1] * b[1] + b[2] * b[2]) || 1;
138
+ for (let r = 0; r < 3; r++) b[r] = a[r] + b[r] / bl * len;
139
+ g.line(a, b, NORMAL);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ function drawLights(world, o, g, defs) {
145
+ const lights = world.tryResource(Lights);
146
+ if (lights) {
147
+ for (const r of lights.records) {
148
+ if (!r.alive || !passes(world, o, r.entity, defs)) continue;
149
+ a[0] = r.x;
150
+ a[1] = r.y;
151
+ a[2] = r.z;
152
+ if (r.kind === 0) {
153
+ g.sphere(a, r.range, LIGHT);
154
+ continue;
155
+ }
156
+ const rim = Math.tan(r.outerAngle * Math.PI / 180) * r.range;
157
+ const ex = r.x + r.dx * r.range;
158
+ const ey = r.y + r.dy * r.range;
159
+ const ez = r.z + r.dz * r.range;
160
+ const hx = Math.abs(r.dy) < 0.9 ? 0 : 1;
161
+ const hy = Math.abs(r.dy) < 0.9 ? 1 : 0;
162
+ let ux = hy * r.dz;
163
+ let uy = -hx * r.dz;
164
+ let uz = hx * r.dy - hy * r.dx;
165
+ const ul = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
166
+ ux /= ul;
167
+ uy /= ul;
168
+ uz /= ul;
169
+ const vx = r.dy * uz - r.dz * uy;
170
+ const vy = r.dz * ux - r.dx * uz;
171
+ const vz = r.dx * uy - r.dy * ux;
172
+ const SEG = 24;
173
+ for (let i = 0; i < SEG; i++) {
174
+ const t0 = i / SEG * Math.PI * 2;
175
+ const t1 = (i + 1) / SEG * Math.PI * 2;
176
+ const c0 = Math.cos(t0) * rim;
177
+ const s0 = Math.sin(t0) * rim;
178
+ const c1 = Math.cos(t1) * rim;
179
+ const s1 = Math.sin(t1) * rim;
180
+ b[0] = ex + ux * c0 + vx * s0;
181
+ b[1] = ey + uy * c0 + vy * s0;
182
+ b[2] = ez + uz * c0 + vz * s0;
183
+ if (i % 6 === 0) g.line(a, b, LIGHT);
184
+ a[0] = ex + ux * c1 + vx * s1;
185
+ a[1] = ey + uy * c1 + vy * s1;
186
+ a[2] = ez + uz * c1 + vz * s1;
187
+ g.line(b, a, LIGHT);
188
+ a[0] = r.x;
189
+ a[1] = r.y;
190
+ a[2] = r.z;
191
+ }
192
+ }
193
+ }
194
+ for (const table of world.query({ with: [DirectionalLight, GlobalTransform] }).tables) {
195
+ const m = table.column(GlobalTransform, "matrix");
196
+ for (let i = 0; i < table.count; i++) {
197
+ if (!passes(world, o, table.entities[i], defs)) continue;
198
+ const off = i * 12;
199
+ a[0] = m[off + 3];
200
+ a[1] = m[off + 7];
201
+ a[2] = m[off + 11];
202
+ b[0] = a[0] - m[off + 2] * 2;
203
+ b[1] = a[1] - m[off + 6] * 2;
204
+ b[2] = a[2] - m[off + 10] * 2;
205
+ g.arrow(a, b, LIGHT);
206
+ }
207
+ }
208
+ }
209
+ function primaryCamera(world) {
210
+ let best;
211
+ let order = Number.POSITIVE_INFINITY;
212
+ for (const view of world.resource(Views).list) {
213
+ const cam = cameraOf(view);
214
+ if (cam && view.order < order) {
215
+ order = view.order;
216
+ best = cam.entity;
217
+ }
218
+ }
219
+ return best;
220
+ }
221
+ const drawOverlays = defineSystem({
222
+ name: "render/debug-overlays",
223
+ run: (_, world) => {
224
+ const o = world.resource(DebugOverlays);
225
+ const g = world.resource(Gizmos);
226
+ g.overlay.clear();
227
+ let extra = false;
228
+ for (const name in o.extra) if (o.extra[name] && extraOverlays.has(name)) extra = true;
229
+ if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || extra))
230
+ return;
231
+ g.beginOverlays();
232
+ try {
233
+ draw(world, o, g);
234
+ } finally {
235
+ g.endOverlays();
236
+ }
237
+ }
238
+ });
239
+ function draw(world, o, g) {
240
+ {
241
+ const defs = [];
242
+ for (const name of o.filter.components) {
243
+ const def = findComponent(name);
244
+ if (def) defs.push(def);
245
+ }
246
+ if (o.bounds || o.labels || o.normals) drawInstances(world, o, g, defs);
247
+ for (const [name, def] of extraOverlays) {
248
+ if (o.extra[name]) def.draw(world, g, (entity) => passes(world, o, entity, defs));
249
+ }
250
+ if (o.lights) drawLights(world, o, g, defs);
251
+ const primary = primaryCamera(world);
252
+ if (o.cameras) {
253
+ for (const [entity, cam] of world.resource(Cameras)) {
254
+ if (entity === primary || !passes(world, o, entity, defs)) continue;
255
+ g.frustum(cam.viewProjNoJitter, CAMERA);
256
+ }
257
+ }
258
+ if (o.cascades && primary !== void 0) {
259
+ const pv = world.tryResource(ForwardStateResource)?.views.get(`camera:${primary}`);
260
+ if (pv) {
261
+ for (let i = 0; i < pv.cascades.count; i++) {
262
+ g.frustum(pv.cascades.views[i].viewProj, CASCADE[i % CASCADE.length]);
263
+ }
264
+ }
265
+ }
266
+ if (o.axes || o.labels) {
267
+ for (const table of world.query({ with: [GlobalTransform] }).tables) {
268
+ const m = table.column(GlobalTransform, "matrix");
269
+ const meshed = table.has(InstanceSlot);
270
+ for (let i = 0; i < table.count; i++) {
271
+ const entity = table.entities[i];
272
+ if (!passes(world, o, entity, defs)) continue;
273
+ const off = i * 12;
274
+ a[0] = m[off + 3];
275
+ a[1] = m[off + 7];
276
+ a[2] = m[off + 11];
277
+ if (o.axes) {
278
+ for (let k = 0; k < 3; k++) {
279
+ b[0] = a[0] + m[off + k];
280
+ b[1] = a[1] + m[off + 4 + k];
281
+ b[2] = a[2] + m[off + 8 + k];
282
+ g.line(a, b, AXES[k], AXES_WIDTH);
283
+ }
284
+ }
285
+ if (o.labels && !meshed) {
286
+ const name = o.name(world, entity);
287
+ if (name) g.label(a, name, LABEL);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+ const GRID_CELL = [0.3, 1, 0.5, 1];
295
+ const NEIGHBOR = [0.3, 0.6, 1, 0.45];
296
+ const ga = new Float32Array(3);
297
+ const gb = new Float32Array(3);
298
+ const GRID_LINE = { width: 1.5 };
299
+ const CELL_LINE = { width: 2.5 };
300
+ const gridsOverlay = defineOverlay({
301
+ name: "grids",
302
+ description: "Large worlds: the floating origin's cell (bright) and its neighbors' edges, in the origin grid.",
303
+ draw: (world, g) => {
304
+ const frames = world.tryResource(GridFramesResource);
305
+ if (!frames?.active || frames.originSlot === 0) return;
306
+ const cs = frames.cellSize[frames.originSlot];
307
+ if (!(cs > 0)) return;
308
+ const h = cs / 2;
309
+ for (let axis = 0; axis < 3; axis++) {
310
+ const u = (axis + 1) % 3;
311
+ const v = (axis + 2) % 3;
312
+ for (let i = 0; i < 4; i++) {
313
+ for (let j = 0; j < 4; j++) {
314
+ const pu = (i - 1.5) * cs;
315
+ const pv = (j - 1.5) * cs;
316
+ ga[axis] = -3 * h;
317
+ gb[axis] = 3 * h;
318
+ ga[u] = gb[u] = pu;
319
+ ga[v] = gb[v] = pv;
320
+ g.line(ga, gb, NEIGHBOR, GRID_LINE);
321
+ }
322
+ }
323
+ for (let k = 0; k < 4; k++) {
324
+ ga[axis] = -h;
325
+ gb[axis] = h;
326
+ ga[u] = gb[u] = k & 1 ? h : -h;
327
+ ga[v] = gb[v] = k & 2 ? h : -h;
328
+ g.line(ga, gb, GRID_CELL, CELL_LINE);
329
+ }
330
+ }
331
+ }
332
+ });
333
+ export {
334
+ DebugOverlays,
335
+ OVERLAYS2 as OVERLAYS,
336
+ allOverlays,
337
+ defineOverlay2 as defineOverlay,
338
+ drawOverlays,
339
+ gridsOverlay,
340
+ isOverlayOn,
341
+ overlayNames,
342
+ setOverlays,
343
+ slotMesh
344
+ };
@@ -0,0 +1,20 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /**
3
+ * An entity's name: the last segment of its scene path (`scene/SceneMember`) or instance path
4
+ * (`scene/InstancePart`). Undefined for entities made in code without either. Cold path: allocates.
5
+ */
6
+ export declare function entityName(world: World, entity: Entity): string | undefined;
7
+ /**
8
+ * Every descendant of `root` by path relative to it (`Armature/Hips/Spine`), plus `''` for the root
9
+ * itself. Entities without a name (and their subtrees) are left out. Cold path: allocates.
10
+ */
11
+ export declare function descendantPaths(world: World, root: Entity): Map<string, Entity>;
12
+ /**
13
+ * Resolves paths relative to a model root, looking for that root among `entity` and its
14
+ * ancestors: the nearest one under which `probe` (a path every root has, like a skin's first joint)
15
+ * exists. Returns the root and its paths, or undefined. Cold path: allocates.
16
+ */
17
+ export declare function findModelRoot(world: World, entity: Entity, probe: string): {
18
+ root: Entity;
19
+ paths: Map<string, Entity>;
20
+ } | undefined;
package/dist/paths.js ADDED
@@ -0,0 +1,46 @@
1
+ import {
2
+ ChildOf,
3
+ Children,
4
+ findComponent
5
+ } from "@aethervtt/shard-core";
6
+ let memberDef;
7
+ let partDef;
8
+ function entityName(world, entity) {
9
+ if (!memberDef) memberDef = findComponent("scene/SceneMember") ?? null;
10
+ if (!partDef) partDef = findComponent("scene/InstancePart") ?? null;
11
+ const path = memberDef && world.tryGet(entity, memberDef)?.path || partDef && world.tryGet(entity, partDef)?.path;
12
+ if (!path) return void 0;
13
+ const slash = path.lastIndexOf("/");
14
+ return slash === -1 ? path : path.slice(slash + 1);
15
+ }
16
+ function descendantPaths(world, root) {
17
+ const out = /* @__PURE__ */ new Map([["", root]]);
18
+ const visit = (entity, prefix, depth) => {
19
+ if (depth > 64 || !world.has(entity, Children)) return;
20
+ for (const child of world.get(entity, Children).entities) {
21
+ if (child === null || !world.isAlive(child)) continue;
22
+ const name = entityName(world, child);
23
+ if (name === void 0) continue;
24
+ const path = prefix ? `${prefix}/${name}` : name;
25
+ if (!out.has(path)) out.set(path, child);
26
+ visit(child, path, depth + 1);
27
+ }
28
+ };
29
+ visit(root, "", 0);
30
+ return out;
31
+ }
32
+ function findModelRoot(world, entity, probe) {
33
+ let current = entity;
34
+ for (let depth = 0; current !== null && depth < 32; depth++) {
35
+ const paths = descendantPaths(world, current);
36
+ if (paths.has(probe)) return { root: current, paths };
37
+ current = world.has(current, ChildOf) ? world.get(current, ChildOf).parent : null;
38
+ if (current !== null && !world.isAlive(current)) return void 0;
39
+ }
40
+ return void 0;
41
+ }
42
+ export {
43
+ descendantPaths,
44
+ entityName,
45
+ findModelRoot
46
+ };
@@ -0,0 +1,2 @@
1
+ /** Picking (spec 0027): what's under a pixel (`pick`), and CPU raycasts against meshes. */
2
+ export declare const pickingPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,20 @@
1
+ import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { PICK_SHADERS } from "./debug-shaders.js";
3
+ import { addPickNodes, BvhResource, Picking } from "./picking.js";
4
+ import { Shaders } from "./plugin.js";
5
+ import { registerShaders } from "./shaders.js";
6
+ const pickingPlugin = definePlugin({
7
+ name: "render/picking",
8
+ dependencies: ["render/forward"],
9
+ provides: [BvhResource, Picking],
10
+ build(app) {
11
+ app.world.initResource(Picking);
12
+ },
13
+ ready(app) {
14
+ registerShaders(app.world.resource(Shaders), PICK_SHADERS);
15
+ addPickNodes(app.world);
16
+ }
17
+ });
18
+ export {
19
+ pickingPlugin
20
+ };
@@ -0,0 +1,94 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { Mesh } from '@aethervtt/shard-mesh';
3
+ import type { NodeContext, NodeDescriptor, RenderView } from './graph.js';
4
+ import { type CameraData } from './view.js';
5
+ export interface PickHit {
6
+ entity: Entity;
7
+ /** Scene path, when the entity came from a scene. */
8
+ path: string | undefined;
9
+ /** World position of the surface under the pixel. */
10
+ position: [number, number, number];
11
+ /** World normal of that surface, facing the camera. */
12
+ normal: [number, number, number];
13
+ /** Meters from the camera (from the ray origin, for raycasts). */
14
+ distance: number;
15
+ }
16
+ interface PickRequest {
17
+ x: number;
18
+ y: number;
19
+ resolve: (hit: PickHit | undefined) => void;
20
+ reject: (error: unknown) => void;
21
+ }
22
+ /**
23
+ * Something that draws pickable things into the picking pass besides meshes (sprites). It gets
24
+ * the pass with the picking targets bound (PICK_TARGETS: r32uint entity id, rgba32float normal
25
+ * and depth) and a depth32float attachment (reversed Z, compare greater).
26
+ */
27
+ export type PickDrawer = (ctx: NodeContext, cam: CameraData) => void;
28
+ /**
29
+ * Something drawn over the scene that takes pointer input at a pixel (UI): a pick there hits it,
30
+ * not the world, and resolves to undefined. Gets the view name and the pixel.
31
+ */
32
+ export type PickBlocker = (world: World, view: string, x: number, y: number) => boolean;
33
+ export interface PickingState {
34
+ /** Requests per view name, served by the next frame that renders the view. */
35
+ pending: Map<string, PickRequest[]>;
36
+ drawers: Map<string, PickDrawer>;
37
+ blockers: Map<string, PickBlocker>;
38
+ /** Views whose pick pass skipped a draw (a pipeline still compiling): their picks wait a frame. */
39
+ incomplete: Set<string>;
40
+ }
41
+ export declare const Picking: import("@aethervtt/shard-core").ResourceDef<PickingState>;
42
+ /** The camera view `pick` and `render.capture` use by default: the first to render. */
43
+ export declare function primaryView(world: World): RenderView | undefined;
44
+ /**
45
+ * What's under pixel (x, y) of a camera's view (pixels from the top left): the entity, its scene
46
+ * path, and the world position and normal of the surface. Resolves after the next frame renders
47
+ * the view; undefined when nothing's there. Meshes (instanced and LOD too) and world-space sprites
48
+ * are pickable; gizmos and screen-space sprites and text aren't. A pixel covered by UI that takes
49
+ * the pointer (a PickBlocker) resolves to undefined: clicks on a HUD don't reach the world.
50
+ */
51
+ export declare function pick(world: World, camera: Entity | undefined, x: number, y: number): Promise<PickHit | undefined>;
52
+ /** The picking pass: meshes, then the extra drawers, into the pick targets of views with requests. */
53
+ export declare function pickNode(world: World): NodeDescriptor;
54
+ /** Copies the requested pixels out of the pick targets and resolves the requests once mapped. */
55
+ export declare function pickReadbackNode(world: World): NodeDescriptor;
56
+ /** Adds the picking targets and nodes to the graph. */
57
+ export declare function addPickNodes(world: World): void;
58
+ export interface RaycastOptions {
59
+ /** Ignore hits farther than this (m). Default: no limit. */
60
+ maxDistance?: number;
61
+ /** Every hit along the ray, nearest first (default: only the nearest). */
62
+ all?: boolean;
63
+ /** Test bounds only, never triangles. */
64
+ boundsOnly?: boolean;
65
+ }
66
+ interface Item {
67
+ entity: Entity;
68
+ mesh: Mesh;
69
+ /** World matrix rows (12) and their inverse (12). */
70
+ m: Float32Array;
71
+ inv: Float32Array;
72
+ box: Float32Array;
73
+ }
74
+ /** A bounding volume hierarchy over the world bounds of mesh entities, rebuilt when they change. */
75
+ interface Bvh {
76
+ items: Item[];
77
+ /** Node boxes (6 floats each) and, per node, [left, right, 0] (inner) or [first item, 0, count]. */
78
+ boxes: Float32Array;
79
+ nodes: Int32Array;
80
+ order: Int32Array;
81
+ count: number;
82
+ tick: number;
83
+ tables: number;
84
+ }
85
+ export declare const BvhResource: import("@aethervtt/shard-core").ResourceDef<{
86
+ bvh: Bvh | undefined;
87
+ }>;
88
+ /**
89
+ * Casts a ray against mesh entities on the CPU: their bounds (through a BVH), then their triangles.
90
+ * Works without a GPU. Direction needn't be normalized. Hits come back nearest first; normals face
91
+ * the ray. Physics (M6) adds collider hits behind this same function.
92
+ */
93
+ export declare function raycast(world: World, origin: ArrayLike<number>, direction: ArrayLike<number>, options?: RaycastOptions): PickHit[];
94
+ export {};