@aethervtt/shard-render 0.1.0 → 0.2.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 (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. package/package.json +19 -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++) {
@@ -149,7 +153,9 @@ function meanDifference(a, b) {
149
153
  export {
150
154
  compareGolden,
151
155
  comparePng,
156
+ expectScenario,
152
157
  meanDifference,
158
+ measureScenario,
153
159
  pixel,
154
160
  pngBytes,
155
161
  renderView,
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;
@@ -24,7 +33,13 @@ export declare class GpuTimer {
24
33
  /** Timestamp writes for the next pass, or undefined when timing is off or full. */
25
34
  allocate(name: string): GPURenderPassTimestampWrites | undefined;
26
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;
27
38
  resolve(encoder: GPUCommandEncoder): void;
28
- /** 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
+ */
29
44
  readback(world: World): void;
30
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 = [];
@@ -32,6 +51,7 @@ class GpuTimer {
32
51
  this.names = [];
33
52
  if (!this.enabled || this.generation === this.gpu.generation) return;
34
53
  this.generation = this.gpu.generation;
54
+ this.lastStamps.fill(0n);
35
55
  const device = this.gpu.device;
36
56
  this.querySet = device.createQuerySet({
37
57
  label: "gpu-timer",
@@ -52,7 +72,9 @@ class GpuTimer {
52
72
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
53
73
  }),
54
74
  busy: false,
55
- names: []
75
+ names: [],
76
+ frame: 0,
77
+ submitAt: 0
56
78
  });
57
79
  }
58
80
  }
@@ -68,6 +90,11 @@ class GpuTimer {
68
90
  };
69
91
  }
70
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
+ }
71
98
  resolve(encoder) {
72
99
  this.pendingCopy = void 0;
73
100
  if (!this.enabled || this.names.length === 0) return;
@@ -80,36 +107,54 @@ class GpuTimer {
80
107
  target.names = this.names;
81
108
  this.pendingCopy = target;
82
109
  }
83
- /** 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
+ */
84
115
  readback(world) {
116
+ const profiler = world.tryResource(ProfilerResource);
117
+ if (profiler) profiler.gpu.status = this.enabled ? "available" : "unavailable";
85
118
  const target = this.pendingCopy;
86
119
  if (!target) return;
87
120
  this.pendingCopy = void 0;
121
+ target.frame = profiler?.frame ?? 0;
122
+ target.submitAt = profiler ? profiler.now() : 0;
88
123
  const generation = this.generation;
89
124
  target.buffer.mapAsync(GPUMapMode.READ).then(
90
125
  () => {
91
126
  if (generation !== this.gpu.generation) return;
92
127
  const times = new BigUint64Array(target.buffer.getMappedRange());
93
- const profiler = world.tryResource(ProfilerResource);
94
128
  const totals = /* @__PURE__ */ new Map();
95
- let latest = 0n;
96
- for (let i = 0; i < target.names.length; i++) {
97
- const end = times[i * 2 + 1];
98
- if (end > latest) latest = end;
99
- }
100
- const stale = latest - STALE_NS;
129
+ const count = target.names.length * 2;
101
130
  let first = 0n;
102
- let last = 0n;
131
+ let lastEnd = 0n;
103
132
  for (let i = 0; i < target.names.length; i++) {
104
133
  const begin = times[i * 2];
105
134
  const end = times[i * 2 + 1];
106
- if (begin === 0n || end < begin || begin < stale) continue;
135
+ if (begin === 0n || end < begin || this.stale(times, i)) continue;
107
136
  const name = target.names[i];
108
137
  totals.set(name, (totals.get(name) ?? 0) + Number(end - begin) / 1e6);
109
138
  if (first === 0n || begin < first) first = begin;
110
- 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
+ }
111
157
  }
112
- for (const [name, ms] of totals) profiler?.record(`gpu:${name}`, ms);
113
158
  for (let g = 0; g < SPANS.length; g++) {
114
159
  const [label, match] = SPANS[g];
115
160
  let a = 0n;
@@ -117,17 +162,19 @@ class GpuTimer {
117
162
  for (let i = 0; i < target.names.length; i++) {
118
163
  const begin = times[i * 2];
119
164
  const end = times[i * 2 + 1];
120
- 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;
121
167
  if (a === 0n || begin < a) a = begin;
122
168
  if (end > b) b = end;
123
169
  }
124
- 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);
125
171
  }
126
172
  if (first !== 0n) {
127
- this.frameMs = Number(last - first) / 1e6;
173
+ this.frameMs = Number(lastEnd - first) / 1e6;
128
174
  this.frameSamples++;
129
- profiler?.record("gpu:frame", this.frameMs);
175
+ profiler?.record("gpu:frame", this.frameMs, TRACK.gpu, target.submitAt, target.frame);
130
176
  }
177
+ for (let k = 0; k < count; k++) this.lastStamps[k] = times[k];
131
178
  target.buffer.unmap();
132
179
  target.busy = false;
133
180
  },
@@ -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 {};
@@ -0,0 +1,291 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
3
+ import { Textures } from "@aethervtt/shard-texture";
4
+ import { GpuAssetsResource } from "./gpu-assets.js";
5
+ import { RenderPhase } from "./graph.js";
6
+ import { InstanceSlot } from "./instances.js";
7
+ import { Shaders, Views } from "./plugin.js";
8
+ import { bindingDimension } from "./tier.js";
9
+ import { cameraOf } from "./view.js";
10
+ const VFX_SHADERS = {
11
+ "shard::vfx::ribbon": `
12
+ struct View { vp: mat4x4f, eye: vec4f, forward: vec4f, params: vec4f, tint: vec4f }
13
+ @group(0) @binding(0) var<uniform> view: View;
14
+ @group(0) @binding(1) var image: texture_2d<f32>;
15
+ @group(0) @binding(2) var image_sampler: sampler;
16
+ struct Input {
17
+ @location(0) position: vec3f,
18
+ @location(1) tangent: vec3f,
19
+ @location(2) uv: vec2f,
20
+ @location(3) color: vec4f,
21
+ @location(4) width_side: vec2f,
22
+ }
23
+ struct Output { @builtin(position) position: vec4f, @location(0) uv: vec2f, @location(1) color: vec4f }
24
+ @vertex fn vs(in: Input) -> Output {
25
+ var out: Output;
26
+ var pos = in.position;
27
+ if (in.width_side.x > 0.0) {
28
+ var toward = view.eye.xyz - pos;
29
+ if (view.eye.w > 0.5) { toward = -view.forward.xyz; }
30
+ var side = cross(in.tangent, toward);
31
+ if (dot(side, side) < 1e-12) { side = cross(in.tangent, vec3f(0.0, 1.0, 0.0)); }
32
+ if (dot(side, side) < 1e-12) { side = vec3f(1.0, 0.0, 0.0); }
33
+ pos += normalize(side) * in.width_side.x * in.width_side.y;
34
+ }
35
+ out.position = view.vp * vec4f(pos, 1.0);
36
+ out.uv = in.uv;
37
+ out.color = in.color * view.tint;
38
+ return out;
39
+ }
40
+ @fragment fn fs(in: Output) -> @location(0) vec4f {
41
+ let c = textureSample(image, image_sampler, in.uv) * in.color;
42
+ let alpha = c.a * view.params.x;
43
+ if (alpha <= 0.00001) { discard; }
44
+ return vec4f(c.rgb * alpha, alpha);
45
+ }`
46
+ };
47
+ const OVER = {
48
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
49
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
50
+ };
51
+ const ADD = {
52
+ color: { srcFactor: "one", dstFactor: "one" },
53
+ alpha: { srcFactor: "zero", dstFactor: "one" }
54
+ };
55
+ const BUFFERS = [
56
+ {
57
+ arrayStride: 56,
58
+ attributes: [
59
+ { shaderLocation: 0, offset: 0, format: "float32x3" },
60
+ { shaderLocation: 1, offset: 12, format: "float32x3" },
61
+ { shaderLocation: 2, offset: 24, format: "float32x2" },
62
+ { shaderLocation: 3, offset: 32, format: "float32x4" },
63
+ { shaderLocation: 4, offset: 48, format: "float32x2" }
64
+ ]
65
+ }
66
+ ];
67
+ class VfxRenderer {
68
+ draws = [];
69
+ generation = -1;
70
+ layout;
71
+ pipelines = [void 0, void 0];
72
+ white;
73
+ whiteView;
74
+ sampler;
75
+ decal;
76
+ records;
77
+ constructor(records, decal) {
78
+ this.records = records;
79
+ this.decal = decal;
80
+ }
81
+ dispose() {
82
+ for (let i = 0; i < this.draws.length; i++) {
83
+ this.draws[i].vertices.destroy();
84
+ this.draws[i].uniform.destroy();
85
+ }
86
+ this.draws.length = 0;
87
+ this.white?.destroy();
88
+ this.white = void 0;
89
+ this.whiteView = void 0;
90
+ }
91
+ prune(world) {
92
+ const views = world.resource(Views).list;
93
+ for (let i = this.draws.length - 1; i >= 0; i--) {
94
+ const d = this.draws[i];
95
+ let view = false;
96
+ for (let j = 0; j < views.length; j++)
97
+ if (views[j].name === d.view) {
98
+ view = true;
99
+ break;
100
+ }
101
+ if (!view || !d.source.count || !this.records.includes(d.source)) {
102
+ d.vertices.destroy();
103
+ d.uniform.destroy();
104
+ this.draws.splice(i, 1);
105
+ }
106
+ }
107
+ }
108
+ init(gpu) {
109
+ if (this.generation === gpu.generation) return;
110
+ this.dispose();
111
+ this.generation = gpu.generation;
112
+ this.pipelines[0] = void 0;
113
+ this.pipelines[1] = void 0;
114
+ this.layout = gpu.layouts.bindGroupLayout({
115
+ label: "vfx",
116
+ entries: [
117
+ {
118
+ binding: 0,
119
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
120
+ buffer: { type: "uniform" }
121
+ },
122
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
123
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } }
124
+ ]
125
+ });
126
+ this.white = gpu.device.createTexture({
127
+ label: "vfx/white",
128
+ size: [1, 1],
129
+ format: "rgba8unorm",
130
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
131
+ ...bindingDimension(gpu, "2d")
132
+ });
133
+ gpu.device.queue.writeTexture(
134
+ { texture: this.white },
135
+ new Uint8Array([255, 255, 255, 255]),
136
+ { bytesPerRow: 4 },
137
+ [1, 1]
138
+ );
139
+ this.whiteView = this.white.createView();
140
+ this.sampler = gpu.device.createSampler({
141
+ magFilter: "linear",
142
+ minFilter: "linear",
143
+ addressModeU: this.decal ? "clamp-to-edge" : "repeat",
144
+ addressModeV: "clamp-to-edge"
145
+ });
146
+ }
147
+ node() {
148
+ return {
149
+ kind: "render",
150
+ phase: RenderPhase.Resolve + (this.decal ? 5 : 15),
151
+ reads: ["depth"],
152
+ writes: ["hdr"],
153
+ color: [{ resource: "hdr" }],
154
+ depth: { resource: "depth", readOnly: true },
155
+ enabled: (view) => {
156
+ const cam = cameraOf(view);
157
+ if (!cam) return false;
158
+ for (let i = 0; i < this.records.length; i++) {
159
+ const r = this.records[i];
160
+ if (r.count && r.visible && r.opacity > 0 && r.layers & cam.layers) return true;
161
+ }
162
+ return false;
163
+ },
164
+ run: (ctx) => this.draw(ctx)
165
+ };
166
+ }
167
+ draw(ctx) {
168
+ const gpu = ctx.gpu, cam = cameraOf(ctx.view), pass = ctx.renderPass;
169
+ this.init(gpu);
170
+ const textures = ctx.world.resource(Textures), assets = ctx.world.resource(GpuAssetsResource);
171
+ for (let i = 0; i < this.records.length; i++) {
172
+ const r = this.records[i];
173
+ if (!r.count || !r.visible || r.opacity <= 0 || !(r.layers & cam.layers)) continue;
174
+ const index = r.additive ? 1 : 0;
175
+ let pipeline = this.pipelines[index];
176
+ if (!pipeline) {
177
+ const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::vfx::ribbon" });
178
+ if (!module) {
179
+ gpu.pipelines.skipped++;
180
+ continue;
181
+ }
182
+ pipeline = gpu.pipelines.render({
183
+ label: index ? "vfx/additive" : "vfx/alpha",
184
+ layout: gpu.layouts.pipelineLayout({ bindGroupLayouts: [this.layout] }),
185
+ vertex: { module, entryPoint: "vs", buffers: BUFFERS },
186
+ fragment: {
187
+ module,
188
+ entryPoint: "fs",
189
+ targets: [{ format: "rgba16float", blend: index ? ADD : OVER }]
190
+ },
191
+ primitive: { topology: "triangle-list", cullMode: "none" },
192
+ depthStencil: {
193
+ format: "depth32float",
194
+ depthWriteEnabled: false,
195
+ depthCompare: "greater-equal",
196
+ // Resolved MSAA depth comes from a subsample, not the fragment center.
197
+ // Positive reversed-Z slope bias keeps coplanar decals visible on every adapter.
198
+ depthBias: this.decal ? 1 : 0,
199
+ depthBiasSlopeScale: this.decal ? 1 : 0
200
+ }
201
+ });
202
+ if (!pipeline) {
203
+ gpu.pipelines.skipped++;
204
+ continue;
205
+ }
206
+ this.pipelines[index] = pipeline;
207
+ }
208
+ let d;
209
+ for (let j = 0; j < this.draws.length; j++)
210
+ if (this.draws[j].source === r && this.draws[j].view === ctx.view.name) {
211
+ d = this.draws[j];
212
+ break;
213
+ }
214
+ if (!d) {
215
+ d = {
216
+ source: r,
217
+ view: ctx.view.name,
218
+ vertices: new GpuBuffer(gpu, {
219
+ label: "vfx/vertices",
220
+ usage: GPUBufferUsage.VERTEX,
221
+ size: r.vertices.byteLength
222
+ }),
223
+ uniform: new GpuBuffer(gpu, {
224
+ label: "vfx/view",
225
+ usage: GPUBufferUsage.UNIFORM,
226
+ size: 128
227
+ }),
228
+ data: new Float32Array(32),
229
+ group: void 0,
230
+ texture: void 0,
231
+ version: -1,
232
+ generation: gpu.generation
233
+ };
234
+ this.draws.push(d);
235
+ }
236
+ if (d.version !== r.version) {
237
+ d.vertices.write(r.vertices, 0, 0, r.count * 14);
238
+ d.version = r.version;
239
+ }
240
+ const texture = r.texture ? textures.get(r.texture) : void 0, gt = texture ? assets.texture(texture) : void 0;
241
+ r.textureState = !r.texture ? "none" : gt ? "ready" : assetServer(ctx.world).entry(r.texture) ? "loading" : "missing";
242
+ const image = gt ? texture.usage === "color" ? gt.srgb : gt.linear : this.whiteView;
243
+ if (!d.group || d.texture !== image) {
244
+ d.texture = image;
245
+ d.group = gpu.device.createBindGroup({
246
+ label: "vfx/draw",
247
+ layout: this.layout,
248
+ entries: [
249
+ { binding: 0, resource: { buffer: d.uniform.buffer } },
250
+ { binding: 1, resource: image },
251
+ { binding: 2, resource: this.sampler }
252
+ ]
253
+ });
254
+ }
255
+ d.data.set(cam.viewProj);
256
+ d.data.set(cam.position, 16);
257
+ d.data[19] = cam.orthographic ? 1 : 0;
258
+ d.data.set(cam.forward, 20);
259
+ d.data[24] = r.opacity;
260
+ if (this.decal) {
261
+ const colors = r.columns.color;
262
+ for (let k = 0; k < 4; k++) d.data[28 + k] = colors[r.row * 4 + k];
263
+ } else {
264
+ d.data[28] = 1;
265
+ d.data[29] = 1;
266
+ d.data[30] = 1;
267
+ d.data[31] = 1;
268
+ }
269
+ d.uniform.write(d.data);
270
+ pass.setPipeline(pipeline);
271
+ pass.setBindGroup(0, d.group);
272
+ pass.setVertexBuffer(0, d.vertices.buffer);
273
+ if (this.decal) {
274
+ const ranges = r.ranges;
275
+ for (let j = 0; j < ranges.length; j++) {
276
+ const range = ranges[j];
277
+ if (!(range.layers & cam.layers)) continue;
278
+ if (cam.hidden && ctx.world.has(range.entity, InstanceSlot)) {
279
+ const table = ctx.world.entityTableUnchecked(range.entity), row = ctx.world.entityRowUnchecked(range.entity), slot = table.column(InstanceSlot, "slot")[row] - 1;
280
+ if (slot >= 0 && (cam.hidden.bits[slot >> 5] ?? 0) & 1 << (slot & 31)) continue;
281
+ }
282
+ pass.draw(range.count, 1, range.start);
283
+ }
284
+ } else pass.draw(r.count);
285
+ }
286
+ }
287
+ }
288
+ export {
289
+ VFX_SHADERS,
290
+ VfxRenderer
291
+ };
@@ -0,0 +1,42 @@
1
+ import type { AssetRef, ComponentDef, Entity, Table, TypedArray, World } from '@aethervtt/shard-core';
2
+ export declare const VFX_STRIDE = 14;
3
+ export interface VfxRecord {
4
+ entity: Entity;
5
+ def: ComponentDef;
6
+ kind: 'trail' | 'beam' | 'decal';
7
+ table: Table;
8
+ row: number;
9
+ columns: Record<string, TypedArray | unknown[]>;
10
+ matrix: Float64Array;
11
+ vertices: Float32Array;
12
+ count: number;
13
+ version: number;
14
+ age: number;
15
+ wasPlaying: boolean;
16
+ error: string | null;
17
+ seen: number;
18
+ opacity: number;
19
+ texture: AssetRef<'Texture'> | null;
20
+ layers: number;
21
+ additive: boolean;
22
+ textureState: string;
23
+ visible: boolean;
24
+ }
25
+ export declare function createRecord(world: World, entity: Entity, def: ComponentDef, kind: VfxRecord['kind'], vertices: number): VfxRecord;
26
+ export declare function refreshRecord(world: World, r: VfxRecord): void;
27
+ export declare function number(r: VfxRecord, field: string): number;
28
+ export declare function matrixChanged(world: World, entity: Entity, out: Float64Array): boolean;
29
+ export declare function point(out: Float64Array, at: number, m: ArrayLike<number>, x: number, y: number, z: number): void;
30
+ export declare function recordDescription(r: VfxRecord): {
31
+ entity: number;
32
+ kind: "beam" | "decal" | "trail";
33
+ vertices: number;
34
+ age: number;
35
+ opacity: number;
36
+ error: string | null;
37
+ visible: boolean;
38
+ cpuBytes: number;
39
+ texture: string;
40
+ playing: boolean;
41
+ paused: boolean;
42
+ };
@@ -0,0 +1,81 @@
1
+ import { GlobalTransform } from "@aethervtt/shard-transform";
2
+ import { ComputedVisibility } from "./visibility.js";
3
+ const VFX_STRIDE = 14;
4
+ function createRecord(world, entity, def, kind, vertices) {
5
+ const table = world.entityTable(entity);
6
+ return {
7
+ entity,
8
+ def,
9
+ kind,
10
+ table,
11
+ row: world.entityRow(entity),
12
+ columns: table.storage(def).byName,
13
+ matrix: new Float64Array(12),
14
+ vertices: new Float32Array(vertices * VFX_STRIDE),
15
+ count: 0,
16
+ version: 0,
17
+ age: 0,
18
+ wasPlaying: false,
19
+ error: null,
20
+ seen: 0,
21
+ opacity: 1,
22
+ texture: null,
23
+ layers: 1,
24
+ additive: false,
25
+ textureState: "none",
26
+ visible: true
27
+ };
28
+ }
29
+ function refreshRecord(world, r) {
30
+ r.table = world.entityTableUnchecked(r.entity);
31
+ r.row = world.entityRowUnchecked(r.entity);
32
+ r.columns = r.table.storage(r.def).byName;
33
+ r.texture = r.columns.texture[r.row];
34
+ r.opacity = number(r, "opacity");
35
+ r.layers = number(r, "layers");
36
+ r.additive = number(r, "blend") === 1;
37
+ r.visible = !r.table.has(ComputedVisibility) || r.table.column(ComputedVisibility, "visible")[r.row] !== 0;
38
+ }
39
+ function number(r, field) {
40
+ return r.columns[field][r.row];
41
+ }
42
+ function matrixChanged(world, entity, out) {
43
+ const t = world.entityTableUnchecked(entity), row = world.entityRowUnchecked(entity);
44
+ const m = t.column(GlobalTransform, "matrix");
45
+ let changed = false;
46
+ for (let i = 0; i < 12; i++) {
47
+ const v = m[row * 12 + i];
48
+ if (out[i] !== v) changed = true;
49
+ out[i] = v;
50
+ }
51
+ return changed;
52
+ }
53
+ function point(out, at, m, x, y, z) {
54
+ out[at] = m[0] * x + m[1] * y + m[2] * z + m[3];
55
+ out[at + 1] = m[4] * x + m[5] * y + m[6] * z + m[7];
56
+ out[at + 2] = m[8] * x + m[9] * y + m[10] * z + m[11];
57
+ }
58
+ function recordDescription(r) {
59
+ return {
60
+ entity: r.entity,
61
+ kind: r.kind,
62
+ vertices: r.count,
63
+ age: r.age,
64
+ opacity: r.opacity,
65
+ error: r.error,
66
+ visible: r.visible,
67
+ cpuBytes: r.vertices.byteLength,
68
+ texture: r.textureState,
69
+ playing: !!number(r, "playing"),
70
+ paused: !!number(r, "paused")
71
+ };
72
+ }
73
+ export {
74
+ VFX_STRIDE,
75
+ createRecord,
76
+ matrixChanged,
77
+ number,
78
+ point,
79
+ recordDescription,
80
+ refreshRecord
81
+ };