@aethervtt/shard-render 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ablation.d.ts +74 -0
- package/dist/ablation.js +117 -0
- 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 +157 -30
- package/dist/atmosphere-plugin.js +8 -2
- package/dist/atmosphere-shaders.js +17 -5
- 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 +133 -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/crowd.d.ts +21 -0
- package/dist/crowd.js +121 -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/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-shaders.js +2 -0
- package/dist/deferred.js +28 -4
- 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 +37 -13
- 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/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.d.ts +38 -2
- package/dist/forward.js +608 -186
- 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 +45 -0
- package/dist/graph.js +169 -19
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +40 -11
- package/dist/index.js +343 -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/overlay-registry.d.ts +3 -1
- package/dist/overlay-registry.js +3 -1
- package/dist/overlays.js +6 -2
- package/dist/perf-overlay.d.ts +9 -0
- package/dist/perf-overlay.js +125 -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 +69 -7
- package/dist/plugin.js +223 -25
- package/dist/post-common.d.ts +22 -2
- package/dist/post-common.js +82 -2
- package/dist/post-nodes.d.ts +7 -1
- package/dist/post-nodes.js +235 -111
- package/dist/post-plugin.js +5 -3
- package/dist/post-scene.d.ts +130 -0
- package/dist/post-scene.js +164 -0
- package/dist/post-shaders.js +85 -62
- package/dist/post.js +11 -1
- 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/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/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +189 -34
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +27 -2
- package/dist/shadows.js +100 -20
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +18 -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 +11 -0
- package/dist/testing.js +39 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +18 -1
- package/dist/timer.js +73 -20
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -0
- 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/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 +23 -11
package/dist/lights.js
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
9
9
|
import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
|
|
10
10
|
import { LightPresets } from "./camera.js";
|
|
11
|
+
import { DataStore } from "./data-store.js";
|
|
12
|
+
import { warnFeatureMissing } from "./features.js";
|
|
13
|
+
import { InteriorPath } from "./interior.js";
|
|
11
14
|
const LuminousPowerPresets = {
|
|
12
15
|
candle: 12,
|
|
13
16
|
"bulb-40w": 450,
|
|
@@ -33,6 +36,10 @@ const shadowFields = (defaults) => ({
|
|
|
33
36
|
description: "PCF filter radius in texels beyond the base 3x3 (0 = 3x3). Softer edges."
|
|
34
37
|
})
|
|
35
38
|
});
|
|
39
|
+
const SHADOW_UPDATES = ["always", "on-change"];
|
|
40
|
+
const shadowUpdateField = () => t.enum(SHADOW_UPDATES, {
|
|
41
|
+
description: "always: shadow maps redraw every frame. on-change: they redraw only when the light or the camera's cascade fit moved, or a shadow caster inside them changed (0055); a still scene draws none."
|
|
42
|
+
});
|
|
36
43
|
const CascadeSettings = t.struct(
|
|
37
44
|
{
|
|
38
45
|
count: t.u8({ default: 4, min: 1, max: 4, description: "Number of cascades." }),
|
|
@@ -63,6 +70,7 @@ const DirectionalLight = defineComponent(
|
|
|
63
70
|
description: "Presets: direct-sun 100000, daylight 10000, overcast 1000, indoor 400, twilight 10, moonlight 0.3."
|
|
64
71
|
}),
|
|
65
72
|
...shadowFields({ bias: 0.02, normalBias: 1.5 }),
|
|
73
|
+
shadowUpdate: shadowUpdateField(),
|
|
66
74
|
cascades: CascadeSettings,
|
|
67
75
|
angularDiameter: t.f32({
|
|
68
76
|
default: 0.53,
|
|
@@ -96,21 +104,41 @@ const pointFields = {
|
|
|
96
104
|
min: 0,
|
|
97
105
|
unit: "m",
|
|
98
106
|
description: "Emitter size: softens specular highlights."
|
|
107
|
+
}),
|
|
108
|
+
blockedByWalls: t.bool({
|
|
109
|
+
description: "Occluded by its level's walls and closed doors (0069): structure's interiorLightingPlugin gives it a polar row from the plan, with no shadow-map budget. It lights only its own level. Without the plugin it lights through walls."
|
|
99
110
|
})
|
|
100
111
|
};
|
|
112
|
+
const FALLOFFS = ["physical", "tabletop"];
|
|
101
113
|
const PointLight = defineComponent(
|
|
102
114
|
"render/PointLight",
|
|
103
|
-
{ ...pointFields, ...shadowFields({ bias: 0.02, normalBias: 1 }) },
|
|
104
115
|
{
|
|
105
|
-
|
|
116
|
+
...pointFields,
|
|
117
|
+
...shadowFields({ bias: 0.02, normalBias: 1 }),
|
|
118
|
+
shadowUpdate: shadowUpdateField(),
|
|
119
|
+
falloff: t.enum(FALLOFFS, {
|
|
120
|
+
description: "physical: inverse square, windowed to zero at range. tabletop (0057): full intensity inside bright, then linear to zero at range (a VTT light's bright and dim radii)."
|
|
121
|
+
}),
|
|
122
|
+
bright: t.f32({
|
|
123
|
+
min: 0,
|
|
124
|
+
unit: "m",
|
|
125
|
+
description: "Tabletop falloff: the radius of full intensity. Past it, light fades linearly to zero at range."
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
description: "Light emitted equally in all directions, in lumens (intensity lm / 4\u03C0 candela), with physical inverse-square falloff windowed to zero at range, or tabletop falloff (bright, then dim to range).",
|
|
106
130
|
requires: [Transform]
|
|
107
131
|
}
|
|
108
132
|
);
|
|
133
|
+
function tabletopFalloff(distance, bright, range) {
|
|
134
|
+
return Math.min(1, Math.max(0, (range - distance) / Math.max(range - bright, 1e-4)));
|
|
135
|
+
}
|
|
109
136
|
const SpotLight = defineComponent(
|
|
110
137
|
"render/SpotLight",
|
|
111
138
|
{
|
|
112
139
|
...pointFields,
|
|
113
140
|
...shadowFields({ bias: 0.02, normalBias: 1 }),
|
|
141
|
+
shadowUpdate: shadowUpdateField(),
|
|
114
142
|
innerAngle: t.f32({
|
|
115
143
|
default: 30,
|
|
116
144
|
min: 0,
|
|
@@ -133,22 +161,26 @@ const SpotLight = defineComponent(
|
|
|
133
161
|
);
|
|
134
162
|
const AmbientLight = defineResource("render/AmbientLight", {
|
|
135
163
|
description: "Uniform fill light, in cd/m\xB2. Used when no environment map lights the view.",
|
|
136
|
-
init: () => ({ color: [1, 1, 1], brightness: 0 })
|
|
164
|
+
init: () => ({ color: [1, 1, 1], brightness: 0 }),
|
|
165
|
+
hostWritable: true
|
|
137
166
|
});
|
|
138
167
|
const LightingSettings = defineResource("render/LightingSettings", {
|
|
139
|
-
description: "Light and shadow budgets: light buffer size, cluster range, shadow map sizes.",
|
|
168
|
+
description: "Light and shadow budgets: light buffer size, cluster range, shadow map sizes, and the baseline tier's lights per view.",
|
|
169
|
+
hostWritable: true,
|
|
140
170
|
init: () => ({
|
|
141
171
|
maxLights: 1024,
|
|
142
172
|
clusterFar: 0,
|
|
143
173
|
cascadeMapSize: 2048,
|
|
144
174
|
shadowMapSize: 1024,
|
|
145
175
|
maxShadowedPoints: 4,
|
|
146
|
-
maxShadowedSpots: 8
|
|
176
|
+
maxShadowedSpots: 8,
|
|
177
|
+
baselineMaxLights: 128
|
|
147
178
|
})
|
|
148
179
|
});
|
|
149
180
|
const LIGHT_FLOATS = 20;
|
|
150
181
|
const LIGHT_KIND = { point: 0, spot: 1 };
|
|
151
182
|
const MAX_DIRECTIONAL = 4;
|
|
183
|
+
const LIGHT_ROW_WORD = { point: 12, spot: 19 };
|
|
152
184
|
class LightStore {
|
|
153
185
|
records = [];
|
|
154
186
|
byEntity = /* @__PURE__ */ new Map();
|
|
@@ -162,9 +194,13 @@ class LightStore {
|
|
|
162
194
|
uploadedBytes = 0;
|
|
163
195
|
buffer;
|
|
164
196
|
/** Directional lights: count + up to 4 × (direction, shadow cascade flag, color × lux, disk radius). */
|
|
197
|
+
/** Directional lights: `@data(uniform)`, a uniform block on baseline. */
|
|
165
198
|
directional;
|
|
166
199
|
directionalData = new Float32Array(4 + MAX_DIRECTIONAL * 8);
|
|
167
200
|
directionalU32 = new Uint32Array(this.directionalData.buffer);
|
|
201
|
+
/** What the directional buffer holds, so an unchanged frame writes nothing (0055). */
|
|
202
|
+
directionalSent = new Float32Array(this.directionalData.length);
|
|
203
|
+
directionalStale = true;
|
|
168
204
|
/** The first directional light with shadows: entity, and its settings. */
|
|
169
205
|
shadowSun = null;
|
|
170
206
|
directionalCount = 0;
|
|
@@ -181,9 +217,9 @@ class LightStore {
|
|
|
181
217
|
usage: GPUBufferUsage.STORAGE,
|
|
182
218
|
size: capacity * LIGHT_FLOATS * 4
|
|
183
219
|
});
|
|
184
|
-
this.directional = new
|
|
220
|
+
this.directional = new DataStore(gpu, {
|
|
185
221
|
label: "lights/directional",
|
|
186
|
-
|
|
222
|
+
kind: "uniform",
|
|
187
223
|
size: this.directionalData.byteLength
|
|
188
224
|
});
|
|
189
225
|
}
|
|
@@ -217,7 +253,9 @@ class LightStore {
|
|
|
217
253
|
shadowIndex: -1,
|
|
218
254
|
outerAngle: 0,
|
|
219
255
|
radius: 0,
|
|
220
|
-
alive: true
|
|
256
|
+
alive: true,
|
|
257
|
+
cachedShadow: false,
|
|
258
|
+
row: -1
|
|
221
259
|
};
|
|
222
260
|
this.records[slot] = r;
|
|
223
261
|
this.byEntity.set(entity, r);
|
|
@@ -251,28 +289,39 @@ class LightStore {
|
|
|
251
289
|
if (this.generation !== this.gpu.generation) {
|
|
252
290
|
this.generation = this.gpu.generation;
|
|
253
291
|
this.dirty.fill(1, 0, this.high);
|
|
292
|
+
this.directionalStale = true;
|
|
254
293
|
}
|
|
255
294
|
let lights = 0;
|
|
256
295
|
let runStart = -1;
|
|
257
296
|
for (let s = 0; s <= this.high; s++) {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
297
|
+
const d2 = s < this.high && this.dirty[s] === 1;
|
|
298
|
+
if (d2) {
|
|
260
299
|
this.dirty[s] = 0;
|
|
261
300
|
lights++;
|
|
262
301
|
if (runStart < 0) runStart = s;
|
|
263
302
|
} else if (runStart >= 0) {
|
|
264
|
-
this.
|
|
265
|
-
this.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
303
|
+
if (this.gpu.tier === "full") {
|
|
304
|
+
this.buffer.write(
|
|
305
|
+
this.data,
|
|
306
|
+
runStart * LIGHT_FLOATS * 4,
|
|
307
|
+
runStart * LIGHT_FLOATS,
|
|
308
|
+
(s - runStart) * LIGHT_FLOATS
|
|
309
|
+
);
|
|
310
|
+
}
|
|
270
311
|
runStart = -1;
|
|
271
312
|
}
|
|
272
313
|
}
|
|
273
314
|
this.uploadedLights = lights;
|
|
274
315
|
this.uploadedBytes = lights * LIGHT_FLOATS * 4;
|
|
275
|
-
this.
|
|
316
|
+
const d = this.directionalData;
|
|
317
|
+
const sent = this.directionalSent;
|
|
318
|
+
let same = !this.directionalStale;
|
|
319
|
+
for (let i = 0; same && i < d.length; i++) if (!Object.is(d[i], sent[i])) same = false;
|
|
320
|
+
if (!same) {
|
|
321
|
+
this.directional.write(d);
|
|
322
|
+
sent.set(d);
|
|
323
|
+
this.directionalStale = false;
|
|
324
|
+
}
|
|
276
325
|
}
|
|
277
326
|
}
|
|
278
327
|
const Lights = defineResource("render/Lights", {
|
|
@@ -281,7 +330,7 @@ const Lights = defineResource("render/Lights", {
|
|
|
281
330
|
const POINT_KIND = LIGHT_KIND.point;
|
|
282
331
|
const SPOT_KIND = LIGHT_KIND.spot;
|
|
283
332
|
const INV_4PI = 1 / (4 * Math.PI);
|
|
284
|
-
function writeLight(store, r, g, o, color, c, intensity, range, radius, bias, normalBias, softness, inner, outer) {
|
|
333
|
+
function writeLight(store, r, g, o, color, c, intensity, range, radius, bias, normalBias, softness, inner, outer, falloff = 0, bright = 0) {
|
|
285
334
|
const s = r.slot;
|
|
286
335
|
r.x = g[o + 3];
|
|
287
336
|
r.y = g[o + 7];
|
|
@@ -318,7 +367,7 @@ function writeLight(store, r, g, o, color, c, intensity, range, radius, bias, no
|
|
|
318
367
|
r.cosOuter = cosOuter;
|
|
319
368
|
r.outerAngle = outer;
|
|
320
369
|
} else {
|
|
321
|
-
store.set(s, 12,
|
|
370
|
+
store.set(s, 12, r.row + 1);
|
|
322
371
|
store.set(s, 13, 1);
|
|
323
372
|
r.cosOuter = -2;
|
|
324
373
|
}
|
|
@@ -326,6 +375,16 @@ function writeLight(store, r, g, o, color, c, intensity, range, radius, bias, no
|
|
|
326
375
|
store.set(s, 15, bias);
|
|
327
376
|
store.set(s, 16, normalBias);
|
|
328
377
|
store.set(s, 17, softness);
|
|
378
|
+
store.set(s, 18, falloff);
|
|
379
|
+
store.set(s, 19, r.kind === SPOT_KIND ? r.row + 1 : bright);
|
|
380
|
+
}
|
|
381
|
+
function warnBlockedWithoutPlugin(world) {
|
|
382
|
+
warnFeatureMissing(
|
|
383
|
+
world,
|
|
384
|
+
"A light has blockedByWalls",
|
|
385
|
+
"interiorPlugin",
|
|
386
|
+
"it lights through walls"
|
|
387
|
+
);
|
|
329
388
|
}
|
|
330
389
|
const extractLights = defineSystem({
|
|
331
390
|
name: "render/extract-lights",
|
|
@@ -339,6 +398,7 @@ const extractLights = defineSystem({
|
|
|
339
398
|
run: ({ directional, points, spots, seen }, world, ctx) => {
|
|
340
399
|
const store = world.resource(Lights);
|
|
341
400
|
const since = ctx.lastRunTick;
|
|
401
|
+
const interior = world.hasResource(InteriorPath);
|
|
342
402
|
seen.clear();
|
|
343
403
|
for (const [def, kind, q] of [
|
|
344
404
|
[PointLight, POINT_KIND, points],
|
|
@@ -359,7 +419,11 @@ const extractLights = defineSystem({
|
|
|
359
419
|
const normalBias = table.column(def, "shadowNormalBias");
|
|
360
420
|
const softness = table.column(def, "shadowSoftness");
|
|
361
421
|
const inner = kind === SPOT_KIND ? table.column(SpotLight, "innerAngle") : void 0;
|
|
422
|
+
const update = table.column(def, "shadowUpdate");
|
|
423
|
+
const falloff = kind === POINT_KIND ? table.column(PointLight, "falloff") : void 0;
|
|
424
|
+
const bright = kind === POINT_KIND ? table.column(PointLight, "bright") : void 0;
|
|
362
425
|
const outer = kind === SPOT_KIND ? table.column(SpotLight, "outerAngle") : void 0;
|
|
426
|
+
const blocked = table.column(def, "blockedByWalls");
|
|
363
427
|
for (let i = 0; i < n; i++) {
|
|
364
428
|
const entity = table.entities[i];
|
|
365
429
|
seen.add(entity);
|
|
@@ -367,7 +431,9 @@ const extractLights = defineSystem({
|
|
|
367
431
|
const r = store.record(entity, kind);
|
|
368
432
|
if (!r) continue;
|
|
369
433
|
r.shadows = shadows[i] !== 0;
|
|
434
|
+
r.cachedShadow = update[i] === 1;
|
|
370
435
|
if (existing === r && gChanged[i] <= since && changed[i] <= since) continue;
|
|
436
|
+
if (blocked[i] !== 0 && !interior) warnBlockedWithoutPlugin(world);
|
|
371
437
|
writeLight(
|
|
372
438
|
store,
|
|
373
439
|
r,
|
|
@@ -382,7 +448,9 @@ const extractLights = defineSystem({
|
|
|
382
448
|
normalBias[i],
|
|
383
449
|
softness[i],
|
|
384
450
|
inner ? inner[i] : 0,
|
|
385
|
-
outer ? outer[i] : 90
|
|
451
|
+
outer ? outer[i] : 90,
|
|
452
|
+
falloff ? falloff[i] : 0,
|
|
453
|
+
bright ? bright[i] : 0
|
|
386
454
|
);
|
|
387
455
|
}
|
|
388
456
|
}
|
|
@@ -426,7 +494,8 @@ const extractLights = defineSystem({
|
|
|
426
494
|
splitLambda: c.splitLambda,
|
|
427
495
|
bias: table.column(DirectionalLight, "shadowBias")[i],
|
|
428
496
|
normalBias: table.column(DirectionalLight, "shadowNormalBias")[i],
|
|
429
|
-
softness: table.column(DirectionalLight, "shadowSoftness")[i]
|
|
497
|
+
softness: table.column(DirectionalLight, "shadowSoftness")[i],
|
|
498
|
+
cached: table.column(DirectionalLight, "shadowUpdate")[i] === 1
|
|
430
499
|
};
|
|
431
500
|
}
|
|
432
501
|
count++;
|
|
@@ -443,6 +512,11 @@ function observeLightRemovals(world) {
|
|
|
443
512
|
});
|
|
444
513
|
}
|
|
445
514
|
}
|
|
515
|
+
function setLightRow(store, r, row) {
|
|
516
|
+
if (r.row === row) return;
|
|
517
|
+
r.row = row;
|
|
518
|
+
store.set(r.slot, r.kind === SPOT_KIND ? LIGHT_ROW_WORD.spot : LIGHT_ROW_WORD.point, row + 1);
|
|
519
|
+
}
|
|
446
520
|
function setShadowIndex(store, r, index) {
|
|
447
521
|
if (r.shadowIndex === index) return;
|
|
448
522
|
r.shadowIndex = index;
|
|
@@ -452,17 +526,22 @@ export {
|
|
|
452
526
|
AmbientLight,
|
|
453
527
|
CascadeSettings,
|
|
454
528
|
DirectionalLight,
|
|
529
|
+
FALLOFFS,
|
|
455
530
|
LIGHT_FLOATS,
|
|
456
531
|
LIGHT_KIND,
|
|
532
|
+
LIGHT_ROW_WORD,
|
|
457
533
|
LightStore,
|
|
458
534
|
LightingSettings,
|
|
459
535
|
Lights,
|
|
460
536
|
LuminousPowerPresets,
|
|
461
537
|
MAX_DIRECTIONAL,
|
|
462
538
|
PointLight,
|
|
539
|
+
SHADOW_UPDATES,
|
|
463
540
|
SpotLight,
|
|
464
541
|
extractLights,
|
|
465
542
|
lumens,
|
|
466
543
|
observeLightRemovals,
|
|
467
|
-
|
|
544
|
+
setLightRow,
|
|
545
|
+
setShadowIndex,
|
|
546
|
+
tabletopFalloff
|
|
468
547
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type World } from '@aethervtt/shard-core';
|
|
1
|
+
import { ShardError, type World } from '@aethervtt/shard-core';
|
|
2
2
|
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
3
3
|
import type { ShaderLibrary } from '@aethervtt/shard-shader';
|
|
4
|
-
import
|
|
4
|
+
import { MaterialAsset } from './assets.js';
|
|
5
5
|
import { type BlendMode, type MaterialType } from './materials.js';
|
|
6
6
|
/** A small stable number per material type, for sorting and pipeline keys. */
|
|
7
7
|
export declare function typeOrdinal(type: MaterialType): number;
|
|
@@ -25,16 +25,49 @@ export declare function registerMaterialModule(library: ShaderLibrary, type: Mat
|
|
|
25
25
|
*/
|
|
26
26
|
export declare class MaterialPipelines {
|
|
27
27
|
private frame;
|
|
28
|
+
private revision;
|
|
28
29
|
private readonly modules;
|
|
29
30
|
private readonly pipelines;
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Types whose shader or pipeline failed (0061), with the type version that failed: their draws
|
|
33
|
+
* use the standard pipeline through a proxy material until the type is defined again.
|
|
34
|
+
*/
|
|
35
|
+
private readonly failedTypes;
|
|
36
|
+
private readonly proxies;
|
|
37
|
+
/** `revision`: the shader library's; any edit gives failed types another try (0061). */
|
|
38
|
+
beginFrame(frame: number, revision?: number): void;
|
|
31
39
|
/**
|
|
32
40
|
* The module for a type linked from `root` with its shader's overrides. `slot` distinguishes
|
|
33
41
|
* roots and define sets (a small integer chosen by the caller).
|
|
34
42
|
*/
|
|
35
43
|
module(world: World, gpu: GpuContext, type: MaterialType, slot: number, root: string, defines: Readonly<Record<string, boolean>> | undefined): GPUShaderModule | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the type's shader or pipeline failed (0061). Callers then draw its batches with
|
|
46
|
+
* `proxy(material)` and the standard pipeline, so nothing using it disappears.
|
|
47
|
+
*/
|
|
48
|
+
failing(type: MaterialType): boolean;
|
|
49
|
+
/** How many types draw through the standard fallback now. */
|
|
50
|
+
get failureCount(): number;
|
|
51
|
+
/** Types drawing through the standard fallback now, with why (for RenderHealth). */
|
|
52
|
+
failures(): {
|
|
53
|
+
type: string;
|
|
54
|
+
error: ShardError;
|
|
55
|
+
}[];
|
|
56
|
+
/**
|
|
57
|
+
* A standard material standing in for one whose type failed: its standard fields when the type
|
|
58
|
+
* extends standard, else its `baseColor` or `color` if it has one. Kept per material and
|
|
59
|
+
* refreshed when the material changes.
|
|
60
|
+
*/
|
|
61
|
+
proxy(material: MaterialAsset): MaterialAsset;
|
|
36
62
|
/** A compiled pipeline under `key` (unique per pass, type, and variant), if there is one. */
|
|
37
63
|
cached(key: number): GPURenderPipeline | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Per (pass, sample count) slot of CPU-culled lists: the batch structure version and device
|
|
66
|
+
* generation whose pipelines were all asked for and compiled (forward's warmPipelines).
|
|
67
|
+
*/
|
|
68
|
+
private readonly warm;
|
|
69
|
+
warmed(slot: number, structure: number, generation: number): boolean;
|
|
70
|
+
markWarmed(slot: number, structure: number, generation: number): void;
|
|
38
71
|
/** The last pipeline that compiled under each key, with the layout it was made for. */
|
|
39
72
|
private readonly previous;
|
|
40
73
|
/**
|
|
@@ -42,5 +75,5 @@ export declare class MaterialPipelines {
|
|
|
42
75
|
* While a replacement compiles (an edited shader or noise graph), draws keep the previous one
|
|
43
76
|
* for the key if its layout is the same object (the bind groups still fit), instead of vanishing.
|
|
44
77
|
*/
|
|
45
|
-
create(gpu: GpuContext, key: number, descriptor: GPURenderPipelineDescriptor): GPURenderPipeline | undefined;
|
|
78
|
+
create(gpu: GpuContext, key: number, descriptor: GPURenderPipelineDescriptor, type?: MaterialType): GPURenderPipeline | undefined;
|
|
46
79
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
2
|
import { ShardError } from "@aethervtt/shard-core";
|
|
3
3
|
import { LogResource } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { MaterialAsset } from "./assets.js";
|
|
4
5
|
import { MaterialNoise } from "./material-noise.js";
|
|
5
6
|
import { allMaterialTypes, BLEND_MODES } from "./materials.js";
|
|
6
7
|
import { Shaders } from "./plugin.js";
|
|
8
|
+
import { STANDARD_FIELDS } from "./standard-fields.js";
|
|
7
9
|
const ordinals = /* @__PURE__ */ new WeakMap();
|
|
8
10
|
let nextOrdinal = 0;
|
|
9
11
|
function typeOrdinal(type) {
|
|
@@ -78,11 +80,25 @@ function watchNoiseGraphs(world, library) {
|
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
const missingNoise = /* @__PURE__ */ new WeakSet();
|
|
83
|
+
const missingPlugin = /* @__PURE__ */ new WeakSet();
|
|
81
84
|
class MaterialPipelines {
|
|
82
85
|
frame = -1;
|
|
86
|
+
revision = -1;
|
|
83
87
|
modules = /* @__PURE__ */ new Map();
|
|
84
88
|
pipelines = /* @__PURE__ */ new Map();
|
|
85
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Types whose shader or pipeline failed (0061), with the type version that failed: their draws
|
|
91
|
+
* use the standard pipeline through a proxy material until the type is defined again.
|
|
92
|
+
*/
|
|
93
|
+
failedTypes = /* @__PURE__ */ new Map();
|
|
94
|
+
proxies = /* @__PURE__ */ new WeakMap();
|
|
95
|
+
/** `revision`: the shader library's; any edit gives failed types another try (0061). */
|
|
96
|
+
beginFrame(frame, revision = this.revision) {
|
|
97
|
+
if (revision !== this.revision) {
|
|
98
|
+
this.revision = revision;
|
|
99
|
+
this.failedTypes.clear();
|
|
100
|
+
this.warm.clear();
|
|
101
|
+
}
|
|
86
102
|
if (frame === this.frame) return;
|
|
87
103
|
this.frame = frame;
|
|
88
104
|
this.modules.clear();
|
|
@@ -93,7 +109,7 @@ class MaterialPipelines {
|
|
|
93
109
|
* roots and define sets (a small integer chosen by the caller).
|
|
94
110
|
*/
|
|
95
111
|
module(world, gpu, type, slot, root, defines) {
|
|
96
|
-
const key = typeOrdinal(type) *
|
|
112
|
+
const key = typeOrdinal(type) * 128 + slot;
|
|
97
113
|
if (this.modules.has(key)) return this.modules.get(key);
|
|
98
114
|
const library = world.resource(Shaders);
|
|
99
115
|
let noise;
|
|
@@ -120,20 +136,94 @@ class MaterialPipelines {
|
|
|
120
136
|
return void 0;
|
|
121
137
|
}
|
|
122
138
|
}
|
|
139
|
+
if (type.plugin && type.shader && !library.has(type.shader)) {
|
|
140
|
+
const error = new ShardError(
|
|
141
|
+
"render/feature-missing",
|
|
142
|
+
`Material ${type.name} needs ${type.plugin.split(" ")[0]}, which isn't installed`,
|
|
143
|
+
{ hint: `Add ${type.plugin}.` }
|
|
144
|
+
);
|
|
145
|
+
if (!missingPlugin.has(type)) {
|
|
146
|
+
missingPlugin.add(type);
|
|
147
|
+
world.tryResource(LogResource)?.error(error);
|
|
148
|
+
}
|
|
149
|
+
this.failedTypes.set(type, { version: type.version, error });
|
|
150
|
+
this.modules.set(key, void 0);
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
123
153
|
registerMaterialModule(library, type, noise);
|
|
124
|
-
const
|
|
154
|
+
const request = {
|
|
125
155
|
root,
|
|
126
156
|
defines,
|
|
127
157
|
overrides: type.shader ? [type.shader] : void 0,
|
|
128
158
|
label: type.name === "render/StandardMaterial" ? void 0 : `material ${type.name}`
|
|
129
|
-
}
|
|
159
|
+
};
|
|
160
|
+
const module = library.module(gpu, request);
|
|
161
|
+
if (!module && type.name !== "render/StandardMaterial") {
|
|
162
|
+
const error = library.failure(request, gpu);
|
|
163
|
+
if (error) this.failedTypes.set(type, { version: type.version, error });
|
|
164
|
+
}
|
|
130
165
|
this.modules.set(key, module);
|
|
131
166
|
return module;
|
|
132
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Whether the type's shader or pipeline failed (0061). Callers then draw its batches with
|
|
170
|
+
* `proxy(material)` and the standard pipeline, so nothing using it disappears.
|
|
171
|
+
*/
|
|
172
|
+
failing(type) {
|
|
173
|
+
if (this.failedTypes.size === 0) return false;
|
|
174
|
+
const f = this.failedTypes.get(type);
|
|
175
|
+
return f !== void 0 && f.version === type.version;
|
|
176
|
+
}
|
|
177
|
+
/** How many types draw through the standard fallback now. */
|
|
178
|
+
get failureCount() {
|
|
179
|
+
let n = 0;
|
|
180
|
+
for (const [type, f] of this.failedTypes) if (f.version === type.version) n++;
|
|
181
|
+
return n;
|
|
182
|
+
}
|
|
183
|
+
/** Types drawing through the standard fallback now, with why (for RenderHealth). */
|
|
184
|
+
failures() {
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const [type, f] of this.failedTypes) {
|
|
187
|
+
if (f.version === type.version) out.push({ type: type.name, error: f.error });
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* A standard material standing in for one whose type failed: its standard fields when the type
|
|
193
|
+
* extends standard, else its `baseColor` or `color` if it has one. Kept per material and
|
|
194
|
+
* refreshed when the material changes.
|
|
195
|
+
*/
|
|
196
|
+
proxy(material) {
|
|
197
|
+
const hit = this.proxies.get(material);
|
|
198
|
+
if (hit && hit.version === material.version) return hit.proxy;
|
|
199
|
+
const value = material.value;
|
|
200
|
+
const init = {};
|
|
201
|
+
if (material.type.standard) {
|
|
202
|
+
for (const k of Object.keys(STANDARD_FIELDS)) if (k in value) init[k] = value[k];
|
|
203
|
+
} else {
|
|
204
|
+
const color = value.baseColor ?? value.color;
|
|
205
|
+
if (isColor(color)) init.baseColor = color;
|
|
206
|
+
}
|
|
207
|
+
const proxy = hit?.proxy ?? new MaterialAsset(init);
|
|
208
|
+
if (hit) proxy.set(init);
|
|
209
|
+
this.proxies.set(material, { version: material.version, proxy });
|
|
210
|
+
return proxy;
|
|
211
|
+
}
|
|
133
212
|
/** A compiled pipeline under `key` (unique per pass, type, and variant), if there is one. */
|
|
134
213
|
cached(key) {
|
|
135
214
|
return this.pipelines.get(key);
|
|
136
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Per (pass, sample count) slot of CPU-culled lists: the batch structure version and device
|
|
218
|
+
* generation whose pipelines were all asked for and compiled (forward's warmPipelines).
|
|
219
|
+
*/
|
|
220
|
+
warm = /* @__PURE__ */ new Map();
|
|
221
|
+
warmed(slot, structure, generation) {
|
|
222
|
+
return this.warm.get(slot) === structure * 1024 + generation % 1024;
|
|
223
|
+
}
|
|
224
|
+
markWarmed(slot, structure, generation) {
|
|
225
|
+
this.warm.set(slot, structure * 1024 + generation % 1024);
|
|
226
|
+
}
|
|
137
227
|
/** The last pipeline that compiled under each key, with the layout it was made for. */
|
|
138
228
|
previous = /* @__PURE__ */ new Map();
|
|
139
229
|
/**
|
|
@@ -141,8 +231,12 @@ class MaterialPipelines {
|
|
|
141
231
|
* While a replacement compiles (an edited shader or noise graph), draws keep the previous one
|
|
142
232
|
* for the key if its layout is the same object (the bind groups still fit), instead of vanishing.
|
|
143
233
|
*/
|
|
144
|
-
create(gpu, key, descriptor) {
|
|
234
|
+
create(gpu, key, descriptor, type) {
|
|
145
235
|
const pipeline = gpu.pipelines.render(descriptor);
|
|
236
|
+
if (!pipeline && type && type.name !== "render/StandardMaterial") {
|
|
237
|
+
const error = gpu.pipelines.failure(descriptor);
|
|
238
|
+
if (error) this.failedTypes.set(type, { version: type.version, error });
|
|
239
|
+
}
|
|
146
240
|
if (pipeline) {
|
|
147
241
|
this.pipelines.set(key, pipeline);
|
|
148
242
|
this.previous.set(key, { pipeline, layout: descriptor.layout, generation: gpu.generation });
|
|
@@ -155,6 +249,9 @@ class MaterialPipelines {
|
|
|
155
249
|
return void 0;
|
|
156
250
|
}
|
|
157
251
|
}
|
|
252
|
+
function isColor(value) {
|
|
253
|
+
return (Array.isArray(value) || value instanceof Float32Array) && (value.length === 3 || value.length === 4) && Array.prototype.every.call(value, (c) => typeof c === "number");
|
|
254
|
+
}
|
|
158
255
|
export {
|
|
159
256
|
MaterialPipelines,
|
|
160
257
|
blendState,
|
package/dist/materials.d.ts
CHANGED
|
@@ -28,12 +28,27 @@ export interface MaterialTypeOptions<F extends Fields> {
|
|
|
28
28
|
* layer). Arrays of color textures sample through the sRGB view.
|
|
29
29
|
*/
|
|
30
30
|
arrays?: readonly string[];
|
|
31
|
+
/**
|
|
32
|
+
* Texture fields that hold color (albedo, an atlas): sampled through the sRGB view, as
|
|
33
|
+
* `baseColorTexture` is. Other texture fields sample the stored values (the linear view).
|
|
34
|
+
*/
|
|
35
|
+
colors?: readonly string[];
|
|
31
36
|
/**
|
|
32
37
|
* Standard extensions only: false leaves the standard material's five texture slots (and their
|
|
33
38
|
* samplers) out of the layout, for types that build their own surface and need the room (a stage
|
|
34
39
|
* holds 16 sampled textures). Its shader must then not call `standard_input`.
|
|
35
40
|
*/
|
|
36
41
|
standardTextures?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* false: draws of this type aren't pick targets. A pick (GPU or raycast) goes through them to
|
|
44
|
+
* what's under: a grid's lines over the tiles they're drawn on (0059). Default true.
|
|
45
|
+
*/
|
|
46
|
+
pickable?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Where `shader` comes from when a plugin registers it, e.g. `surfacePlugin from
|
|
49
|
+
* '@aethervtt/shard-render/surface'`: a draw without it logs `render/feature-missing` naming it.
|
|
50
|
+
*/
|
|
51
|
+
plugin?: string;
|
|
37
52
|
description?: string;
|
|
38
53
|
}
|
|
39
54
|
/** A material type's noise slot: `noise_<name>` calls the graph at `path`. */
|
|
@@ -60,6 +75,12 @@ export declare class MaterialType {
|
|
|
60
75
|
textures: string[];
|
|
61
76
|
/** Texture fields bound as 2D arrays. */
|
|
62
77
|
arrays: Set<string>;
|
|
78
|
+
/** Texture fields sampled through the sRGB view. */
|
|
79
|
+
colors: Set<string>;
|
|
80
|
+
/** Whether picks can hit draws of this type (MaterialTypeOptions.pickable). */
|
|
81
|
+
pickable: boolean;
|
|
82
|
+
/** The plugin that registers its shader (MaterialTypeOptions.plugin). */
|
|
83
|
+
plugin: string | undefined;
|
|
63
84
|
blend: BlendMode | undefined;
|
|
64
85
|
shader: string | undefined;
|
|
65
86
|
/** Noise graphs the module wraps as `noise_<name>`. */
|
|
@@ -67,6 +88,8 @@ export declare class MaterialType {
|
|
|
67
88
|
description: string;
|
|
68
89
|
/** Increments on every redefinition, so GPU state and pipelines rebuild. */
|
|
69
90
|
version: number;
|
|
91
|
+
/** Its definition as a string: defining the name again with an equal one is a no-op (0052). */
|
|
92
|
+
signature: string;
|
|
70
93
|
readonly modulePath: string;
|
|
71
94
|
/** WGSL identifier of the uniform (the PascalName). */
|
|
72
95
|
readonly varName: string;
|