@aethervtt/shard-render 0.0.1 → 0.2.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 (170) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.js +43 -2
  5. package/dist/atmosphere-nodes.d.ts +9 -0
  6. package/dist/atmosphere-nodes.js +157 -30
  7. package/dist/atmosphere-plugin.js +8 -2
  8. package/dist/atmosphere-shaders.js +17 -5
  9. package/dist/baseline/atmosphere.d.ts +16 -0
  10. package/dist/baseline/atmosphere.js +301 -0
  11. package/dist/baseline/data-texture.d.ts +36 -0
  12. package/dist/baseline/data-texture.js +181 -0
  13. package/dist/baseline/environment.d.ts +14 -0
  14. package/dist/baseline/environment.js +368 -0
  15. package/dist/baseline/exposure.d.ts +25 -0
  16. package/dist/baseline/exposure.js +133 -0
  17. package/dist/baseline/lights.d.ts +33 -0
  18. package/dist/baseline/lights.js +133 -0
  19. package/dist/baseline/textures.d.ts +97 -0
  20. package/dist/baseline/textures.js +203 -0
  21. package/dist/camera.d.ts +2 -0
  22. package/dist/camera.js +4 -0
  23. package/dist/crowd.d.ts +21 -0
  24. package/dist/crowd.js +121 -0
  25. package/dist/culling-shaders.js +21 -2
  26. package/dist/culling.d.ts +11 -2
  27. package/dist/culling.js +44 -18
  28. package/dist/cutaway-plugin.d.ts +48 -0
  29. package/dist/cutaway-plugin.js +364 -0
  30. package/dist/cutaway.d.ts +27 -0
  31. package/dist/cutaway.js +49 -0
  32. package/dist/data-store.d.ts +69 -0
  33. package/dist/data-store.js +105 -0
  34. package/dist/debug-shaders.js +17 -3
  35. package/dist/debug-views.js +4 -2
  36. package/dist/decals-plugin.d.ts +5 -0
  37. package/dist/decals-plugin.js +54 -0
  38. package/dist/decals.d.ts +69 -0
  39. package/dist/decals.js +326 -0
  40. package/dist/deferred-shaders.js +2 -0
  41. package/dist/deferred.js +28 -4
  42. package/dist/deform.d.ts +5 -4
  43. package/dist/deform.js +4 -9
  44. package/dist/describe-plugin.d.ts +4 -2
  45. package/dist/describe-plugin.js +75 -3
  46. package/dist/display-nodes.js +37 -13
  47. package/dist/dynamic-resolution.d.ts +2 -0
  48. package/dist/dynamic-resolution.js +6 -1
  49. package/dist/environment-plugin.js +17 -2
  50. package/dist/environment-state.d.ts +7 -3
  51. package/dist/environment-state.js +20 -9
  52. package/dist/environment.js +7 -1
  53. package/dist/features.d.ts +49 -0
  54. package/dist/features.js +106 -0
  55. package/dist/foliage-budget.d.ts +64 -0
  56. package/dist/foliage-budget.js +108 -0
  57. package/dist/foliage-path.d.ts +19 -0
  58. package/dist/foliage-path.js +7 -0
  59. package/dist/foliage-shaders.d.ts +11 -0
  60. package/dist/foliage-shaders.js +504 -0
  61. package/dist/foliage.d.ts +194 -0
  62. package/dist/foliage.js +1027 -0
  63. package/dist/forward.d.ts +38 -2
  64. package/dist/forward.js +608 -186
  65. package/dist/fxaa.js +12 -2
  66. package/dist/gizmos-plugin.js +7 -0
  67. package/dist/gizmos.d.ts +3 -3
  68. package/dist/gizmos.js +11 -13
  69. package/dist/gpu-assets.d.ts +58 -4
  70. package/dist/gpu-assets.js +191 -8
  71. package/dist/graph.d.ts +45 -0
  72. package/dist/graph.js +169 -19
  73. package/dist/health.d.ts +58 -0
  74. package/dist/health.js +112 -0
  75. package/dist/index.d.ts +40 -11
  76. package/dist/index.js +343 -9
  77. package/dist/instances.d.ts +119 -8
  78. package/dist/instances.js +387 -78
  79. package/dist/interior-plugin.d.ts +121 -0
  80. package/dist/interior-plugin.js +465 -0
  81. package/dist/interior.d.ts +24 -0
  82. package/dist/interior.js +13 -0
  83. package/dist/layers.d.ts +35 -0
  84. package/dist/layers.js +49 -0
  85. package/dist/lens-plugin.d.ts +43 -0
  86. package/dist/lens-plugin.js +304 -0
  87. package/dist/lens.d.ts +41 -0
  88. package/dist/lens.js +99 -0
  89. package/dist/lights.d.ts +50 -5
  90. package/dist/lights.js +101 -22
  91. package/dist/material-pipelines.d.ts +37 -4
  92. package/dist/material-pipelines.js +102 -5
  93. package/dist/materials.d.ts +23 -0
  94. package/dist/materials.js +44 -1
  95. package/dist/outline-plugin.d.ts +48 -0
  96. package/dist/outline-plugin.js +529 -0
  97. package/dist/outline.d.ts +15 -0
  98. package/dist/outline.js +42 -0
  99. package/dist/overlay-registry.d.ts +3 -1
  100. package/dist/overlay-registry.js +3 -1
  101. package/dist/overlays.js +6 -2
  102. package/dist/perf-overlay.d.ts +9 -0
  103. package/dist/perf-overlay.js +125 -0
  104. package/dist/picking.d.ts +17 -0
  105. package/dist/picking.js +40 -11
  106. package/dist/pixel-perfect-plugin.js +10 -2
  107. package/dist/plugin.d.ts +69 -7
  108. package/dist/plugin.js +223 -25
  109. package/dist/post-common.d.ts +22 -2
  110. package/dist/post-common.js +82 -2
  111. package/dist/post-nodes.d.ts +7 -1
  112. package/dist/post-nodes.js +235 -111
  113. package/dist/post-plugin.js +5 -3
  114. package/dist/post-scene.d.ts +130 -0
  115. package/dist/post-scene.js +164 -0
  116. package/dist/post-shaders.js +85 -62
  117. package/dist/post.js +11 -1
  118. package/dist/projection.d.ts +24 -0
  119. package/dist/projection.js +87 -0
  120. package/dist/readback.d.ts +1 -0
  121. package/dist/readback.js +1 -0
  122. package/dist/render-scale.js +1 -0
  123. package/dist/ribbons-plugin.d.ts +5 -0
  124. package/dist/ribbons-plugin.js +59 -0
  125. package/dist/ribbons.d.ts +44 -0
  126. package/dist/ribbons.js +288 -0
  127. package/dist/scenario.d.ts +70 -0
  128. package/dist/scenario.js +125 -0
  129. package/dist/screen-effects.d.ts +97 -0
  130. package/dist/screen-effects.js +192 -0
  131. package/dist/shaders.d.ts +13 -0
  132. package/dist/shaders.js +189 -34
  133. package/dist/shadow-catcher.d.ts +9 -0
  134. package/dist/shadow-catcher.js +133 -0
  135. package/dist/shadows.d.ts +27 -2
  136. package/dist/shadows.js +100 -20
  137. package/dist/standard-fields.d.ts +1 -0
  138. package/dist/standard-fields.js +5 -0
  139. package/dist/standard.js +18 -0
  140. package/dist/stats.d.ts +45 -1
  141. package/dist/stats.js +91 -3
  142. package/dist/surface-model.d.ts +57 -0
  143. package/dist/surface-model.js +289 -0
  144. package/dist/surface-shaders.d.ts +3 -0
  145. package/dist/surface-shaders.js +160 -0
  146. package/dist/surface.d.ts +40 -0
  147. package/dist/surface.js +86 -0
  148. package/dist/target.d.ts +1 -13
  149. package/dist/target.js +1 -26
  150. package/dist/testing.d.ts +11 -0
  151. package/dist/testing.js +39 -1
  152. package/dist/tier.d.ts +13 -0
  153. package/dist/tier.js +14 -0
  154. package/dist/timer.d.ts +18 -1
  155. package/dist/timer.js +73 -20
  156. package/dist/variants.d.ts +37 -0
  157. package/dist/variants.js +104 -0
  158. package/dist/vfx-draw.d.ts +37 -0
  159. package/dist/vfx-draw.js +291 -0
  160. package/dist/vfx-state.d.ts +42 -0
  161. package/dist/vfx-state.js +81 -0
  162. package/dist/vfx.d.ts +69 -0
  163. package/dist/vfx.js +103 -0
  164. package/dist/view-visibility-plugin.d.ts +61 -0
  165. package/dist/view-visibility-plugin.js +267 -0
  166. package/dist/view-visibility.d.ts +28 -0
  167. package/dist/view-visibility.js +46 -0
  168. package/dist/view.d.ts +29 -1
  169. package/dist/view.js +57 -34
  170. package/package.json +23 -11
package/dist/culling.js CHANGED
@@ -7,8 +7,9 @@ import {
7
7
  SLOT_MASK
8
8
  } from "./instances.js";
9
9
  import { Shaders } from "./plugin.js";
10
+ import { NO_HIDDEN } from "./view-visibility.js";
10
11
  const ARGS_WORDS = 5;
11
- const VIEW_FLOATS = 32;
12
+ const VIEW_FLOATS = 36;
12
13
  const BATCH_WORDS = 8;
13
14
  const LOD_WORDS = 20;
14
15
  const VIEW_CASTERS = 1;
@@ -21,6 +22,8 @@ class GpuCuller {
21
22
  supported;
22
23
  /** Turn off to force CPU culling (tests, debugging). */
23
24
  enabled = true;
25
+ /** Views this frame with a hidden set (0070): the cull links its HIDDEN variant. */
26
+ hiddenViews = 0;
24
27
  views = [];
25
28
  viewCount = 0;
26
29
  cameraViews = 0;
@@ -59,7 +62,7 @@ class GpuCuller {
59
62
  pending = /* @__PURE__ */ new Set();
60
63
  constructor(gpu) {
61
64
  this.gpu = gpu;
62
- this.supported = gpu.features.has("indirect-first-instance");
65
+ this.supported = gpu.tier === "full" && gpu.features.has("indirect-first-instance");
63
66
  const storage = GPUBufferUsage.STORAGE;
64
67
  this.viewBuffer = new GpuBuffer(gpu, { label: "cull/views", usage: storage, size: 4096 });
65
68
  this.batchBuffer = new GpuBuffer(gpu, { label: "cull/batches", usage: storage, size: 2048 });
@@ -82,6 +85,7 @@ class GpuCuller {
82
85
  beginFrame() {
83
86
  this.viewCount = 0;
84
87
  this.cameraViews = 0;
88
+ this.hiddenViews = 0;
85
89
  }
86
90
  /** The LOD state row of a camera (cameras choose; their shadow views reuse the choice). */
87
91
  lodCamera(entity) {
@@ -110,7 +114,9 @@ class GpuCuller {
110
114
  hasEye: false,
111
115
  lodScale: 1,
112
116
  flags: 0,
113
- lodCamera: 0
117
+ lodCamera: 0,
118
+ layers: 4294967295,
119
+ hiddenBase: NO_HIDDEN
114
120
  };
115
121
  this.views.push(view);
116
122
  }
@@ -122,6 +128,9 @@ class GpuCuller {
122
128
  if (params.eye) view.eye.set(params.eye);
123
129
  view.lodScale = params.lodScale;
124
130
  view.lodCamera = lodCamera;
131
+ view.layers = params.layers ?? 4294967295;
132
+ view.hiddenBase = params.hiddenBase ?? NO_HIDDEN;
133
+ if (view.hiddenBase !== NO_HIDDEN) this.hiddenViews++;
125
134
  view.flags = (params.require & InstanceFlags.Caster ? VIEW_CASTERS : 0) | (params.updateLod ? VIEW_UPDATE_LOD : 0) | (params.orthographic ? VIEW_ORTHO : 0) | (params.planes ? 0 : VIEW_NO_PLANES);
126
135
  if (params.updateLod) this.cameraViews = v + 1;
127
136
  list.cullView = v;
@@ -136,7 +145,8 @@ class GpuCuller {
136
145
  let n = 0;
137
146
  let m = 0;
138
147
  for (const batch of store.sorted) {
139
- if (batch.count === 0 || batch.transparent || !store.anyVisible(batch)) continue;
148
+ if (batch.count === 0 || batch.transparent || batch.ground || !store.anyVisible(batch))
149
+ continue;
140
150
  if (!batch.ready) {
141
151
  list.pending += batch.count;
142
152
  continue;
@@ -157,9 +167,22 @@ class GpuCuller {
157
167
  if (forwardOnly) forwardOnly.length = m;
158
168
  return v;
159
169
  }
160
- /** Uploads the frame's view, batch, and LOD tables, and sizes the buffers. */
161
- prepare(store) {
170
+ /**
171
+ * Uploads the frame's view, batch, and LOD tables, and sizes the buffers. Hidden sets (0070)
172
+ * ride in the LOD state buffer after its entries: the cull's storage bindings are at WebGPU's
173
+ * guaranteed 8 per stage.
174
+ */
175
+ prepare(store, hidden) {
162
176
  if (this.viewCount === 0) return;
177
+ const lodCams = Math.max(1, this.lodCameras.size);
178
+ const hiddenWords = hidden && hidden.sets.size > 0 ? hidden.data.length : 0;
179
+ if (this.lodStateCapacity !== store.capacity * lodCams || this.lodState.byteLength < (this.lodStateCapacity + hiddenWords) * 4) {
180
+ this.lodStateCapacity = store.capacity * lodCams;
181
+ this.lodState.ensureCapacity((this.lodStateCapacity + hiddenWords) * 4);
182
+ const fill = new Uint32Array(this.lodStateCapacity).fill(LOD_UNSET);
183
+ this.lodState.write(fill);
184
+ }
185
+ if (hiddenWords > 0) hidden.upload(this.lodState, this.lodStateCapacity * 4);
163
186
  const batches = store.batches;
164
187
  this.batchCount = batches.length;
165
188
  if (this.batchData.length < batches.length * BATCH_WORDS) {
@@ -182,7 +205,7 @@ class GpuCuller {
182
205
  this.batchData[o + 4] = batch.mesh.drawCount;
183
206
  this.batchData[o + 5] = batch.mesh.indexed ? 1 + batch.mesh.baseVertex : 0;
184
207
  this.batchData[o + 6] = batch.region;
185
- this.batchData[o + 7] = (batch.transparent ? BATCH_TRANSPARENT : 0) | (batch.ready ? 0 : BATCH_NOT_READY);
208
+ this.batchData[o + 7] = (batch.transparent || batch.ground ? BATCH_TRANSPARENT : 0) | (batch.ready ? 0 : BATCH_NOT_READY);
186
209
  }
187
210
  this.regionSize = Math.max(1, region);
188
211
  this.batchBuffer.write(this.batchData, 0, 0, Math.max(1, batches.length) * BATCH_WORDS);
@@ -220,6 +243,8 @@ class GpuCuller {
220
243
  this.viewU32[o + 29] = view.lodCamera;
221
244
  this.viewU32[o + 30] = v * batches.length;
222
245
  this.viewU32[o + 31] = v * this.regionSize;
246
+ this.viewU32[o + 32] = view.layers;
247
+ this.viewU32[o + 33] = view.hiddenBase === NO_HIDDEN ? NO_HIDDEN : this.lodStateCapacity + view.hiddenBase;
223
248
  for (const list of [view.list, view.forwardOnly]) {
224
249
  if (!list) continue;
225
250
  for (let d = 0; d < list.length; d++) {
@@ -232,12 +257,6 @@ class GpuCuller {
232
257
  this.viewBuffer.write(this.viewData, 0, 0, this.viewCount * VIEW_FLOATS);
233
258
  this.args.ensureCapacity(Math.max(1, this.viewCount * batches.length) * ARGS_WORDS * 4);
234
259
  this.visible.ensureCapacity(this.viewCount * this.regionSize * 4);
235
- const lodCams = Math.max(1, this.lodCameras.size);
236
- if (this.lodStateCapacity !== store.capacity * lodCams) {
237
- this.lodStateCapacity = store.capacity * lodCams;
238
- const fill = new Uint32Array(this.lodStateCapacity).fill(LOD_UNSET);
239
- this.lodState.write(fill);
240
- }
241
260
  this.frameViews = this.views.slice(0, this.viewCount);
242
261
  this.slotCount = store.high;
243
262
  this.slotCapacity = store.capacity;
@@ -249,7 +268,8 @@ class GpuCuller {
249
268
  if (this.viewCount === 0) return;
250
269
  const gpu = ctx.gpu;
251
270
  const world = ctx.world;
252
- const module = world.resource(Shaders).module(gpu, { root: "shard::cull" });
271
+ const hidden = this.hiddenViews > 0;
272
+ const module = world.resource(Shaders).module(gpu, hidden ? HIDDEN_REQUEST : CULL_REQUEST);
253
273
  if (!module) {
254
274
  gpu.pipelines.skipped++;
255
275
  return;
@@ -257,12 +277,12 @@ class GpuCuller {
257
277
  const layout = cullLayout(gpu);
258
278
  const pipelineLayout = gpu.layouts.pipelineLayout({ label: "cull", bindGroupLayouts: [layout] });
259
279
  const reset = gpu.pipelines.compute({
260
- label: "cull/reset",
280
+ label: hidden ? "cull/reset/hidden" : "cull/reset",
261
281
  layout: pipelineLayout,
262
282
  compute: { module, entryPoint: "reset" }
263
283
  });
264
284
  const cull = gpu.pipelines.compute({
265
- label: "cull/instances",
285
+ label: hidden ? "cull/instances/hidden" : "cull/instances",
266
286
  layout: pipelineLayout,
267
287
  compute: { module, entryPoint: "cull" }
268
288
  });
@@ -309,7 +329,7 @@ class GpuCuller {
309
329
  label: "cull",
310
330
  layout,
311
331
  entries: [
312
- { binding: 0, resource: { buffer: store.instanceBuffer.buffer } },
332
+ { binding: 0, resource: { buffer: store.instanceBuffer.gpuBuffer } },
313
333
  { binding: 1, resource: { buffer: this.batchBuffer.buffer } },
314
334
  { binding: 2, resource: { buffer: this.lodBuffer.buffer } },
315
335
  { binding: 3, resource: { buffer: this.viewBuffer.buffer } },
@@ -317,7 +337,7 @@ class GpuCuller {
317
337
  { binding: 5, resource: { buffer: this.visible.buffer } },
318
338
  { binding: 6, resource: { buffer: this.lodState.buffer } },
319
339
  { binding: 7, resource: { buffer: params.buffer } },
320
- { binding: 8, resource: { buffer: store.deform.recordBuffer.buffer } }
340
+ { binding: 8, resource: { buffer: store.deform.recordBuffer.gpuBuffer } }
321
341
  ]
322
342
  })
323
343
  )
@@ -434,6 +454,8 @@ class GpuCuller {
434
454
  while (this.pending.size > 0) await Promise.all(this.pending);
435
455
  }
436
456
  }
457
+ const CULL_REQUEST = { root: "shard::cull" };
458
+ const HIDDEN_REQUEST = { root: "shard::cull", defines: { HIDDEN: true } };
437
459
  function cullLayout(gpu) {
438
460
  const C = GPUShaderStage.COMPUTE;
439
461
  const read = (binding) => ({
@@ -467,6 +489,9 @@ const Culler = defineResource("render/Culler", {
467
489
  function cullTransparent(store, list, params, forward) {
468
490
  store.cullTransparentMembers(list, params, forward);
469
491
  }
492
+ function cullGround(store, list, params, overlay) {
493
+ store.cullGroundMembers(list, params, overlay);
494
+ }
470
495
  function visibleSlots(store, list, mask = SLOT_MASK) {
471
496
  const out = /* @__PURE__ */ new Set();
472
497
  for (let d = 0; d < list.length; d++) {
@@ -492,6 +517,7 @@ async function readVisibleSlots(gpu, culler, list, mask = SLOT_MASK) {
492
517
  export {
493
518
  Culler,
494
519
  GpuCuller,
520
+ cullGround,
495
521
  cullTransparent,
496
522
  readVisibleSlots,
497
523
  visibleSlots
@@ -0,0 +1,48 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { GpuBuffer } from '@aethervtt/shard-gpu';
3
+ export declare const CUTAWAY_SHADERS: Record<string, string>;
4
+ /** A camera's reveal points as uploaded, and the bind group over them. */
5
+ interface CutawayCamera {
6
+ data: Float32Array;
7
+ buffer: GpuBuffer | undefined;
8
+ bindGroup: GPUBindGroup | undefined;
9
+ /** The device generation and buffer version the bind group was made for. */
10
+ boundGeneration: number;
11
+ boundVersion: number;
12
+ /** Points in use (at most MAX_REVEAL_POINTS). */
13
+ count: number;
14
+ /** Points the component listed. */
15
+ listed: number;
16
+ frame: number;
17
+ /** Too many points was reported for this camera. */
18
+ warned: boolean;
19
+ /** The device generation its buffer was last written on (a new device loses the contents). */
20
+ written: number;
21
+ /** Per batch index: 1 when a point can cut one of its Cutaway instances this frame. */
22
+ cuts: Uint8Array;
23
+ }
24
+ export interface CutawayState {
25
+ cameras: Map<Entity, CutawayCamera>;
26
+ frame: number;
27
+ layout: {
28
+ layout: GPUBindGroupLayout;
29
+ generation: number;
30
+ } | undefined;
31
+ }
32
+ export declare const CutawayCameras: import("@aethervtt/shard-core").ResourceDef<CutawayState>;
33
+ /**
34
+ * Packs each camera's CutawayView into its uniform, with each point's line of sight from the
35
+ * camera. Only a camera whose values changed uploads (a camera moving changes the lines); a still
36
+ * frame writes nothing and allocates nothing.
37
+ */
38
+ export declare const extractCutaways: import("@aethervtt/shard-core").SystemDef<{
39
+ q: import("@aethervtt/shard-core").Query;
40
+ }>;
41
+ /** render.describe's cutaways section: per camera, its reveal points, radius, margin and edge. */
42
+ export declare function describeCutaways(world: World): Record<string, unknown>;
43
+ /**
44
+ * Cutaways (0070): Cutaway renderables open around a camera's CutawayView points, in every camera
45
+ * pass and no shadow pass. Cameras without reveal points draw the plain variants.
46
+ */
47
+ export declare const cutawayPlugin: import("@aethervtt/shard-runtime").Plugin;
48
+ export {};
@@ -0,0 +1,364 @@
1
+ import { defineResource, defineSystem, Last } from "@aethervtt/shard-core";
2
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
3
+ import { definePlugin, LogResource } from "@aethervtt/shard-runtime";
4
+ import { Camera3d } from "./camera.js";
5
+ import { CutawayPath, CutawayView, MAX_REVEAL_POINTS } from "./cutaway.js";
6
+ import { addRenderFeatures } from "./features.js";
7
+ import {
8
+ INSTANCE_FLOATS,
9
+ InstanceFlags,
10
+ Instances,
11
+ prepareInstances
12
+ } from "./instances.js";
13
+ import { Gpu, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
14
+ import { registerShaders } from "./shaders.js";
15
+ import { Cameras } from "./view.js";
16
+ const CUTAWAY_FLOATS = MAX_REVEAL_POINTS * 8 + 4;
17
+ const DIRS = MAX_REVEAL_POINTS * 4;
18
+ const PARAMS = MAX_REVEAL_POINTS * 8;
19
+ const CUTAWAY_SHADERS = {
20
+ "shard::cutaway": `
21
+ struct Cutaway {
22
+ /** Reveal points (xyz). */
23
+ points: array<vec4f, ${MAX_REVEAL_POINTS}>,
24
+ /**
25
+ * Each point's line of sight, unit (xyz): from the eye through it, or the view direction when
26
+ * orthographic. Made on the CPU once a frame, not per fragment.
27
+ */
28
+ dirs: array<vec4f, ${MAX_REVEAL_POINTS}>,
29
+ /** radius, margin, edge (m), and how many points. */
30
+ params: vec4f,
31
+ }
32
+
33
+ @group(3) @binding(0) var<uniform> cutaway: Cutaway;
34
+
35
+ /** InstanceFlags.Cutaway in the record's flags word. */
36
+ const CUTAWAY_FLAG: u32 = 0x10000000u;
37
+
38
+ /** 0 to 1 across an edge band of width \`edge\` ending at 0 (a step when edge is 0). */
39
+ fn cutaway_ramp(inside: f32, edge: f32) -> f32 {
40
+ if (edge <= 0.0) { return select(0.0, 1.0, inside > 0.0); }
41
+ return clamp(inside / edge, 0.0, 1.0);
42
+ }
43
+
44
+ /**
45
+ * How cut a world point is, 0 (whole) to 1: within radius of the line from the eye through a
46
+ * reveal point (along the view direction when orthographic), and nearer the camera than the point
47
+ * by more than margin. The edge band ramps both.
48
+ */
49
+ fn cutaway_amount(world: vec3f) -> f32 {
50
+ let radius = cutaway.params.x;
51
+ let margin = cutaway.params.y;
52
+ let edge = cutaway.params.z;
53
+ let count = u32(cutaway.params.w);
54
+ var amount = 0.0;
55
+ for (var i = 0u; i < count; i++) {
56
+ let d = world - cutaway.points[i].xyz;
57
+ let along = dot(d, cutaway.dirs[i].xyz);
58
+ // Not in front of the point by the margin, or outside the radius: this point cuts nothing.
59
+ let ahead = -along - margin;
60
+ if (ahead <= 0.0) { continue; }
61
+ let across2 = max(dot(d, d) - along * along, 0.0);
62
+ if (across2 >= radius * radius) { continue; }
63
+ amount = max(amount, min(cutaway_ramp(radius - sqrt(across2), edge), cutaway_ramp(ahead, edge)));
64
+ if (amount >= 1.0) { break; }
65
+ }
66
+ return amount;
67
+ }
68
+
69
+ /** An ordered 4\xD74 dither threshold at a pixel, in (0, 1). */
70
+ fn cutaway_threshold(frag: vec2f) -> f32 {
71
+ let x = u32(frag.x);
72
+ let y = u32(frag.y);
73
+ let low = (((x ^ y) & 1u) << 1u) | (y & 1u);
74
+ let high = ((((x ^ y) >> 1u) & 1u) << 1u) | ((y >> 1u) & 1u);
75
+ return (f32(low * 4u + high) + 0.5) / 16.0;
76
+ }
77
+
78
+ /** Removes the fragment of a Cutaway instance that stands in front of a reveal point. */
79
+ fn cutaway_clip(world: vec3f, frag: vec2f, flags: u32) {
80
+ if ((flags & CUTAWAY_FLAG) == 0u) { return; }
81
+ if (cutaway_amount(world) > cutaway_threshold(frag)) { discard; }
82
+ }`
83
+ };
84
+ const CutawayCameras = defineResource("render/CutawayCameras", {
85
+ description: "Each camera's reveal points as the cutaway variants read them (0070).",
86
+ init: () => ({ cameras: /* @__PURE__ */ new Map(), frame: 0, layout: void 0 })
87
+ });
88
+ function cameraOf(state, camera) {
89
+ let c = state.cameras.get(camera);
90
+ if (!c) {
91
+ c = {
92
+ data: new Float32Array(CUTAWAY_FLOATS),
93
+ buffer: void 0,
94
+ bindGroup: void 0,
95
+ boundGeneration: -1,
96
+ boundVersion: -1,
97
+ count: -1,
98
+ listed: 0,
99
+ frame: -1,
100
+ warned: false,
101
+ written: -1,
102
+ cuts: new Uint8Array(64)
103
+ };
104
+ state.cameras.set(camera, c);
105
+ }
106
+ return c;
107
+ }
108
+ function warnTooMany(world, camera, listed) {
109
+ world.tryResource(LogResource)?.log(
110
+ "warn",
111
+ `Camera ${camera} has ${listed} reveal points; only the first ${MAX_REVEAL_POINTS} cut`,
112
+ {
113
+ code: "render/too-many-reveal-points",
114
+ hint: `Give CutawayView at most ${MAX_REVEAL_POINTS} points: the party, or the selected tokens.`,
115
+ data: { camera, points: listed }
116
+ }
117
+ );
118
+ }
119
+ function put(d, o, v) {
120
+ if (d[o] === Math.fround(v)) return false;
121
+ d[o] = v;
122
+ return true;
123
+ }
124
+ const extractCutaways = defineSystem({
125
+ name: "render/extract-cutaways",
126
+ description: "Packs cameras' reveal points for the cutaway variants (0070).",
127
+ setup: (world) => ({ q: world.query({ with: [Camera3d, CutawayView] }) }),
128
+ run: ({ q }, world) => {
129
+ const state = world.resource(CutawayCameras);
130
+ const gpu = world.resource(Gpu);
131
+ const cameras = world.resource(Cameras);
132
+ const store = world.resource(Instances);
133
+ const frame = ++state.frame;
134
+ for (const table of q.tables) {
135
+ const points = table.column(CutawayView, "points");
136
+ const radius = table.column(CutawayView, "radius");
137
+ const margin = table.column(CutawayView, "margin");
138
+ const edge = table.column(CutawayView, "edge");
139
+ for (let i = 0; i < table.count; i++) {
140
+ const camera = table.entities[i];
141
+ const c = cameraOf(state, camera);
142
+ c.frame = frame;
143
+ const list = points[i] ?? EMPTY;
144
+ const listed = list.length;
145
+ const count = Math.min(listed, MAX_REVEAL_POINTS);
146
+ if (listed > MAX_REVEAL_POINTS && !c.warned) {
147
+ c.warned = true;
148
+ warnTooMany(world, camera, listed);
149
+ }
150
+ c.listed = listed;
151
+ const cam = cameras.get(camera);
152
+ const n = cam ? count : 0;
153
+ const d = c.data;
154
+ let changed = c.count !== n;
155
+ for (let k = 0; k < n; k++) {
156
+ const p = list[k];
157
+ const o = k * 4;
158
+ changed = put(d, o, p[0]) || changed;
159
+ changed = put(d, o + 1, p[1]) || changed;
160
+ changed = put(d, o + 2, p[2]) || changed;
161
+ const f = cam.forward;
162
+ let x = f[0];
163
+ let y = f[1];
164
+ let z = f[2];
165
+ if (!cam.orthographic) {
166
+ const e = cam.position;
167
+ const dx = p[0] - e[0];
168
+ const dy = p[1] - e[1];
169
+ const dz = p[2] - e[2];
170
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
171
+ if (len > 1e-5) {
172
+ x = dx / len;
173
+ y = dy / len;
174
+ z = dz / len;
175
+ }
176
+ }
177
+ changed = put(d, DIRS + o, x) || changed;
178
+ changed = put(d, DIRS + o + 1, y) || changed;
179
+ changed = put(d, DIRS + o + 2, z) || changed;
180
+ }
181
+ changed = put(d, PARAMS, radius[i]) || changed;
182
+ changed = put(d, PARAMS + 1, margin[i]) || changed;
183
+ changed = put(d, PARAMS + 2, edge[i]) || changed;
184
+ changed = put(d, PARAMS + 3, n) || changed;
185
+ c.count = n;
186
+ if (n > 0) markCuts(c, store);
187
+ if (n > 0 && (changed || c.written !== gpu.generation)) {
188
+ c.written = gpu.generation;
189
+ c.buffer ??= new GpuBuffer(gpu, {
190
+ label: `camera:${camera}/cutaway`,
191
+ usage: GPUBufferUsage.UNIFORM,
192
+ size: CUTAWAY_FLOATS * 4
193
+ });
194
+ c.buffer.write(d);
195
+ }
196
+ }
197
+ }
198
+ let live = 0;
199
+ for (const table of q.tables) live += table.count;
200
+ if (state.cameras.size > live) {
201
+ for (const [camera, c] of state.cameras) {
202
+ if (c.frame === frame) continue;
203
+ c.buffer?.destroy();
204
+ state.cameras.delete(camera);
205
+ }
206
+ }
207
+ }
208
+ });
209
+ const EMPTY = [];
210
+ const box = new Float32Array(6);
211
+ function slotBox(f, o, b) {
212
+ const cx = (b[0] + b[3]) * 0.5;
213
+ const cy = (b[1] + b[4]) * 0.5;
214
+ const cz = (b[2] + b[5]) * 0.5;
215
+ const ex = (b[3] - b[0]) * 0.5;
216
+ const ey = (b[4] - b[1]) * 0.5;
217
+ const ez = (b[5] - b[2]) * 0.5;
218
+ for (let r = 0; r < 3; r++) {
219
+ const m = o + r * 4;
220
+ box[r] = f[m] * cx + f[m + 1] * cy + f[m + 2] * cz + f[m + 3];
221
+ box[3 + r] = Math.abs(f[m]) * ex + Math.abs(f[m + 1]) * ey + Math.abs(f[m + 2]) * ez;
222
+ }
223
+ }
224
+ function reaches(d, k, radius, margin) {
225
+ const o = k * 4;
226
+ let t0 = 0;
227
+ let t1 = Number.POSITIVE_INFINITY;
228
+ for (let a = 0; a < 3; a++) {
229
+ const dir = -d[DIRS + o + a];
230
+ const start = d[o + a] + dir * margin;
231
+ const lo = box[a] - box[3 + a] - radius;
232
+ const hi = box[a] + box[3 + a] + radius;
233
+ if (Math.abs(dir) < 1e-9) {
234
+ if (start < lo || start > hi) return false;
235
+ continue;
236
+ }
237
+ let near = (lo - start) / dir;
238
+ let far = (hi - start) / dir;
239
+ if (near > far) {
240
+ const t = near;
241
+ near = far;
242
+ far = t;
243
+ }
244
+ if (near > t0) t0 = near;
245
+ if (far < t1) t1 = far;
246
+ if (t0 > t1) return false;
247
+ }
248
+ return true;
249
+ }
250
+ function markCuts(c, store) {
251
+ const batches = store.batches;
252
+ if (c.cuts.length < batches.length) c.cuts = new Uint8Array(batches.length * 2);
253
+ const cuts = c.cuts;
254
+ const d = c.data;
255
+ const n = d[PARAMS + 3];
256
+ const radius = d[PARAMS];
257
+ const margin = d[PARAMS + 1];
258
+ for (let b = 0; b < batches.length; b++) {
259
+ const batch = batches[b];
260
+ cuts[b] = 0;
261
+ if (batch.cutaway === 0) continue;
262
+ if (batch.memberCount < batch.count) {
263
+ cuts[b] = 1;
264
+ continue;
265
+ }
266
+ for (let m = 0; m < batch.memberCount && cuts[b] === 0; m++) {
267
+ const slot = batch.members[m];
268
+ const flags = store.flags[slot];
269
+ if ((flags & InstanceFlags.Cutaway) === 0) continue;
270
+ if (flags & InstanceFlags.Skinned) {
271
+ cuts[b] = 1;
272
+ break;
273
+ }
274
+ slotBox(store.f32, slot * INSTANCE_FLOATS, batch.mesh.bounds);
275
+ for (let k = 0; k < n; k++) {
276
+ if (reaches(d, k, radius, margin)) {
277
+ cuts[b] = 1;
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+ function support(world) {
285
+ const state = world.resource(CutawayCameras);
286
+ const layout = (gpu) => {
287
+ if (!state.layout || state.layout.generation !== gpu.generation) {
288
+ state.layout = {
289
+ layout: gpu.layouts.bindGroupLayout({
290
+ label: "cutaway",
291
+ entries: [
292
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }
293
+ ]
294
+ }),
295
+ generation: gpu.generation
296
+ };
297
+ }
298
+ return state.layout.layout;
299
+ };
300
+ return {
301
+ layout,
302
+ cuts(camera, batch) {
303
+ const c = state.cameras.get(camera);
304
+ return c !== void 0 && c.frame === state.frame && c.count > 0 && c.cuts[batch] === 1;
305
+ },
306
+ bindGroup(gpu, camera) {
307
+ const c = state.cameras.get(camera);
308
+ if (!c || c.frame !== state.frame || c.count <= 0 || !c.buffer) return void 0;
309
+ const buffer = c.buffer;
310
+ const gpuBuffer = buffer.buffer;
311
+ if (!c.bindGroup || c.boundGeneration !== gpu.generation || c.boundVersion !== buffer.version) {
312
+ c.bindGroup = gpu.device.createBindGroup({
313
+ label: "cutaway",
314
+ layout: layout(gpu),
315
+ entries: [{ binding: 0, resource: { buffer: gpuBuffer } }]
316
+ });
317
+ c.boundGeneration = gpu.generation;
318
+ c.boundVersion = buffer.version;
319
+ }
320
+ return c.bindGroup;
321
+ }
322
+ };
323
+ }
324
+ function describeCutaways(world) {
325
+ const out = {};
326
+ world.query({ with: [Camera3d, CutawayView] }).each((camera, row, table) => {
327
+ const points = table.column(CutawayView, "points")[row] ?? [];
328
+ out[`camera:${camera}`] = {
329
+ points: Array.from(points.slice(0, MAX_REVEAL_POINTS), (p) => [p[0], p[1], p[2]]),
330
+ ignored: Math.max(0, points.length - MAX_REVEAL_POINTS),
331
+ radius: table.column(CutawayView, "radius")[row],
332
+ margin: table.column(CutawayView, "margin")[row],
333
+ edge: table.column(CutawayView, "edge")[row]
334
+ };
335
+ });
336
+ return out;
337
+ }
338
+ const cutawayPlugin = definePlugin({
339
+ name: "render/cutaway",
340
+ dependencies: ["render/forward"],
341
+ provides: [CutawayCameras],
342
+ build(app) {
343
+ app.world.initResource(CutawayCameras);
344
+ app.insertResource(CutawayPath, support(app.world));
345
+ app.addSystems(Last, extractCutaways.inSet(RenderSet.Prepare).after(prepareInstances));
346
+ },
347
+ ready(app) {
348
+ registerShaders(app.world.resource(Shaders), CUTAWAY_SHADERS);
349
+ app.world.initResource(RenderDescribers).set("cutaways", describeCutaways);
350
+ addRenderFeatures(app.world, {
351
+ name: "render/cutaway",
352
+ description: "Cutaway surfaces opened around cameras' reveal points.",
353
+ nodes: [],
354
+ baseline: { strategy: "The same discard; points in a uniform block" }
355
+ });
356
+ }
357
+ });
358
+ export {
359
+ CUTAWAY_SHADERS,
360
+ CutawayCameras,
361
+ cutawayPlugin,
362
+ describeCutaways,
363
+ extractCutaways
364
+ };
@@ -0,0 +1,27 @@
1
+ import { type Entity } from '@aethervtt/shard-core';
2
+ import type { GpuContext } from '@aethervtt/shard-gpu';
3
+ /** Reveal points a camera can have; more are ignored with `render/too-many-reveal-points`. */
4
+ export declare const MAX_REVEAL_POINTS = 16;
5
+ export declare const Cutaway: import("@aethervtt/shard-core").ComponentDef<{}>;
6
+ export declare const CutawayView: import("@aethervtt/shard-core").ComponentDef<{
7
+ readonly points: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3[], "object">;
8
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
9
+ readonly margin: import("@aethervtt/shard-core").FieldType<number, "f32">;
10
+ readonly edge: import("@aethervtt/shard-core").FieldType<number, "f32">;
11
+ }>;
12
+ /**
13
+ * Set by `cutawayPlugin`: what the camera passes need to draw the cutaway variants of pipelines.
14
+ * Forward asks it per view, so core imports no cutaway code.
15
+ */
16
+ export interface CutawaySupport {
17
+ /** The bind group layout cutaway pipelines take at group 3. */
18
+ layout(gpu: GpuContext): GPUBindGroupLayout;
19
+ /** The camera's reveal points, bound at group 3: undefined when it has none this frame. */
20
+ bindGroup(gpu: GpuContext, camera: Entity): GPUBindGroup | undefined;
21
+ /**
22
+ * Whether a point of the camera can cut an instance of the batch this frame: its bounds reach a
23
+ * reveal cylinder. The others draw the plain variant (no discard, so no per-fragment cost).
24
+ */
25
+ cuts(camera: Entity, batch: number): boolean;
26
+ }
27
+ export declare const CutawayPath: import("@aethervtt/shard-core").ResourceDef<CutawaySupport>;
@@ -0,0 +1,49 @@
1
+ import { defineComponent, defineResource, t } from "@aethervtt/shard-core";
2
+ import { Camera3d } from "./camera.js";
3
+ const MAX_REVEAL_POINTS = 16;
4
+ const Cutaway = defineComponent(
5
+ "render/Cutaway",
6
+ {},
7
+ {
8
+ description: "Tag on a renderable: it may be cut away around a camera's reveal points (CutawayView). Shadows never cut."
9
+ }
10
+ );
11
+ const CutawayView = defineComponent(
12
+ "render/CutawayView",
13
+ {
14
+ points: t.list(t.vec3, {
15
+ description: `World points to reveal, at most ${MAX_REVEAL_POINTS}: a token's position, the party.`
16
+ }),
17
+ radius: t.f32({
18
+ default: 2.5,
19
+ min: 0,
20
+ unit: "m",
21
+ description: "How far from each point's line of sight a Cutaway surface is cut."
22
+ }),
23
+ margin: t.f32({
24
+ default: 0.6,
25
+ min: 0,
26
+ unit: "m",
27
+ description: "How far in front of a point the cut starts, so the floor it stands on, and a stairwell rim, stay."
28
+ }),
29
+ edge: t.f32({
30
+ default: 0.3,
31
+ min: 0,
32
+ unit: "m",
33
+ description: "Width of the dithered rim; 0 is a hard cut."
34
+ })
35
+ },
36
+ {
37
+ description: "Cuts Cutaway surfaces away between this camera and its reveal points (0070): a disc in a roof from above, a hole in a wall at an angle. Every camera pass (picking too) sees the hole; shadows do not.",
38
+ requires: [Camera3d]
39
+ }
40
+ );
41
+ const CutawayPath = defineResource("render/CutawayPath", {
42
+ description: "Present when cutaways (cutawayPlugin) are installed."
43
+ });
44
+ export {
45
+ Cutaway,
46
+ CutawayPath,
47
+ CutawayView,
48
+ MAX_REVEAL_POINTS
49
+ };