@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,456 @@
1
+ import { defineResource, mat4, ShardError } from "@aethervtt/shard-core";
2
+ import { GlobalTransform } from "@aethervtt/shard-transform";
3
+ import { Meshes } from "./assets.js";
4
+ import { drawMaterials, ForwardStateResource, PASS_PICK } from "./forward.js";
5
+ import { RenderPhase } from "./graph.js";
6
+ import { Lod, Mesh3d } from "./instances.js";
7
+ import { DebugOverlays } from "./overlays.js";
8
+ import { Graph, Views } from "./plugin.js";
9
+ import { cameraOf } from "./view.js";
10
+ import { ComputedVisibility } from "./visibility.js";
11
+ const Picking = defineResource("render/Picking", {
12
+ description: "GPU picking: pending pick requests per view, and extra pick drawers (sprites).",
13
+ init: () => ({
14
+ pending: /* @__PURE__ */ new Map(),
15
+ drawers: /* @__PURE__ */ new Map(),
16
+ blockers: /* @__PURE__ */ new Map(),
17
+ incomplete: /* @__PURE__ */ new Set()
18
+ })
19
+ });
20
+ function primaryView(world) {
21
+ let best;
22
+ for (const view of world.resource(Views).list) {
23
+ if (!cameraOf(view)) continue;
24
+ if (!best || view.order < best.order) best = view;
25
+ }
26
+ return best;
27
+ }
28
+ function pick(world, camera, x, y) {
29
+ const name = camera !== void 0 ? `camera:${camera}` : primaryView(world)?.name;
30
+ if (!name) {
31
+ return Promise.reject(
32
+ new ShardError("render/no-view", "Nothing to pick from: no camera renders to a target", {
33
+ hint: "Spawn an entity with Camera3d, or pass the camera to pick from."
34
+ })
35
+ );
36
+ }
37
+ const picking = world.resource(Picking);
38
+ for (const blocked of picking.blockers.values()) {
39
+ if (blocked(world, name, x, y)) return Promise.resolve(void 0);
40
+ }
41
+ return new Promise((resolve, reject) => {
42
+ let list = picking.pending.get(name);
43
+ if (!list) {
44
+ list = [];
45
+ picking.pending.set(name, list);
46
+ }
47
+ list.push({ x, y, resolve, reject });
48
+ });
49
+ }
50
+ function pickNode(world) {
51
+ const picking = world.resource(Picking);
52
+ return {
53
+ kind: "render",
54
+ phase: RenderPhase.Debug,
55
+ enabled: (view) => cameraOf(view) !== void 0 && (picking.pending.get(view.name)?.length ?? 0) > 0,
56
+ reads: ["culled"],
57
+ writes: ["pick-id", "pick-normal", "pick-depth"],
58
+ color: [
59
+ { resource: "pick-id", clear: { r: 0, g: 0, b: 0, a: 0 } },
60
+ { resource: "pick-normal", clear: { r: 0, g: 0, b: 0, a: 0 } }
61
+ ],
62
+ depth: { resource: "pick-depth", clear: 0 },
63
+ run: (ctx) => {
64
+ const cam = cameraOf(ctx.view);
65
+ const state = ctx.world.resource(ForwardStateResource);
66
+ const pv = state.views.get(ctx.view.name);
67
+ if (!pv) return;
68
+ const skipped = ctx.gpu.pipelines.skipped;
69
+ drawMaterials(ctx, state, pv, cam, cam.draws, PASS_PICK);
70
+ if (cam.deferred) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_PICK);
71
+ drawMaterials(ctx, state, pv, cam, cam.transparent, PASS_PICK);
72
+ for (const draw of picking.drawers.values()) draw(ctx, cam);
73
+ if (ctx.gpu.pipelines.skipped > skipped) picking.incomplete.add(ctx.view.name);
74
+ }
75
+ };
76
+ }
77
+ const inv = new Float32Array(16);
78
+ function pickReadbackNode(world) {
79
+ const picking = world.resource(Picking);
80
+ return {
81
+ kind: "raw",
82
+ phase: RenderPhase.Debug + 1,
83
+ enabled: (view) => cameraOf(view) !== void 0 && (picking.pending.get(view.name)?.length ?? 0) > 0,
84
+ reads: ["pick-id", "pick-normal"],
85
+ sideEffects: true,
86
+ run: (ctx) => {
87
+ const requests = picking.pending.get(ctx.view.name);
88
+ if (!requests || requests.length === 0) return;
89
+ if (picking.incomplete.delete(ctx.view.name)) return;
90
+ picking.pending.set(ctx.view.name, []);
91
+ const cam = cameraOf(ctx.view);
92
+ const device = ctx.gpu.device;
93
+ const id = ctx.texture("pick-id");
94
+ const normal = ctx.texture("pick-normal");
95
+ const buffer = device.createBuffer({
96
+ label: "pick/readback",
97
+ size: requests.length * 512,
98
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
99
+ });
100
+ const width = id.width;
101
+ const height = id.height;
102
+ const sx = width / ctx.view.target.width;
103
+ const sy = height / ctx.view.target.height;
104
+ requests.forEach((r, i) => {
105
+ const x = Math.min(width - 1, Math.max(0, Math.floor(r.x * sx)));
106
+ const y = Math.min(height - 1, Math.max(0, Math.floor(r.y * sy)));
107
+ const origin = { x, y };
108
+ ctx.encoder.copyTextureToBuffer(
109
+ { texture: id, origin },
110
+ { buffer, offset: i * 512, bytesPerRow: 256 },
111
+ [1, 1]
112
+ );
113
+ ctx.encoder.copyTextureToBuffer(
114
+ { texture: normal, origin },
115
+ { buffer, offset: i * 512 + 256, bytesPerRow: 256 },
116
+ [1, 1]
117
+ );
118
+ });
119
+ if (!mat4.invert(inv, cam.viewProjNoJitter)) inv.fill(0);
120
+ const unproject = new Float32Array(inv);
121
+ const eye = Float32Array.from(cam.position);
122
+ const name = ctx.world.resource(DebugOverlays).name;
123
+ const w = ctx.world;
124
+ ctx.afterSubmit(() => {
125
+ buffer.mapAsync(GPUMapMode.READ).then(
126
+ () => {
127
+ const bytes = buffer.getMappedRange();
128
+ requests.forEach((r, i) => {
129
+ const entity = new Uint32Array(bytes, i * 512, 1)[0];
130
+ const n = new Float32Array(bytes, i * 512 + 256, 4);
131
+ const d = n[3];
132
+ if (!(d > 0)) {
133
+ r.resolve(void 0);
134
+ return;
135
+ }
136
+ const px = Math.min(width - 1, Math.max(0, Math.floor(r.x * sx))) + 0.5;
137
+ const py = Math.min(height - 1, Math.max(0, Math.floor(r.y * sy))) + 0.5;
138
+ const nx = px / width * 2 - 1;
139
+ const ny = 1 - py / height * 2;
140
+ const m = unproject;
141
+ const hw = m[3] * nx + m[7] * ny + m[11] * d + m[15];
142
+ const position = [
143
+ (m[0] * nx + m[4] * ny + m[8] * d + m[12]) / hw,
144
+ (m[1] * nx + m[5] * ny + m[9] * d + m[13]) / hw,
145
+ (m[2] * nx + m[6] * ny + m[10] * d + m[14]) / hw
146
+ ];
147
+ const dx = position[0] - eye[0];
148
+ const dy = position[1] - eye[1];
149
+ const dz = position[2] - eye[2];
150
+ r.resolve({
151
+ entity,
152
+ path: w.isAlive(entity) ? name(w, entity) : void 0,
153
+ position,
154
+ normal: [n[0], n[1], n[2]],
155
+ distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
156
+ });
157
+ });
158
+ buffer.unmap();
159
+ buffer.destroy();
160
+ },
161
+ (error) => {
162
+ for (const r of requests) r.reject(error);
163
+ buffer.destroy();
164
+ }
165
+ );
166
+ });
167
+ }
168
+ };
169
+ }
170
+ function addPickNodes(world) {
171
+ const graph = world.resource(Graph);
172
+ const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
173
+ graph.declare({ name: "pick-id", format: "r32uint", usage });
174
+ graph.declare({ name: "pick-normal", format: "rgba32float", usage });
175
+ graph.declare({ name: "pick-depth", format: "depth32float" });
176
+ graph.addNode("picking", pickNode(world));
177
+ graph.addNode("picking/readback", pickReadbackNode(world));
178
+ }
179
+ const BvhResource = defineResource("render/RaycastBvh", {
180
+ description: "The CPU raycast acceleration structure.",
181
+ init: () => ({ bvh: void 0 })
182
+ });
183
+ function meshOf(world, entity) {
184
+ const meshes = world.tryResource(Meshes);
185
+ if (!meshes) return void 0;
186
+ const lod = world.tryGet(entity, Lod);
187
+ const ref = lod?.levels[0]?.mesh ?? world.get(entity, Mesh3d).mesh;
188
+ return ref ? meshes.get(ref) : void 0;
189
+ }
190
+ function invertAffine(m, out) {
191
+ const a = m[0];
192
+ const b = m[1];
193
+ const c = m[2];
194
+ const tx = m[3];
195
+ const d = m[4];
196
+ const e = m[5];
197
+ const f = m[6];
198
+ const ty = m[7];
199
+ const g = m[8];
200
+ const h = m[9];
201
+ const i = m[10];
202
+ const tz = m[11];
203
+ const A = e * i - f * h;
204
+ const B = -(d * i - f * g);
205
+ const C = d * h - e * g;
206
+ const det = a * A + b * B + c * C;
207
+ const s = det !== 0 ? 1 / det : 0;
208
+ out[0] = A * s;
209
+ out[1] = -(b * i - c * h) * s;
210
+ out[2] = (b * f - c * e) * s;
211
+ out[4] = B * s;
212
+ out[5] = (a * i - c * g) * s;
213
+ out[6] = -(a * f - c * d) * s;
214
+ out[8] = C * s;
215
+ out[9] = -(a * h - b * g) * s;
216
+ out[10] = (a * e - b * d) * s;
217
+ out[3] = -(out[0] * tx + out[1] * ty + out[2] * tz);
218
+ out[7] = -(out[4] * tx + out[5] * ty + out[6] * tz);
219
+ out[11] = -(out[8] * tx + out[9] * ty + out[10] * tz);
220
+ }
221
+ function worldBox(bb, m, out) {
222
+ for (let r = 0; r < 3; r++) {
223
+ const o = r * 4;
224
+ let lo2 = m[o + 3];
225
+ let hi = m[o + 3];
226
+ for (let k = 0; k < 3; k++) {
227
+ const x = m[o + k] * bb[k];
228
+ const y = m[o + k] * bb[k + 3];
229
+ lo2 += Math.min(x, y);
230
+ hi += Math.max(x, y);
231
+ }
232
+ out[r] = lo2;
233
+ out[r + 3] = hi;
234
+ }
235
+ }
236
+ function buildBvh(world, tick, tables) {
237
+ const items = [];
238
+ for (const table of world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility] }).tables) {
239
+ const g = table.column(GlobalTransform, "matrix");
240
+ const vis = table.column(ComputedVisibility, "visible");
241
+ for (let i = 0; i < table.count; i++) {
242
+ if (!vis[i]) continue;
243
+ const entity = table.entities[i];
244
+ const mesh = meshOf(world, entity);
245
+ if (!mesh) continue;
246
+ const m = g.slice(i * 12, i * 12 + 12);
247
+ const inverse = new Float32Array(12);
248
+ invertAffine(m, inverse);
249
+ const box = new Float32Array(6);
250
+ worldBox(mesh.bounds, m, box);
251
+ items.push({ entity, mesh, m, inv: inverse, box });
252
+ }
253
+ }
254
+ const n = Math.max(1, items.length);
255
+ const boxes = new Float32Array(n * 2 * 6);
256
+ const nodes = new Int32Array(n * 2 * 3);
257
+ const order = new Int32Array(items.length).map((_, i) => i);
258
+ let count = 0;
259
+ const build = (start, end) => {
260
+ const node = count++;
261
+ const b = boxes.subarray(node * 6, node * 6 + 6);
262
+ b.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
263
+ for (let k = start; k < end; k++) {
264
+ const ib = items[order[k]].box;
265
+ for (let a = 0; a < 3; a++) {
266
+ if (ib[a] < b[a]) b[a] = ib[a];
267
+ if (ib[a + 3] > b[a + 3]) b[a + 3] = ib[a + 3];
268
+ }
269
+ }
270
+ if (end - start <= 4) {
271
+ nodes[node * 3] = start;
272
+ nodes[node * 3 + 2] = end - start;
273
+ return node;
274
+ }
275
+ let axis = 0;
276
+ for (let a = 1; a < 3; a++) if (b[a + 3] - b[a] > b[axis + 3] - b[axis]) axis = a;
277
+ const part = Array.from(order.subarray(start, end)).sort(
278
+ (p, q) => items[p].box[axis] + items[p].box[axis + 3] - (items[q].box[axis] + items[q].box[axis + 3])
279
+ );
280
+ order.set(part, start);
281
+ const mid = start + end >> 1;
282
+ const left = build(start, mid);
283
+ const right = build(mid, end);
284
+ nodes[node * 3] = left;
285
+ nodes[node * 3 + 1] = right;
286
+ nodes[node * 3 + 2] = 0;
287
+ return node;
288
+ };
289
+ if (items.length > 0) build(0, items.length);
290
+ return { items, boxes, nodes, order, count, tick, tables };
291
+ }
292
+ function bvhOf(world) {
293
+ const r = world.initResource(BvhResource);
294
+ const q = world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility] });
295
+ let tick = 0;
296
+ for (const table of q.tables) {
297
+ tick = Math.max(
298
+ tick,
299
+ table.lastStructural,
300
+ table.lastChanged(GlobalTransform),
301
+ table.lastChanged(Mesh3d),
302
+ table.lastChanged(ComputedVisibility)
303
+ );
304
+ }
305
+ const cur = r.bvh;
306
+ if (!cur || cur.tick !== tick || cur.tables !== q.tables.length)
307
+ r.bvh = buildBvh(world, tick, q.tables.length);
308
+ return r.bvh;
309
+ }
310
+ function slabs(o, d, b, max) {
311
+ let t0 = 0;
312
+ let t1 = max;
313
+ for (let a = 0; a < 3; a++) {
314
+ const inv2 = 1 / d[a];
315
+ let near = (b[a] - o[a]) * inv2;
316
+ let far = (b[a + 3] - o[a]) * inv2;
317
+ if (near > far) {
318
+ const t = near;
319
+ near = far;
320
+ far = t;
321
+ }
322
+ if (Number.isNaN(near)) near = -Infinity;
323
+ if (Number.isNaN(far)) far = Infinity;
324
+ if (near > t0) t0 = near;
325
+ if (far < t1) t1 = far;
326
+ if (t0 > t1) return -1;
327
+ }
328
+ return t0;
329
+ }
330
+ const lo = new Float64Array(3);
331
+ const ld = new Float64Array(3);
332
+ const ln = new Float64Array(3);
333
+ function intersectMesh(mesh, max) {
334
+ const p = mesh.positions;
335
+ const idx = mesh.indices;
336
+ const tris = idx ? idx.length / 3 : p.length / 9;
337
+ let best = -1;
338
+ for (let t = 0; t < tris; t++) {
339
+ const i0 = (idx ? idx[t * 3] : t * 3) * 3;
340
+ const i1 = (idx ? idx[t * 3 + 1] : t * 3 + 1) * 3;
341
+ const i2 = (idx ? idx[t * 3 + 2] : t * 3 + 2) * 3;
342
+ const e1x = p[i1] - p[i0];
343
+ const e1y = p[i1 + 1] - p[i0 + 1];
344
+ const e1z = p[i1 + 2] - p[i0 + 2];
345
+ const e2x = p[i2] - p[i0];
346
+ const e2y = p[i2 + 1] - p[i0 + 1];
347
+ const e2z = p[i2 + 2] - p[i0 + 2];
348
+ const px = ld[1] * e2z - ld[2] * e2y;
349
+ const py = ld[2] * e2x - ld[0] * e2z;
350
+ const pz = ld[0] * e2y - ld[1] * e2x;
351
+ const det = e1x * px + e1y * py + e1z * pz;
352
+ if (Math.abs(det) < 1e-12) continue;
353
+ const invDet = 1 / det;
354
+ const sx = lo[0] - p[i0];
355
+ const sy = lo[1] - p[i0 + 1];
356
+ const sz = lo[2] - p[i0 + 2];
357
+ const u = (sx * px + sy * py + sz * pz) * invDet;
358
+ if (u < 0 || u > 1) continue;
359
+ const qx = sy * e1z - sz * e1y;
360
+ const qy = sz * e1x - sx * e1z;
361
+ const qz = sx * e1y - sy * e1x;
362
+ const v = (ld[0] * qx + ld[1] * qy + ld[2] * qz) * invDet;
363
+ if (v < 0 || u + v > 1) continue;
364
+ const dist = (e2x * qx + e2y * qy + e2z * qz) * invDet;
365
+ if (dist < 0 || dist > max || best >= 0 && dist >= best) continue;
366
+ best = dist;
367
+ ln[0] = e1y * e2z - e1z * e2y;
368
+ ln[1] = e1z * e2x - e1x * e2z;
369
+ ln[2] = e1x * e2y - e1y * e2x;
370
+ }
371
+ return best;
372
+ }
373
+ function intersectLocalBox(bb, max) {
374
+ const t = slabs(lo, ld, bb, max);
375
+ if (t < 0) return -1;
376
+ let axis = 0;
377
+ let bestGap = Infinity;
378
+ for (let a = 0; a < 3; a++) {
379
+ const p = lo[a] + ld[a] * t;
380
+ const gap = Math.min(Math.abs(p - bb[a]), Math.abs(p - bb[a + 3]));
381
+ if (gap < bestGap) {
382
+ bestGap = gap;
383
+ axis = a;
384
+ }
385
+ }
386
+ ln[0] = ln[1] = ln[2] = 0;
387
+ ln[axis] = ld[axis] > 0 ? -1 : 1;
388
+ return t;
389
+ }
390
+ function raycast(world, origin, direction, options = {}) {
391
+ const dl = Math.sqrt(direction[0] ** 2 + direction[1] ** 2 + direction[2] ** 2);
392
+ if (dl === 0) return [];
393
+ const d = [direction[0] / dl, direction[1] / dl, direction[2] / dl];
394
+ const max = options.maxDistance ?? Infinity;
395
+ const bvh = bvhOf(world);
396
+ const name = world.tryResource(DebugOverlays)?.name;
397
+ const hits = [];
398
+ let nearest = max;
399
+ const stack = [0];
400
+ while (stack.length > 0 && bvh.items.length > 0) {
401
+ const node = stack.pop();
402
+ const limit = options.all ? max : nearest;
403
+ if (slabs(origin, d, bvh.boxes.subarray(node * 6, node * 6 + 6), limit) < 0) continue;
404
+ const count = bvh.nodes[node * 3 + 2];
405
+ if (count === 0) {
406
+ stack.push(bvh.nodes[node * 3], bvh.nodes[node * 3 + 1]);
407
+ continue;
408
+ }
409
+ const first = bvh.nodes[node * 3];
410
+ for (let k = first; k < first + count; k++) {
411
+ const item = bvh.items[bvh.order[k]];
412
+ if (slabs(origin, d, item.box, options.all ? max : nearest) < 0) continue;
413
+ const iv = item.inv;
414
+ for (let r = 0; r < 3; r++) {
415
+ lo[r] = iv[r * 4] * origin[0] + iv[r * 4 + 1] * origin[1] + iv[r * 4 + 2] * origin[2] + iv[r * 4 + 3];
416
+ ld[r] = iv[r * 4] * d[0] + iv[r * 4 + 1] * d[1] + iv[r * 4 + 2] * d[2];
417
+ }
418
+ const limit2 = options.all ? max : nearest;
419
+ const cpu = !options.boundsOnly && item.mesh.positions?.length > 0;
420
+ const t = cpu ? intersectMesh(item.mesh, limit2) : intersectLocalBox(item.mesh.bounds, limit2);
421
+ if (t < 0) continue;
422
+ let nx = iv[0] * ln[0] + iv[4] * ln[1] + iv[8] * ln[2];
423
+ let ny = iv[1] * ln[0] + iv[5] * ln[1] + iv[9] * ln[2];
424
+ let nz = iv[2] * ln[0] + iv[6] * ln[1] + iv[10] * ln[2];
425
+ const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
426
+ nx /= nl;
427
+ ny /= nl;
428
+ nz /= nl;
429
+ if (nx * d[0] + ny * d[1] + nz * d[2] > 0) {
430
+ nx = -nx;
431
+ ny = -ny;
432
+ nz = -nz;
433
+ }
434
+ hits.push({
435
+ entity: item.entity,
436
+ path: name?.(world, item.entity),
437
+ position: [origin[0] + d[0] * t, origin[1] + d[1] * t, origin[2] + d[2] * t],
438
+ normal: [nx, ny, nz],
439
+ distance: t
440
+ });
441
+ if (t < nearest) nearest = t;
442
+ }
443
+ }
444
+ hits.sort((a, b) => a.distance - b.distance);
445
+ return options.all ? hits : hits.slice(0, 1);
446
+ }
447
+ export {
448
+ BvhResource,
449
+ Picking,
450
+ addPickNodes,
451
+ pick,
452
+ pickNode,
453
+ pickReadbackNode,
454
+ primaryView,
455
+ raycast
456
+ };
@@ -0,0 +1,9 @@
1
+ import type { NodeDescriptor } from './graph.js';
2
+ export declare const PIXEL_PERFECT_SHADERS: Record<string, string>;
3
+ /** Draws a PixelPerfect camera's image onto its real target, scaled by a whole number. */
4
+ export declare function pixelUpscaleNode(): NodeDescriptor;
5
+ /**
6
+ * Pixel-perfect cameras (spec 0045): PixelPerfect renders at one texel per pixel and scales up by a
7
+ * whole number. Without it, PixelPerfect cameras render at full resolution.
8
+ */
9
+ export declare const pixelPerfectPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,135 @@
1
+ import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { RenderPhase } from "./graph.js";
3
+ import { PixelPerfectPath } from "./pixel-perfect.js";
4
+ import { Graph, Shaders } from "./plugin.js";
5
+ import { registerShaders } from "./shaders.js";
6
+ const PIXEL_PERFECT_SHADERS = {
7
+ "shard::post::pixel_upscale": `
8
+ struct Upscale {
9
+ /** offset (xy), scale, 0. */
10
+ params: vec4f,
11
+ }
12
+
13
+ @group(0) @binding(0) var source: texture_2d<f32>;
14
+ @group(0) @binding(1) var<uniform> upscale: Upscale;
15
+
16
+ /** Each texel becomes a scale \xD7 scale block; outside the image, black bars. */
17
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
18
+ let p = (frag.xy - upscale.params.xy) / upscale.params.z;
19
+ let size = vec2f(textureDimensions(source));
20
+ if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0, 1.0); }
21
+ return textureLoad(source, vec2i(floor(p)), 0);
22
+ }`
23
+ };
24
+ const layoutKey = /* @__PURE__ */ new WeakMap();
25
+ function pixelUpscaleNode() {
26
+ const buffers = /* @__PURE__ */ new Map();
27
+ const groups = /* @__PURE__ */ new Map();
28
+ const pipelines = /* @__PURE__ */ new Map();
29
+ const params = new Float32Array(4);
30
+ let generation = -1;
31
+ return {
32
+ kind: "render",
33
+ phase: RenderPhase.Display,
34
+ enabled: (view) => view.data.upscale !== void 0,
35
+ writes: ["view-target"],
36
+ color: [{ resource: "view-target", clear: { r: 0, g: 0, b: 0, a: 1 } }],
37
+ run: (ctx) => {
38
+ const gpu = ctx.gpu;
39
+ if (generation !== gpu.generation) {
40
+ generation = gpu.generation;
41
+ buffers.clear();
42
+ groups.clear();
43
+ pipelines.clear();
44
+ }
45
+ const layout = ctx.view.data.upscale;
46
+ const shaders = ctx.world.resource(Shaders);
47
+ const fs = shaders.module(gpu, { root: "shard::post::pixel_upscale" });
48
+ const vs = shaders.module(gpu, { root: "shard::fullscreen" });
49
+ if (!fs || !vs) {
50
+ gpu.pipelines.skipped++;
51
+ return;
52
+ }
53
+ let bindLayout = layoutKey.get(gpu.device);
54
+ if (!bindLayout) {
55
+ bindLayout = gpu.layouts.bindGroupLayout({
56
+ label: "pixel-upscale",
57
+ entries: [
58
+ {
59
+ binding: 0,
60
+ visibility: GPUShaderStage.FRAGMENT,
61
+ texture: { sampleType: "unfilterable-float" }
62
+ },
63
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
64
+ ]
65
+ });
66
+ layoutKey.set(gpu.device, bindLayout);
67
+ }
68
+ const format = ctx.texture("view-target").format;
69
+ let pipeline = pipelines.get(format);
70
+ if (!pipeline) {
71
+ pipeline = gpu.pipelines.render({
72
+ label: `pixel-upscale/${format}`,
73
+ layout: gpu.layouts.pipelineLayout({
74
+ label: "pixel-upscale",
75
+ bindGroupLayouts: [bindLayout]
76
+ }),
77
+ vertex: { module: vs, entryPoint: "vs" },
78
+ fragment: { module: fs, entryPoint: "fs", targets: [{ format }] }
79
+ });
80
+ if (!pipeline) return;
81
+ pipelines.set(format, pipeline);
82
+ }
83
+ let buffer = buffers.get(ctx.view.name);
84
+ if (!buffer) {
85
+ buffer = gpu.device.createBuffer({
86
+ label: `${ctx.view.name}/upscale`,
87
+ size: 16,
88
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
89
+ });
90
+ buffers.set(ctx.view.name, buffer);
91
+ }
92
+ params[0] = layout.offsetX;
93
+ params[1] = layout.offsetY;
94
+ params[2] = layout.scale;
95
+ gpu.device.queue.writeBuffer(buffer, 0, params);
96
+ const source = layout.target.texture();
97
+ let cached = groups.get(ctx.view.name);
98
+ if (!cached || cached.texture !== source) {
99
+ cached = {
100
+ texture: source,
101
+ group: gpu.device.createBindGroup({
102
+ label: "pixel-upscale",
103
+ layout: bindLayout,
104
+ entries: [
105
+ { binding: 0, resource: source.createView() },
106
+ { binding: 1, resource: { buffer } }
107
+ ]
108
+ })
109
+ };
110
+ groups.set(ctx.view.name, cached);
111
+ }
112
+ const group = cached.group;
113
+ const pass = ctx.renderPass;
114
+ pass.setPipeline(pipeline);
115
+ pass.setBindGroup(0, group);
116
+ pass.draw(3);
117
+ }
118
+ };
119
+ }
120
+ const pixelPerfectPlugin = definePlugin({
121
+ name: "render/pixel-perfect",
122
+ dependencies: ["render/forward"],
123
+ build(app) {
124
+ app.insertResource(PixelPerfectPath, { installed: true });
125
+ },
126
+ ready(app) {
127
+ registerShaders(app.world.resource(Shaders), PIXEL_PERFECT_SHADERS);
128
+ app.world.resource(Graph).addNode("pixel-upscale", pixelUpscaleNode());
129
+ }
130
+ });
131
+ export {
132
+ PIXEL_PERFECT_SHADERS,
133
+ pixelPerfectPlugin,
134
+ pixelUpscaleNode
135
+ };
@@ -0,0 +1,27 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { OffscreenTarget, type RenderTarget } from './target.js';
3
+ export declare const PixelPerfect: import("@aethervtt/shard-core").ComponentDef<{
4
+ readonly pixelsPerUnit: import("@aethervtt/shard-core").FieldType<number, "f32">;
5
+ readonly snap: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
6
+ }>;
7
+ /** How a PixelPerfect camera renders: its low-resolution target and the integer scale up. */
8
+ export interface PixelPerfectLayout {
9
+ target: OffscreenTarget;
10
+ pixelsPerUnit: number;
11
+ snap: boolean;
12
+ /** Whole-number magnification. */
13
+ scale: number;
14
+ /** Low-resolution size, in texels. */
15
+ width: number;
16
+ height: number;
17
+ /** Top-left of the scaled image on the real target (letterbox), in pixels. */
18
+ offsetX: number;
19
+ offsetY: number;
20
+ }
21
+ /** Present when pixelPerfectPlugin is installed; without it PixelPerfect cameras render normally. */
22
+ export declare const PixelPerfectPath: import("@aethervtt/shard-core").ResourceDef<{
23
+ installed: true;
24
+ }>;
25
+ export declare const PixelTargets: import("@aethervtt/shard-core").ResourceDef<Map<number, PixelPerfectLayout>>;
26
+ /** Sizes (and keeps) a PixelPerfect camera's low-resolution target for the target it shows on. */
27
+ export declare function pixelPerfectLayout(world: World, entity: Entity, shown: RenderTarget, orthoHeight: number, pixelsPerUnit: number, snap: boolean): PixelPerfectLayout;
@@ -0,0 +1,70 @@
1
+ import { defineComponent, defineResource, t } from "@aethervtt/shard-core";
2
+ import { Gpu } from "./plugin.js";
3
+ import { OffscreenTarget } from "./target.js";
4
+ const PixelPerfect = defineComponent(
5
+ "render/PixelPerfect",
6
+ {
7
+ pixelsPerUnit: t.f32({
8
+ default: 16,
9
+ min: 0.01,
10
+ description: "Texels per world unit: the art resolution (16 for 16-px tiles one unit wide)."
11
+ }),
12
+ snap: t.bool({
13
+ default: true,
14
+ description: "Moves the camera and sprites in whole texels, so scrolling never shimmers."
15
+ })
16
+ },
17
+ {
18
+ description: "Crisp pixel art on an orthographic camera: renders at one texel per pixel (Camera3d.orthoHeight \xD7 pixelsPerUnit tall), then scales up by the largest whole number that fits the target, with nearest sampling. The camera's orthoHeight is adjusted to fill the letterboxed area exactly."
19
+ }
20
+ );
21
+ const PixelPerfectPath = defineResource("render/PixelPerfectPath", {
22
+ description: "Present when pixel-perfect cameras (pixelPerfectPlugin) are installed."
23
+ });
24
+ const PixelTargets = defineResource("render/PixelTargets", {
25
+ description: "Low-resolution targets of PixelPerfect cameras.",
26
+ init: () => /* @__PURE__ */ new Map()
27
+ });
28
+ function pixelPerfectLayout(world, entity, shown, orthoHeight, pixelsPerUnit, snap) {
29
+ const texels = Math.max(1, Math.round(orthoHeight * pixelsPerUnit));
30
+ const scale = Math.max(1, Math.floor(shown.height / texels));
31
+ const width = Math.max(1, Math.floor(shown.width / scale));
32
+ const height = Math.max(1, Math.floor(shown.height / scale));
33
+ const targets = world.initResource(PixelTargets);
34
+ let layout = targets.get(entity);
35
+ if (!layout) {
36
+ layout = {
37
+ target: new OffscreenTarget(world.resource(Gpu), {
38
+ label: `pixel-perfect:${entity}`,
39
+ width,
40
+ height,
41
+ format: shown.format
42
+ }),
43
+ pixelsPerUnit,
44
+ snap,
45
+ scale,
46
+ width,
47
+ height,
48
+ offsetX: 0,
49
+ offsetY: 0
50
+ };
51
+ targets.set(entity, layout);
52
+ }
53
+ if (layout.target.width !== width || layout.target.height !== height) {
54
+ layout.target.resize(width, height);
55
+ }
56
+ layout.pixelsPerUnit = pixelsPerUnit;
57
+ layout.snap = snap;
58
+ layout.scale = scale;
59
+ layout.width = width;
60
+ layout.height = height;
61
+ layout.offsetX = Math.floor((shown.width - width * scale) / 2);
62
+ layout.offsetY = Math.floor((shown.height - height * scale) / 2);
63
+ return layout;
64
+ }
65
+ export {
66
+ PixelPerfect,
67
+ PixelPerfectPath,
68
+ PixelTargets,
69
+ pixelPerfectLayout
70
+ };