@aethervtt/shard-render 0.1.0 → 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.
- package/dist/ablation.d.ts +74 -0
- package/dist/ablation.js +117 -0
- package/dist/atmosphere-nodes.js +100 -12
- package/dist/atmosphere-shaders.js +17 -5
- package/dist/baseline/exposure.d.ts +1 -1
- package/dist/baseline/exposure.js +18 -5
- package/dist/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -0
- package/dist/decals-plugin.d.ts +5 -0
- package/dist/decals-plugin.js +54 -0
- package/dist/decals.d.ts +69 -0
- package/dist/decals.js +326 -0
- package/dist/deferred.js +4 -1
- package/dist/display-nodes.js +24 -11
- package/dist/foliage-budget.d.ts +64 -0
- package/dist/foliage-budget.js +108 -0
- package/dist/foliage-path.d.ts +19 -0
- package/dist/foliage-path.js +7 -0
- package/dist/foliage-shaders.d.ts +11 -0
- package/dist/foliage-shaders.js +504 -0
- package/dist/foliage.d.ts +194 -0
- package/dist/foliage.js +1027 -0
- package/dist/forward.js +26 -2
- package/dist/graph.d.ts +34 -0
- package/dist/graph.js +139 -14
- package/dist/index.d.ts +11 -1
- package/dist/index.js +57 -0
- package/dist/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +5 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -0
- package/dist/plugin.d.ts +11 -1
- package/dist/plugin.js +33 -2
- package/dist/post-common.d.ts +20 -2
- package/dist/post-common.js +79 -2
- package/dist/post-nodes.d.ts +5 -0
- package/dist/post-nodes.js +136 -104
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +41 -63
- package/dist/post.js +11 -1
- package/dist/ribbons-plugin.d.ts +5 -0
- package/dist/ribbons-plugin.js +59 -0
- package/dist/ribbons.d.ts +44 -0
- package/dist/ribbons.js +288 -0
- package/dist/scenario.d.ts +70 -0
- package/dist/scenario.js +125 -0
- package/dist/shaders.js +58 -5
- package/dist/shadows.d.ts +2 -1
- package/dist/shadows.js +3 -1
- package/dist/standard.js +6 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +6 -0
- package/dist/timer.d.ts +16 -1
- package/dist/timer.js +66 -19
- package/dist/vfx-draw.d.ts +37 -0
- package/dist/vfx-draw.js +291 -0
- package/dist/vfx-state.d.ts +42 -0
- package/dist/vfx-state.js +81 -0
- package/dist/vfx.d.ts +69 -0
- package/dist/vfx.js +103 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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 >
|
|
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 ||
|
|
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(
|
|
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 {};
|
package/dist/vfx-draw.js
ADDED
|
@@ -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
|
+
};
|