@aethervtt/shard-render 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.js +43 -2
  5. package/dist/atmosphere-nodes.d.ts +9 -0
  6. package/dist/atmosphere-nodes.js +157 -30
  7. package/dist/atmosphere-plugin.js +8 -2
  8. package/dist/atmosphere-shaders.js +17 -5
  9. package/dist/baseline/atmosphere.d.ts +16 -0
  10. package/dist/baseline/atmosphere.js +301 -0
  11. package/dist/baseline/data-texture.d.ts +36 -0
  12. package/dist/baseline/data-texture.js +181 -0
  13. package/dist/baseline/environment.d.ts +14 -0
  14. package/dist/baseline/environment.js +368 -0
  15. package/dist/baseline/exposure.d.ts +25 -0
  16. package/dist/baseline/exposure.js +133 -0
  17. package/dist/baseline/lights.d.ts +33 -0
  18. package/dist/baseline/lights.js +133 -0
  19. package/dist/baseline/textures.d.ts +97 -0
  20. package/dist/baseline/textures.js +203 -0
  21. package/dist/camera.d.ts +2 -0
  22. package/dist/camera.js +4 -0
  23. package/dist/crowd.d.ts +21 -0
  24. package/dist/crowd.js +121 -0
  25. package/dist/culling-shaders.js +21 -2
  26. package/dist/culling.d.ts +11 -2
  27. package/dist/culling.js +44 -18
  28. package/dist/cutaway-plugin.d.ts +48 -0
  29. package/dist/cutaway-plugin.js +364 -0
  30. package/dist/cutaway.d.ts +27 -0
  31. package/dist/cutaway.js +49 -0
  32. package/dist/data-store.d.ts +69 -0
  33. package/dist/data-store.js +105 -0
  34. package/dist/debug-shaders.js +17 -3
  35. package/dist/debug-views.js +4 -2
  36. package/dist/decals-plugin.d.ts +5 -0
  37. package/dist/decals-plugin.js +54 -0
  38. package/dist/decals.d.ts +69 -0
  39. package/dist/decals.js +326 -0
  40. package/dist/deferred-shaders.js +2 -0
  41. package/dist/deferred.js +28 -4
  42. package/dist/deform.d.ts +5 -4
  43. package/dist/deform.js +4 -9
  44. package/dist/describe-plugin.d.ts +4 -2
  45. package/dist/describe-plugin.js +75 -3
  46. package/dist/display-nodes.js +37 -13
  47. package/dist/dynamic-resolution.d.ts +2 -0
  48. package/dist/dynamic-resolution.js +6 -1
  49. package/dist/environment-plugin.js +17 -2
  50. package/dist/environment-state.d.ts +7 -3
  51. package/dist/environment-state.js +20 -9
  52. package/dist/environment.js +7 -1
  53. package/dist/features.d.ts +49 -0
  54. package/dist/features.js +106 -0
  55. package/dist/foliage-budget.d.ts +64 -0
  56. package/dist/foliage-budget.js +108 -0
  57. package/dist/foliage-path.d.ts +19 -0
  58. package/dist/foliage-path.js +7 -0
  59. package/dist/foliage-shaders.d.ts +11 -0
  60. package/dist/foliage-shaders.js +504 -0
  61. package/dist/foliage.d.ts +194 -0
  62. package/dist/foliage.js +1027 -0
  63. package/dist/forward.d.ts +38 -2
  64. package/dist/forward.js +608 -186
  65. package/dist/fxaa.js +12 -2
  66. package/dist/gizmos-plugin.js +7 -0
  67. package/dist/gizmos.d.ts +3 -3
  68. package/dist/gizmos.js +11 -13
  69. package/dist/gpu-assets.d.ts +58 -4
  70. package/dist/gpu-assets.js +191 -8
  71. package/dist/graph.d.ts +45 -0
  72. package/dist/graph.js +169 -19
  73. package/dist/health.d.ts +58 -0
  74. package/dist/health.js +112 -0
  75. package/dist/index.d.ts +40 -11
  76. package/dist/index.js +343 -9
  77. package/dist/instances.d.ts +119 -8
  78. package/dist/instances.js +387 -78
  79. package/dist/interior-plugin.d.ts +121 -0
  80. package/dist/interior-plugin.js +465 -0
  81. package/dist/interior.d.ts +24 -0
  82. package/dist/interior.js +13 -0
  83. package/dist/layers.d.ts +35 -0
  84. package/dist/layers.js +49 -0
  85. package/dist/lens-plugin.d.ts +43 -0
  86. package/dist/lens-plugin.js +304 -0
  87. package/dist/lens.d.ts +41 -0
  88. package/dist/lens.js +99 -0
  89. package/dist/lights.d.ts +50 -5
  90. package/dist/lights.js +101 -22
  91. package/dist/material-pipelines.d.ts +37 -4
  92. package/dist/material-pipelines.js +102 -5
  93. package/dist/materials.d.ts +23 -0
  94. package/dist/materials.js +44 -1
  95. package/dist/outline-plugin.d.ts +48 -0
  96. package/dist/outline-plugin.js +529 -0
  97. package/dist/outline.d.ts +15 -0
  98. package/dist/outline.js +42 -0
  99. package/dist/overlay-registry.d.ts +3 -1
  100. package/dist/overlay-registry.js +3 -1
  101. package/dist/overlays.js +6 -2
  102. package/dist/perf-overlay.d.ts +9 -0
  103. package/dist/perf-overlay.js +125 -0
  104. package/dist/picking.d.ts +17 -0
  105. package/dist/picking.js +40 -11
  106. package/dist/pixel-perfect-plugin.js +10 -2
  107. package/dist/plugin.d.ts +69 -7
  108. package/dist/plugin.js +223 -25
  109. package/dist/post-common.d.ts +22 -2
  110. package/dist/post-common.js +82 -2
  111. package/dist/post-nodes.d.ts +7 -1
  112. package/dist/post-nodes.js +235 -111
  113. package/dist/post-plugin.js +5 -3
  114. package/dist/post-scene.d.ts +130 -0
  115. package/dist/post-scene.js +164 -0
  116. package/dist/post-shaders.js +85 -62
  117. package/dist/post.js +11 -1
  118. package/dist/projection.d.ts +24 -0
  119. package/dist/projection.js +87 -0
  120. package/dist/readback.d.ts +1 -0
  121. package/dist/readback.js +1 -0
  122. package/dist/render-scale.js +1 -0
  123. package/dist/ribbons-plugin.d.ts +5 -0
  124. package/dist/ribbons-plugin.js +59 -0
  125. package/dist/ribbons.d.ts +44 -0
  126. package/dist/ribbons.js +288 -0
  127. package/dist/scenario.d.ts +70 -0
  128. package/dist/scenario.js +125 -0
  129. package/dist/screen-effects.d.ts +97 -0
  130. package/dist/screen-effects.js +192 -0
  131. package/dist/shaders.d.ts +13 -0
  132. package/dist/shaders.js +189 -34
  133. package/dist/shadow-catcher.d.ts +9 -0
  134. package/dist/shadow-catcher.js +133 -0
  135. package/dist/shadows.d.ts +27 -2
  136. package/dist/shadows.js +100 -20
  137. package/dist/standard-fields.d.ts +1 -0
  138. package/dist/standard-fields.js +5 -0
  139. package/dist/standard.js +18 -0
  140. package/dist/stats.d.ts +45 -1
  141. package/dist/stats.js +91 -3
  142. package/dist/surface-model.d.ts +57 -0
  143. package/dist/surface-model.js +289 -0
  144. package/dist/surface-shaders.d.ts +3 -0
  145. package/dist/surface-shaders.js +160 -0
  146. package/dist/surface.d.ts +40 -0
  147. package/dist/surface.js +86 -0
  148. package/dist/target.d.ts +1 -13
  149. package/dist/target.js +1 -26
  150. package/dist/testing.d.ts +11 -0
  151. package/dist/testing.js +39 -1
  152. package/dist/tier.d.ts +13 -0
  153. package/dist/tier.js +14 -0
  154. package/dist/timer.d.ts +18 -1
  155. package/dist/timer.js +73 -20
  156. package/dist/variants.d.ts +37 -0
  157. package/dist/variants.js +104 -0
  158. package/dist/vfx-draw.d.ts +37 -0
  159. package/dist/vfx-draw.js +291 -0
  160. package/dist/vfx-state.d.ts +42 -0
  161. package/dist/vfx-state.js +81 -0
  162. package/dist/vfx.d.ts +69 -0
  163. package/dist/vfx.js +103 -0
  164. package/dist/view-visibility-plugin.d.ts +61 -0
  165. package/dist/view-visibility-plugin.js +267 -0
  166. package/dist/view-visibility.d.ts +28 -0
  167. package/dist/view-visibility.js +46 -0
  168. package/dist/view.d.ts +29 -1
  169. package/dist/view.js +57 -34
  170. package/package.json +23 -11
package/dist/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
- description: "Light emitted equally in all directions, in lumens (intensity lm / 4\u03C0 candela), with physical inverse-square falloff windowed to zero at range.",
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 GpuBuffer(gpu, {
220
+ this.directional = new DataStore(gpu, {
185
221
  label: "lights/directional",
186
- usage: GPUBufferUsage.STORAGE,
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 d = s < this.high && this.dirty[s] === 1;
259
- if (d) {
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.buffer.write(
265
- this.data,
266
- runStart * LIGHT_FLOATS * 4,
267
- runStart * LIGHT_FLOATS,
268
- (s - runStart) * LIGHT_FLOATS
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.directional.write(this.directionalData);
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, 0);
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
- setShadowIndex
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 type { MaterialAsset } from './assets.js';
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
- beginFrame(frame: number): void;
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
- beginFrame(frame) {
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) * 64 + slot;
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 module = library.module(gpu, {
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,
@@ -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;