@aethervtt/shard-render 0.0.1 → 0.1.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/assets.d.ts +3 -0
- package/dist/assets.js +43 -2
- package/dist/atmosphere-nodes.d.ts +9 -0
- package/dist/atmosphere-nodes.js +57 -18
- package/dist/atmosphere-plugin.js +8 -2
- package/dist/baseline/atmosphere.d.ts +16 -0
- package/dist/baseline/atmosphere.js +301 -0
- package/dist/baseline/data-texture.d.ts +36 -0
- package/dist/baseline/data-texture.js +181 -0
- package/dist/baseline/environment.d.ts +14 -0
- package/dist/baseline/environment.js +368 -0
- package/dist/baseline/exposure.d.ts +25 -0
- package/dist/baseline/exposure.js +120 -0
- package/dist/baseline/lights.d.ts +33 -0
- package/dist/baseline/lights.js +133 -0
- package/dist/baseline/textures.d.ts +97 -0
- package/dist/baseline/textures.js +203 -0
- package/dist/camera.d.ts +2 -0
- package/dist/camera.js +4 -0
- package/dist/culling-shaders.js +21 -2
- package/dist/culling.d.ts +11 -2
- package/dist/culling.js +44 -18
- package/dist/cutaway-plugin.d.ts +48 -0
- package/dist/cutaway-plugin.js +364 -0
- package/dist/cutaway.d.ts +27 -0
- package/dist/cutaway.js +49 -0
- package/dist/data-store.d.ts +69 -0
- package/dist/data-store.js +105 -0
- package/dist/debug-shaders.js +17 -3
- package/dist/debug-views.js +4 -2
- package/dist/deferred-shaders.js +2 -0
- package/dist/deferred.js +24 -3
- package/dist/deform.d.ts +5 -4
- package/dist/deform.js +4 -9
- package/dist/describe-plugin.d.ts +4 -2
- package/dist/describe-plugin.js +75 -3
- package/dist/display-nodes.js +15 -4
- package/dist/dynamic-resolution.d.ts +2 -0
- package/dist/dynamic-resolution.js +6 -1
- package/dist/environment-plugin.js +17 -2
- package/dist/environment-state.d.ts +7 -3
- package/dist/environment-state.js +20 -9
- package/dist/environment.js +7 -1
- package/dist/features.d.ts +49 -0
- package/dist/features.js +106 -0
- package/dist/forward.d.ts +38 -2
- package/dist/forward.js +582 -184
- package/dist/fxaa.js +12 -2
- package/dist/gizmos-plugin.js +7 -0
- package/dist/gizmos.d.ts +3 -3
- package/dist/gizmos.js +11 -13
- package/dist/gpu-assets.d.ts +58 -4
- package/dist/gpu-assets.js +191 -8
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +30 -5
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +286 -9
- package/dist/instances.d.ts +119 -8
- package/dist/instances.js +387 -78
- package/dist/interior-plugin.d.ts +121 -0
- package/dist/interior-plugin.js +465 -0
- package/dist/interior.d.ts +24 -0
- package/dist/interior.js +13 -0
- package/dist/layers.d.ts +35 -0
- package/dist/layers.js +49 -0
- package/dist/lens-plugin.d.ts +43 -0
- package/dist/lens-plugin.js +304 -0
- package/dist/lens.d.ts +41 -0
- package/dist/lens.js +99 -0
- package/dist/lights.d.ts +50 -5
- package/dist/lights.js +101 -22
- package/dist/material-pipelines.d.ts +37 -4
- package/dist/material-pipelines.js +102 -5
- package/dist/materials.d.ts +23 -0
- package/dist/materials.js +44 -1
- package/dist/outline-plugin.d.ts +48 -0
- package/dist/outline-plugin.js +529 -0
- package/dist/outline.d.ts +15 -0
- package/dist/outline.js +42 -0
- package/dist/overlays.js +1 -0
- package/dist/picking.d.ts +17 -0
- package/dist/picking.js +40 -11
- package/dist/pixel-perfect-plugin.js +10 -2
- package/dist/plugin.d.ts +58 -6
- package/dist/plugin.js +191 -24
- package/dist/post-common.d.ts +2 -0
- package/dist/post-common.js +3 -0
- package/dist/post-nodes.d.ts +2 -1
- package/dist/post-nodes.js +111 -19
- package/dist/post-plugin.js +5 -3
- package/dist/post-shaders.js +51 -6
- package/dist/projection.d.ts +24 -0
- package/dist/projection.js +87 -0
- package/dist/readback.d.ts +1 -0
- package/dist/readback.js +1 -0
- package/dist/render-scale.js +1 -0
- package/dist/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +131 -29
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +25 -1
- package/dist/shadows.js +97 -19
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +12 -0
- package/dist/stats.d.ts +45 -1
- package/dist/stats.js +91 -3
- package/dist/surface-model.d.ts +57 -0
- package/dist/surface-model.js +289 -0
- package/dist/surface-shaders.d.ts +3 -0
- package/dist/surface-shaders.js +160 -0
- package/dist/surface.d.ts +40 -0
- package/dist/surface.js +86 -0
- package/dist/target.d.ts +1 -13
- package/dist/target.js +1 -26
- package/dist/testing.d.ts +10 -0
- package/dist/testing.js +33 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +2 -0
- package/dist/timer.js +7 -1
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -0
- package/dist/view-visibility-plugin.d.ts +61 -0
- package/dist/view-visibility-plugin.js +267 -0
- package/dist/view-visibility.d.ts +28 -0
- package/dist/view-visibility.js +46 -0
- package/dist/view.d.ts +29 -1
- package/dist/view.js +57 -34
- package/package.json +15 -11
|
@@ -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>;
|
package/dist/cutaway.js
ADDED
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
/** Texels per row of a data texture (the baseline rewrite's loaders read the same). */
|
|
3
|
+
export declare const DATA_WIDTH = 1024;
|
|
4
|
+
export interface DataStoreOptions {
|
|
5
|
+
label: string;
|
|
6
|
+
/** Initial size in bytes. Default 256. */
|
|
7
|
+
size?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Buffer usages beyond STORAGE (full tier) or UNIFORM (a baseline uniform block): a GPU pass that
|
|
10
|
+
* writes it, a readback. Data textures ignore it.
|
|
11
|
+
*/
|
|
12
|
+
usage?: GPUBufferUsageFlags;
|
|
13
|
+
/** How baseline shaders read it: a data texture (default), or a uniform block. */
|
|
14
|
+
kind?: 'texture' | 'uniform';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Loads the baseline tier's data textures, once. The render plugin does it as it opens a baseline
|
|
18
|
+
* device, before anything makes a DataStore; so does code that makes one on its own device.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadDataTextures(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* The layout entry a DataStore of `kind` binds with on this device's tier, for layouts made
|
|
23
|
+
* before (or without) the store: storage on the full tier, a data texture or a uniform block on
|
|
24
|
+
* baseline.
|
|
25
|
+
*/
|
|
26
|
+
export declare function dataEntry(gpu: GpuContext, binding: number, visibility: GPUShaderStageFlags, kind?: 'texture' | 'uniform', type?: GPUBufferBindingType): GPUBindGroupLayoutEntry;
|
|
27
|
+
/**
|
|
28
|
+
* Forgets the data textures `owner` made on `gpu` (an app's render plugin, as it disposes): the
|
|
29
|
+
* ledger destroys their textures, so they must not flush again.
|
|
30
|
+
*/
|
|
31
|
+
export declare function releaseDataStores(gpu: GpuContext, owner: string): void;
|
|
32
|
+
/** Uploads what every baseline data texture of `gpu` was written this frame. Before the submit. */
|
|
33
|
+
export declare function flushDataStores(gpu: GpuContext): void;
|
|
34
|
+
/**
|
|
35
|
+
* A binding of indexed engine data that a vertex or fragment shader reads. Same calls as
|
|
36
|
+
* GpuBuffer: `ensureCapacity`, `write`, `version` (bumps when bind groups must be rebuilt); plus
|
|
37
|
+
* the layout entry and resource of whichever form the tier binds.
|
|
38
|
+
*/
|
|
39
|
+
export declare class DataStore {
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly kind: 'texture' | 'uniform';
|
|
42
|
+
/** Who its GPU objects count against (`gpu.owner` when it was made). */
|
|
43
|
+
readonly owner: string;
|
|
44
|
+
/** The texture form: the baseline tier's `@data` binding. */
|
|
45
|
+
readonly textured: boolean;
|
|
46
|
+
private readonly gpu;
|
|
47
|
+
private readonly buffer;
|
|
48
|
+
private readonly texture;
|
|
49
|
+
constructor(gpu: GpuContext, options: DataStoreOptions);
|
|
50
|
+
/** Bumps whenever the GPU object changes: bind groups made on the old one rebuild. */
|
|
51
|
+
get version(): number;
|
|
52
|
+
get byteLength(): number;
|
|
53
|
+
/**
|
|
54
|
+
* The buffer, on the tiers that bind one (full, and baseline uniform blocks): for full-tier
|
|
55
|
+
* passes that read or write it as storage (GPU culling).
|
|
56
|
+
*/
|
|
57
|
+
get gpuBuffer(): GPUBuffer;
|
|
58
|
+
/** Grows to at least `bytes`. Returns true if the GPU object was replaced. */
|
|
59
|
+
ensureCapacity(bytes: number): boolean;
|
|
60
|
+
/** Writes a TypedArray (or a slice of it, in elements) at a byte offset, growing if needed. */
|
|
61
|
+
write(data: ArrayBufferView & ArrayLike<number>, byteOffset?: number, start?: number, count?: number): void;
|
|
62
|
+
/** The bind group layout entry for this binding on this tier. */
|
|
63
|
+
layoutEntry(binding: number, visibility: GPUShaderStageFlags, type?: GPUBufferBindingType): GPUBindGroupLayoutEntry;
|
|
64
|
+
/** What the bind group binds. Rebuild bind groups when `version` changes. */
|
|
65
|
+
resource(): GPUBindingResource;
|
|
66
|
+
/** Uploads the rows written since the last flush (data textures; the rest write through). */
|
|
67
|
+
flush(): void;
|
|
68
|
+
destroy(): void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
3
|
+
import { DATA_TEXTURE_WIDTH } from "@aethervtt/shard-shader";
|
|
4
|
+
const DATA_WIDTH = DATA_TEXTURE_WIDTH;
|
|
5
|
+
let dataTextures;
|
|
6
|
+
async function loadDataTextures() {
|
|
7
|
+
dataTextures ??= await import("./baseline/data-texture.js");
|
|
8
|
+
}
|
|
9
|
+
function dataEntry(gpu, binding, visibility, kind = "texture", type = "read-only-storage") {
|
|
10
|
+
if (gpu.tier !== "baseline") return { binding, visibility, buffer: { type } };
|
|
11
|
+
if (kind === "uniform") return { binding, visibility, buffer: { type: "uniform" } };
|
|
12
|
+
return { binding, visibility, texture: { sampleType: "uint", viewDimension: "2d" } };
|
|
13
|
+
}
|
|
14
|
+
function releaseDataStores(gpu, owner) {
|
|
15
|
+
dataTextures?.releaseDataTextures(gpu, owner);
|
|
16
|
+
}
|
|
17
|
+
function flushDataStores(gpu) {
|
|
18
|
+
dataTextures?.flushDataTextures(gpu);
|
|
19
|
+
}
|
|
20
|
+
class DataStore {
|
|
21
|
+
label;
|
|
22
|
+
kind;
|
|
23
|
+
/** Who its GPU objects count against (`gpu.owner` when it was made). */
|
|
24
|
+
owner;
|
|
25
|
+
/** The texture form: the baseline tier's `@data` binding. */
|
|
26
|
+
textured;
|
|
27
|
+
gpu;
|
|
28
|
+
buffer;
|
|
29
|
+
texture;
|
|
30
|
+
constructor(gpu, options) {
|
|
31
|
+
this.gpu = gpu;
|
|
32
|
+
this.label = options.label;
|
|
33
|
+
this.owner = gpu.owner;
|
|
34
|
+
this.kind = options.kind ?? "texture";
|
|
35
|
+
const size = align16(Math.max(16, options.size ?? 256));
|
|
36
|
+
this.textured = gpu.tier === "baseline" && this.kind === "texture";
|
|
37
|
+
if (this.textured) {
|
|
38
|
+
if (!dataTextures) {
|
|
39
|
+
throw new ShardError(
|
|
40
|
+
"render/data-textures-not-loaded",
|
|
41
|
+
`DataStore "${options.label}" needs the baseline tier's data textures, which aren't loaded`,
|
|
42
|
+
{ hint: "Await loadDataTextures() before making DataStores on a baseline device." }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
this.texture = new dataTextures.DataTexture(gpu, options.label, size);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.buffer = new GpuBuffer(gpu, {
|
|
49
|
+
label: options.label,
|
|
50
|
+
usage: (gpu.tier === "baseline" ? GPUBufferUsage.UNIFORM : GPUBufferUsage.STORAGE) | (options.usage ?? 0),
|
|
51
|
+
size
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Bumps whenever the GPU object changes: bind groups made on the old one rebuild. */
|
|
55
|
+
get version() {
|
|
56
|
+
return this.buffer ? this.buffer.version : this.texture.currentVersion;
|
|
57
|
+
}
|
|
58
|
+
get byteLength() {
|
|
59
|
+
return this.buffer ? this.buffer.byteLength : this.texture.size;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The buffer, on the tiers that bind one (full, and baseline uniform blocks): for full-tier
|
|
63
|
+
* passes that read or write it as storage (GPU culling).
|
|
64
|
+
*/
|
|
65
|
+
get gpuBuffer() {
|
|
66
|
+
if (!this.buffer) throw new Error(`${this.label} is a data texture on this tier`);
|
|
67
|
+
return this.buffer.buffer;
|
|
68
|
+
}
|
|
69
|
+
/** Grows to at least `bytes`. Returns true if the GPU object was replaced. */
|
|
70
|
+
ensureCapacity(bytes) {
|
|
71
|
+
return this.buffer ? this.buffer.ensureCapacity(bytes) : this.texture.ensureCapacity(bytes);
|
|
72
|
+
}
|
|
73
|
+
/** Writes a TypedArray (or a slice of it, in elements) at a byte offset, growing if needed. */
|
|
74
|
+
write(data, byteOffset = 0, start = 0, count) {
|
|
75
|
+
if (this.buffer) this.buffer.write(data, byteOffset, start, count);
|
|
76
|
+
else this.texture.write(data, byteOffset, start, count);
|
|
77
|
+
}
|
|
78
|
+
/** The bind group layout entry for this binding on this tier. */
|
|
79
|
+
layoutEntry(binding, visibility, type = "read-only-storage") {
|
|
80
|
+
return dataEntry(this.gpu, binding, visibility, this.kind, type);
|
|
81
|
+
}
|
|
82
|
+
/** What the bind group binds. Rebuild bind groups when `version` changes. */
|
|
83
|
+
resource() {
|
|
84
|
+
return this.buffer ? { buffer: this.buffer.buffer } : this.texture.resource();
|
|
85
|
+
}
|
|
86
|
+
/** Uploads the rows written since the last flush (data textures; the rest write through). */
|
|
87
|
+
flush() {
|
|
88
|
+
this.texture?.flush();
|
|
89
|
+
}
|
|
90
|
+
destroy() {
|
|
91
|
+
this.buffer?.destroy();
|
|
92
|
+
this.texture?.destroy();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function align16(n) {
|
|
96
|
+
return n + 15 & ~15;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
DATA_WIDTH,
|
|
100
|
+
DataStore,
|
|
101
|
+
dataEntry,
|
|
102
|
+
flushDataStores,
|
|
103
|
+
loadDataTextures,
|
|
104
|
+
releaseDataStores
|
|
105
|
+
};
|
package/dist/debug-shaders.js
CHANGED
|
@@ -37,7 +37,7 @@ struct Line {
|
|
|
37
37
|
bits: u32,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
@group(1) @binding(0) var<storage, read> lines: array<Line>;
|
|
40
|
+
@data @group(1) @binding(0) var<storage, read> lines: array<Line>;
|
|
41
41
|
|
|
42
42
|
struct LineOutput {
|
|
43
43
|
@builtin(position) clip: vec4f,
|
|
@@ -112,7 +112,7 @@ struct Glyph {
|
|
|
112
112
|
_pad1: u32,
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
@group(1) @binding(1) var<storage, read> glyphs: array<Glyph>;
|
|
115
|
+
@data @group(1) @binding(1) var<storage, read> glyphs: array<Glyph>;
|
|
116
116
|
@group(1) @binding(3) var atlas: texture_2d<f32>;
|
|
117
117
|
|
|
118
118
|
struct GlyphOutput {
|
|
@@ -167,11 +167,21 @@ struct PickOutput {
|
|
|
167
167
|
import shard::view::view;
|
|
168
168
|
import shard::mesh::{ instance_at, mesh_vertex_at };
|
|
169
169
|
import shard::pick::types::PickOutput;
|
|
170
|
+
@if(CUTAWAY) import shard::cutaway::cutaway_clip;
|
|
170
171
|
|
|
171
172
|
struct PickVertex {
|
|
172
173
|
@builtin(position) clip: vec4f,
|
|
173
174
|
@location(0) normal: vec3f,
|
|
174
175
|
@location(1) @interpolate(flat) entity: u32,
|
|
176
|
+
/**
|
|
177
|
+
* Clip z and w, interpolated perspective-correct: z / w is the surface's own depth. The
|
|
178
|
+
* fragment's position.z includes the depth bias ground bands draw with (0057), which would move
|
|
179
|
+
* a picked point toward the camera.
|
|
180
|
+
*/
|
|
181
|
+
@location(2) zw: vec2f,
|
|
182
|
+
/** Where cutaways (0070) test the surface. */
|
|
183
|
+
@if(CUTAWAY) @location(3) world: vec3f,
|
|
184
|
+
@if(CUTAWAY) @location(4) @interpolate(flat) flags: u32,
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
@vertex fn vs(
|
|
@@ -190,14 +200,18 @@ struct PickVertex {
|
|
|
190
200
|
out.clip = view.viewProjNoJitter * vec4f(m.world_position, 1.0);
|
|
191
201
|
out.normal = m.world_normal;
|
|
192
202
|
out.entity = inst.entity;
|
|
203
|
+
out.zw = out.clip.zw;
|
|
204
|
+
@if(CUTAWAY) out.world = m.world_position;
|
|
205
|
+
@if(CUTAWAY) out.flags = m.flags;
|
|
193
206
|
return out;
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
@fragment fn fs(in: PickVertex, @builtin(front_facing) front: bool) -> PickOutput {
|
|
210
|
+
@if(CUTAWAY) cutaway_clip(in.world, in.clip.xy, in.flags);
|
|
197
211
|
var out: PickOutput;
|
|
198
212
|
out.id = in.entity;
|
|
199
213
|
let n = normalize(in.normal);
|
|
200
|
-
out.normal = vec4f(select(-n, n, front), in.
|
|
214
|
+
out.normal = vec4f(select(-n, n, front), in.zw.x / in.zw.y);
|
|
201
215
|
return out;
|
|
202
216
|
}`
|
|
203
217
|
};
|
package/dist/debug-views.js
CHANGED
|
@@ -161,8 +161,10 @@ function describeLighting(world) {
|
|
|
161
161
|
})),
|
|
162
162
|
clusters: {
|
|
163
163
|
grid: [16, 9, 24],
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
// GPU clustering's stats; baseline bins on the CPU and drops lights past its budget.
|
|
165
|
+
maxLightsPerCluster: pv.clusters?.latest.maxLightsPerCluster ?? null,
|
|
166
|
+
overflows: pv.clusters?.latest.overflows ?? 0,
|
|
167
|
+
baselineLights: pv.baseline ? { packed: pv.baseline.count, dropped: pv.baseline.dropped } : null
|
|
166
168
|
},
|
|
167
169
|
cascades: pv.cascades.count,
|
|
168
170
|
cascadeSplits: [...pv.cascades.splits.subarray(0, pv.cascades.count)]
|