@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/testing.js CHANGED
@@ -3,6 +3,10 @@ import { dirname, join } from "node:path";
3
3
  import { deflateSync } from "node:zlib";
4
4
  import { Culler } from "./culling.js";
5
5
  import { captureView, Gpu, Shaders } from "./plugin.js";
6
+ import {
7
+ expectScenario,
8
+ measureScenario
9
+ } from "./scenario.js";
6
10
  async function settle(app, frames = 40) {
7
11
  const gpu = app.world.resource(Gpu);
8
12
  for (let i = 0; i < frames; i++) {
@@ -116,11 +120,45 @@ function compareGolden(dir, name, image) {
116
120
  }
117
121
  return { mean: sum / expected.length, max, written: false };
118
122
  }
123
+ function watchBaseline(gpu) {
124
+ const found = { computePasses: 0, renderStorage: [] };
125
+ const device = gpu.device;
126
+ const createEncoder = device.createCommandEncoder.bind(device);
127
+ device.createCommandEncoder = (descriptor) => {
128
+ const encoder = createEncoder(descriptor);
129
+ const begin = encoder.beginComputePass.bind(encoder);
130
+ encoder.beginComputePass = (d) => {
131
+ found.computePasses++;
132
+ return begin(d);
133
+ };
134
+ return encoder;
135
+ };
136
+ const createLayout = device.createBindGroupLayout.bind(device);
137
+ device.createBindGroupLayout = (descriptor) => {
138
+ for (const e of descriptor.entries) {
139
+ const render = e.visibility & (GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
140
+ const t = e.buffer?.type;
141
+ if (render && (t === "storage" || t === "read-only-storage"))
142
+ found.renderStorage.push(`${descriptor.label ?? "?"}#${e.binding}`);
143
+ }
144
+ return createLayout(descriptor);
145
+ };
146
+ return found;
147
+ }
148
+ function meanDifference(a, b) {
149
+ let sum = 0;
150
+ for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
151
+ return sum / a.length;
152
+ }
119
153
  export {
120
154
  compareGolden,
121
155
  comparePng,
156
+ expectScenario,
157
+ meanDifference,
158
+ measureScenario,
122
159
  pixel,
123
160
  pngBytes,
124
161
  renderView,
125
- settle
162
+ settle,
163
+ watchBaseline
126
164
  };
package/dist/tier.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { GpuContext } from '@aethervtt/shard-gpu';
2
+ /**
3
+ * A texture's binding dimension, fixed at creation on baseline (compatibility mode and WebGL2):
4
+ * spread into the texture descriptor. Nothing on the full tier.
5
+ */
6
+ export declare function bindingDimension(gpu: GpuContext, dimension: GPUTextureViewDimension): {
7
+ textureBindingViewDimension?: GPUTextureViewDimension;
8
+ };
9
+ /**
10
+ * The layout entry for a depth texture read without comparison. On baseline the shader rewrite
11
+ * reads it as a float texture (naga can't load from depth), so it binds as unfilterable float.
12
+ */
13
+ export declare function depthReadEntry(gpu: GpuContext, binding: number, visibility: GPUShaderStageFlags, viewDimension?: GPUTextureViewDimension): GPUBindGroupLayoutEntry;
package/dist/tier.js ADDED
@@ -0,0 +1,14 @@
1
+ function bindingDimension(gpu, dimension) {
2
+ return gpu.tier === "baseline" ? { textureBindingViewDimension: dimension } : {};
3
+ }
4
+ function depthReadEntry(gpu, binding, visibility, viewDimension = "2d") {
5
+ return {
6
+ binding,
7
+ visibility,
8
+ texture: gpu.tier === "baseline" ? { sampleType: "unfilterable-float", viewDimension } : viewDimension === "2d" ? { sampleType: "depth" } : { sampleType: "depth", viewDimension }
9
+ };
10
+ }
11
+ export {
12
+ bindingDimension,
13
+ depthReadEntry
14
+ };
package/dist/timer.d.ts CHANGED
@@ -11,6 +11,15 @@ export declare class GpuTimer {
11
11
  frameMs: number;
12
12
  /** Frames measured so far: changes when `frameMs` does. */
13
13
  frameSamples: number;
14
+ /**
15
+ * Every pass time so far was a whole number of 100 µs: the browser quantizes timestamps
16
+ * (0074's `gpuQuantized`). Decided after a few dozen passes.
17
+ */
18
+ quantized: boolean;
19
+ private passTimes;
20
+ private unquantized;
21
+ /** Each query slot's stamps at the last readback: a slot that still holds them is left over. */
22
+ private readonly lastStamps;
14
23
  private querySet;
15
24
  private resolveBuffer;
16
25
  private readonly readbacks;
@@ -18,11 +27,19 @@ export declare class GpuTimer {
18
27
  private generation;
19
28
  private readonly gpu;
20
29
  constructor(gpu: GpuContext);
30
+ /** Destroys the query set; buffers are released with the rest of the app's (0052). */
31
+ destroy(): void;
21
32
  beginFrame(): void;
22
33
  /** Timestamp writes for the next pass, or undefined when timing is off or full. */
23
34
  allocate(name: string): GPURenderPassTimestampWrites | undefined;
24
35
  private pendingCopy;
36
+ /** Pass `i`'s slot still holds the stamps it had at the last readback: the GPU skipped it. */
37
+ private stale;
25
38
  resolve(encoder: GPUCommandEncoder): void;
26
- /** Call after submit. Records timings when the copy lands. */
39
+ /**
40
+ * Call after submit. Records timings when the copy lands: aggregates per name, and (during a
41
+ * capture) GPU-track spans placed from this frame's submit time, since WebGPU's clock isn't the
42
+ * CPU's (0074).
43
+ */
27
44
  readback(world: World): void;
28
45
  }
package/dist/timer.js CHANGED
@@ -1,17 +1,36 @@
1
- import { ProfilerResource } from "@aethervtt/shard-core";
1
+ import { ProfilerResource, TRACK } from "@aethervtt/shard-core";
2
2
  const MAX_PASSES = 96;
3
- const STALE_NS = 1000000000n;
4
3
  const SPANS = [
5
4
  ["gpu:span/post", (n) => n.startsWith("post/") || n === "tonemap"],
6
5
  ["gpu:span/ssao", (n) => n === "ssao" || n.startsWith("ssao/")]
7
6
  ];
8
7
  const READBACK_BUFFERS = 3;
8
+ const QUANTUM_NS = 100000n;
9
+ const QUANTIZED_AFTER = 32;
10
+ const gpuNames = /* @__PURE__ */ new Map();
11
+ function gpuName(node) {
12
+ let name = gpuNames.get(node);
13
+ if (name === void 0) {
14
+ name = `gpu:${node}`;
15
+ gpuNames.set(node, name);
16
+ }
17
+ return name;
18
+ }
9
19
  class GpuTimer {
10
20
  enabled;
11
21
  /** The latest frame's GPU span in ms (first pass start to last pass end), once one landed. */
12
22
  frameMs = 0;
13
23
  /** Frames measured so far: changes when `frameMs` does. */
14
24
  frameSamples = 0;
25
+ /**
26
+ * Every pass time so far was a whole number of 100 µs: the browser quantizes timestamps
27
+ * (0074's `gpuQuantized`). Decided after a few dozen passes.
28
+ */
29
+ quantized = false;
30
+ passTimes = 0;
31
+ unquantized = false;
32
+ /** Each query slot's stamps at the last readback: a slot that still holds them is left over. */
33
+ lastStamps = new BigUint64Array(MAX_PASSES * 2);
15
34
  querySet;
16
35
  resolveBuffer;
17
36
  readbacks = [];
@@ -22,10 +41,17 @@ class GpuTimer {
22
41
  this.gpu = gpu;
23
42
  this.enabled = gpu.features.has("timestamp-query");
24
43
  }
44
+ /** Destroys the query set; buffers are released with the rest of the app's (0052). */
45
+ destroy() {
46
+ this.querySet?.destroy();
47
+ this.querySet = void 0;
48
+ this.generation = this.gpu.generation;
49
+ }
25
50
  beginFrame() {
26
51
  this.names = [];
27
52
  if (!this.enabled || this.generation === this.gpu.generation) return;
28
53
  this.generation = this.gpu.generation;
54
+ this.lastStamps.fill(0n);
29
55
  const device = this.gpu.device;
30
56
  this.querySet = device.createQuerySet({
31
57
  label: "gpu-timer",
@@ -46,7 +72,9 @@ class GpuTimer {
46
72
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
47
73
  }),
48
74
  busy: false,
49
- names: []
75
+ names: [],
76
+ frame: 0,
77
+ submitAt: 0
50
78
  });
51
79
  }
52
80
  }
@@ -62,6 +90,11 @@ class GpuTimer {
62
90
  };
63
91
  }
64
92
  pendingCopy;
93
+ /** Pass `i`'s slot still holds the stamps it had at the last readback: the GPU skipped it. */
94
+ stale(times, i) {
95
+ const last = this.lastStamps;
96
+ return times[i * 2] === last[i * 2] && times[i * 2 + 1] === last[i * 2 + 1];
97
+ }
65
98
  resolve(encoder) {
66
99
  this.pendingCopy = void 0;
67
100
  if (!this.enabled || this.names.length === 0) return;
@@ -74,36 +107,54 @@ class GpuTimer {
74
107
  target.names = this.names;
75
108
  this.pendingCopy = target;
76
109
  }
77
- /** Call after submit. Records timings when the copy lands. */
110
+ /**
111
+ * Call after submit. Records timings when the copy lands: aggregates per name, and (during a
112
+ * capture) GPU-track spans placed from this frame's submit time, since WebGPU's clock isn't the
113
+ * CPU's (0074).
114
+ */
78
115
  readback(world) {
116
+ const profiler = world.tryResource(ProfilerResource);
117
+ if (profiler) profiler.gpu.status = this.enabled ? "available" : "unavailable";
79
118
  const target = this.pendingCopy;
80
119
  if (!target) return;
81
120
  this.pendingCopy = void 0;
121
+ target.frame = profiler?.frame ?? 0;
122
+ target.submitAt = profiler ? profiler.now() : 0;
82
123
  const generation = this.generation;
83
124
  target.buffer.mapAsync(GPUMapMode.READ).then(
84
125
  () => {
85
126
  if (generation !== this.gpu.generation) return;
86
127
  const times = new BigUint64Array(target.buffer.getMappedRange());
87
- const profiler = world.tryResource(ProfilerResource);
88
128
  const totals = /* @__PURE__ */ new Map();
89
- let latest = 0n;
90
- for (let i = 0; i < target.names.length; i++) {
91
- const end = times[i * 2 + 1];
92
- if (end > latest) latest = end;
93
- }
94
- const stale = latest - STALE_NS;
129
+ const count = target.names.length * 2;
95
130
  let first = 0n;
96
- let last = 0n;
131
+ let lastEnd = 0n;
97
132
  for (let i = 0; i < target.names.length; i++) {
98
133
  const begin = times[i * 2];
99
134
  const end = times[i * 2 + 1];
100
- if (begin === 0n || end < begin || begin < stale) continue;
135
+ if (begin === 0n || end < begin || this.stale(times, i)) continue;
101
136
  const name = target.names[i];
102
137
  totals.set(name, (totals.get(name) ?? 0) + Number(end - begin) / 1e6);
103
138
  if (first === 0n || begin < first) first = begin;
104
- if (end > last) last = end;
139
+ if (end > lastEnd) lastEnd = end;
140
+ if (end > begin) {
141
+ if ((end - begin) % QUANTUM_NS !== 0n) this.unquantized = true;
142
+ this.passTimes++;
143
+ }
144
+ }
145
+ if (this.passTimes >= QUANTIZED_AFTER) this.quantized = !this.unquantized;
146
+ if (profiler) profiler.gpu.quantized = this.quantized;
147
+ for (const [name, ms] of totals) profiler?.sample(gpuName(name), ms);
148
+ const at = (stamp) => target.submitAt + Number(stamp - first) / 1e6;
149
+ if (profiler?.capturing && first !== 0n) {
150
+ for (let i = 0; i < target.names.length; i++) {
151
+ const begin = times[i * 2];
152
+ const end = times[i * 2 + 1];
153
+ if (begin === 0n || end < begin || this.stale(times, i)) continue;
154
+ const ms = Number(end - begin) / 1e6;
155
+ profiler.event(gpuName(target.names[i]), TRACK.gpu, at(begin), ms, target.frame);
156
+ }
105
157
  }
106
- for (const [name, ms] of totals) profiler?.record(`gpu:${name}`, ms);
107
158
  for (let g = 0; g < SPANS.length; g++) {
108
159
  const [label, match] = SPANS[g];
109
160
  let a = 0n;
@@ -111,17 +162,19 @@ class GpuTimer {
111
162
  for (let i = 0; i < target.names.length; i++) {
112
163
  const begin = times[i * 2];
113
164
  const end = times[i * 2 + 1];
114
- if (begin === 0n || end < begin || begin < stale || !match(target.names[i])) continue;
165
+ if (begin === 0n || end < begin || this.stale(times, i) || !match(target.names[i]))
166
+ continue;
115
167
  if (a === 0n || begin < a) a = begin;
116
168
  if (end > b) b = end;
117
169
  }
118
- if (b > a) profiler?.record(label, Number(b - a) / 1e6);
170
+ if (b > a) profiler?.record(label, Number(b - a) / 1e6, TRACK.gpu, at(a), target.frame);
119
171
  }
120
- if (last > first) {
121
- this.frameMs = Number(last - first) / 1e6;
172
+ if (first !== 0n) {
173
+ this.frameMs = Number(lastEnd - first) / 1e6;
122
174
  this.frameSamples++;
123
- profiler?.record("gpu:frame", this.frameMs);
175
+ profiler?.record("gpu:frame", this.frameMs, TRACK.gpu, target.submitAt, target.frame);
124
176
  }
177
+ for (let k = 0; k < count; k++) this.lastStamps[k] = times[k];
125
178
  target.buffer.unmap();
126
179
  target.busy = false;
127
180
  },
@@ -0,0 +1,37 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { App } from '@aethervtt/shard-runtime';
3
+ /** A kind of manifest entry, and how to show the variants it names. */
4
+ export interface ShaderVariantSource {
5
+ /** The entry's key: `{ "dice": … }` is run by the source named `dice`. */
6
+ readonly name: string;
7
+ readonly description: string;
8
+ /**
9
+ * Shows every variant `entry` names for at least a frame, stepping the app until their pipelines
10
+ * are made (`app.update`, then `Shaders.whenIdle` and `gpu.pipelines.whenIdle`). Returns how many
11
+ * variants it showed.
12
+ */
13
+ show(app: App, entry: unknown): Promise<number>;
14
+ }
15
+ export declare const ShaderVariantSources: import("@aethervtt/shard-core").ResourceDef<Map<string, ShaderVariantSource>>;
16
+ /** Registers a manifest entry kind. Plugins call it in `build`. */
17
+ export declare function addShaderVariantSource(world: World, source: ShaderVariantSource): void;
18
+ /** The manifest's shape. */
19
+ export interface ShaderVariantManifest {
20
+ variants: Record<string, unknown>[];
21
+ }
22
+ /** Runs one manifest entry through its source. Throws `render/unknown-variant-source` for a key no plugin registered. */
23
+ export declare function showVariants(app: App, entry: Record<string, unknown>): Promise<number>;
24
+ /** A material asset's path (or guid) and parameter overrides. */
25
+ export interface MaterialVariantEntry {
26
+ path?: string;
27
+ guid?: string;
28
+ params?: Record<string, unknown>;
29
+ }
30
+ /**
31
+ * Shows what `spawn` makes to a camera of its own (an offscreen target, a shadowed sun), stepping
32
+ * the app until every pipeline it asked for is made, then takes it all away again. For variant
33
+ * sources: what's drawn is what a bake records.
34
+ */
35
+ export declare function showToCamera(app: App, spawn: (at: [number, number, number]) => Entity[]): Promise<void>;
36
+ /** `{ "material": { "path": …, "params": … } }`: a material asset, with its parameters set, on a cube. */
37
+ export declare const materialVariants: ShaderVariantSource;
@@ -0,0 +1,104 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import {
3
+ defineResource,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ import { cube } from "@aethervtt/shard-mesh";
7
+ import { lookAt, Transform } from "@aethervtt/shard-transform";
8
+ import { Materials, Meshes, RenderTargets } from "./assets.js";
9
+ import { Camera3d } from "./camera.js";
10
+ import { Mesh3d, MeshMaterial } from "./instances.js";
11
+ import { DirectionalLight } from "./lights.js";
12
+ import { Gpu, Shaders } from "./plugin.js";
13
+ import { OffscreenTarget } from "./target.js";
14
+ const ShaderVariantSources = defineResource(
15
+ "render/ShaderVariantSources",
16
+ {
17
+ description: "Kinds of shaders.variants.json entries (0064) and how to show their variants, for `shard shaders bake`.",
18
+ init: () => /* @__PURE__ */ new Map()
19
+ }
20
+ );
21
+ function addShaderVariantSource(world, source) {
22
+ world.initResource(ShaderVariantSources).set(source.name, source);
23
+ }
24
+ function showVariants(app, entry) {
25
+ const keys = Object.keys(entry);
26
+ if (keys.length !== 1) {
27
+ throw new ShardError(
28
+ "render/variant-entry",
29
+ `A shaders.variants.json entry has one key, its source: got ${keys.length ? keys.join(", ") : "none"}`
30
+ );
31
+ }
32
+ const name = keys[0];
33
+ const source = app.world.tryResource(ShaderVariantSources)?.get(name);
34
+ if (!source) {
35
+ const known = [...app.world.tryResource(ShaderVariantSources)?.keys() ?? []];
36
+ throw new ShardError("render/unknown-variant-source", `No shader variant source "${name}"`, {
37
+ hint: `Known: ${known.join(", ") || "none"}. Plugins register theirs with addShaderVariantSource.`
38
+ });
39
+ }
40
+ return source.show(app, entry[name]);
41
+ }
42
+ const STAGE = [5e4, 0, 5e4];
43
+ async function showToCamera(app, spawn) {
44
+ const world = app.world;
45
+ const gpu = world.resource(Gpu);
46
+ const target = new OffscreenTarget(gpu, { label: "shader-variants", width: 64, height: 64 });
47
+ const ref = world.resource(RenderTargets).add(target, `render:variants/${STAGE.join(",")}`);
48
+ const eye = [STAGE[0], STAGE[1] + 2, STAGE[2] + 4];
49
+ const made = [
50
+ world.spawn(
51
+ [Camera3d, { target: ref, order: 90 }],
52
+ [Transform, { translation: eye, rotation: lookAt(eye, STAGE) }]
53
+ ),
54
+ world.spawn(
55
+ [DirectionalLight, { shadows: true }],
56
+ [Transform, { translation: STAGE, rotation: lookAt([0, 0, 0], [-0.4, -1, -0.3]) }]
57
+ ),
58
+ ...spawn(STAGE)
59
+ ];
60
+ try {
61
+ for (let i = 0; i < 8; i++) {
62
+ app.update(1 / 60);
63
+ await world.resource(Shaders).whenIdle();
64
+ await gpu.pipelines.whenIdle();
65
+ }
66
+ } finally {
67
+ for (const e of made) if (world.isAlive(e)) world.despawn(e);
68
+ app.update(1 / 60);
69
+ target.destroy?.();
70
+ }
71
+ }
72
+ const materialVariants = {
73
+ name: "material",
74
+ description: "A material asset by path or guid, with parameter overrides: { path, params? }.",
75
+ async show(app, entry) {
76
+ const list = Array.isArray(entry) ? entry : [entry];
77
+ const world = app.world;
78
+ for (const m of list) {
79
+ const ref = m.guid ? { guid: m.guid } : { path: m.path };
80
+ await assetServer(world).load(ref);
81
+ const material = world.resource(Materials).get(ref);
82
+ if (!material) {
83
+ throw new ShardError("render/variant-material", `No material ${m.guid ?? m.path}`);
84
+ }
85
+ if (m.params) material.set(m.params);
86
+ const mesh = world.resource(Meshes).add(cube({ size: 1 }));
87
+ await showToCamera(app, (at) => [
88
+ world.spawn(
89
+ [Mesh3d, { mesh }],
90
+ [MeshMaterial, { material: ref }],
91
+ [Transform, { translation: at }]
92
+ )
93
+ ]);
94
+ }
95
+ return list.length;
96
+ }
97
+ };
98
+ export {
99
+ ShaderVariantSources,
100
+ addShaderVariantSource,
101
+ materialVariants,
102
+ showToCamera,
103
+ showVariants
104
+ };
@@ -0,0 +1,37 @@
1
+ import type { World } from '@aethervtt/shard-core';
2
+ import { GpuBuffer } from '@aethervtt/shard-gpu';
3
+ import { type NodeDescriptor } from './graph.js';
4
+ import type { VfxRecord } from './vfx-state.js';
5
+ export declare const VFX_SHADERS: {
6
+ 'shard::vfx::ribbon': string;
7
+ };
8
+ interface DrawRecord {
9
+ source: VfxRecord;
10
+ view: string;
11
+ vertices: GpuBuffer;
12
+ uniform: GpuBuffer;
13
+ data: Float32Array;
14
+ group: GPUBindGroup | undefined;
15
+ texture: GPUTextureView | undefined;
16
+ version: number;
17
+ generation: number;
18
+ }
19
+ /** Per-world, per-view uniforms: two cameras never overwrite one another before submit. */
20
+ export declare class VfxRenderer {
21
+ readonly draws: DrawRecord[];
22
+ private generation;
23
+ private layout;
24
+ private pipelines;
25
+ private white;
26
+ private whiteView;
27
+ private sampler;
28
+ private readonly decal;
29
+ private readonly records;
30
+ constructor(records: VfxRecord[], decal: boolean);
31
+ dispose(): void;
32
+ prune(world: World): void;
33
+ private init;
34
+ node(): NodeDescriptor;
35
+ private draw;
36
+ }
37
+ export {};