@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.
Files changed (134) hide show
  1. package/dist/assets.d.ts +3 -0
  2. package/dist/assets.js +43 -2
  3. package/dist/atmosphere-nodes.d.ts +9 -0
  4. package/dist/atmosphere-nodes.js +57 -18
  5. package/dist/atmosphere-plugin.js +8 -2
  6. package/dist/baseline/atmosphere.d.ts +16 -0
  7. package/dist/baseline/atmosphere.js +301 -0
  8. package/dist/baseline/data-texture.d.ts +36 -0
  9. package/dist/baseline/data-texture.js +181 -0
  10. package/dist/baseline/environment.d.ts +14 -0
  11. package/dist/baseline/environment.js +368 -0
  12. package/dist/baseline/exposure.d.ts +25 -0
  13. package/dist/baseline/exposure.js +120 -0
  14. package/dist/baseline/lights.d.ts +33 -0
  15. package/dist/baseline/lights.js +133 -0
  16. package/dist/baseline/textures.d.ts +97 -0
  17. package/dist/baseline/textures.js +203 -0
  18. package/dist/camera.d.ts +2 -0
  19. package/dist/camera.js +4 -0
  20. package/dist/culling-shaders.js +21 -2
  21. package/dist/culling.d.ts +11 -2
  22. package/dist/culling.js +44 -18
  23. package/dist/cutaway-plugin.d.ts +48 -0
  24. package/dist/cutaway-plugin.js +364 -0
  25. package/dist/cutaway.d.ts +27 -0
  26. package/dist/cutaway.js +49 -0
  27. package/dist/data-store.d.ts +69 -0
  28. package/dist/data-store.js +105 -0
  29. package/dist/debug-shaders.js +17 -3
  30. package/dist/debug-views.js +4 -2
  31. package/dist/deferred-shaders.js +2 -0
  32. package/dist/deferred.js +24 -3
  33. package/dist/deform.d.ts +5 -4
  34. package/dist/deform.js +4 -9
  35. package/dist/describe-plugin.d.ts +4 -2
  36. package/dist/describe-plugin.js +75 -3
  37. package/dist/display-nodes.js +15 -4
  38. package/dist/dynamic-resolution.d.ts +2 -0
  39. package/dist/dynamic-resolution.js +6 -1
  40. package/dist/environment-plugin.js +17 -2
  41. package/dist/environment-state.d.ts +7 -3
  42. package/dist/environment-state.js +20 -9
  43. package/dist/environment.js +7 -1
  44. package/dist/features.d.ts +49 -0
  45. package/dist/features.js +106 -0
  46. package/dist/forward.d.ts +38 -2
  47. package/dist/forward.js +582 -184
  48. package/dist/fxaa.js +12 -2
  49. package/dist/gizmos-plugin.js +7 -0
  50. package/dist/gizmos.d.ts +3 -3
  51. package/dist/gizmos.js +11 -13
  52. package/dist/gpu-assets.d.ts +58 -4
  53. package/dist/gpu-assets.js +191 -8
  54. package/dist/graph.d.ts +11 -0
  55. package/dist/graph.js +30 -5
  56. package/dist/health.d.ts +58 -0
  57. package/dist/health.js +112 -0
  58. package/dist/index.d.ts +30 -11
  59. package/dist/index.js +286 -9
  60. package/dist/instances.d.ts +119 -8
  61. package/dist/instances.js +387 -78
  62. package/dist/interior-plugin.d.ts +121 -0
  63. package/dist/interior-plugin.js +465 -0
  64. package/dist/interior.d.ts +24 -0
  65. package/dist/interior.js +13 -0
  66. package/dist/layers.d.ts +35 -0
  67. package/dist/layers.js +49 -0
  68. package/dist/lens-plugin.d.ts +43 -0
  69. package/dist/lens-plugin.js +304 -0
  70. package/dist/lens.d.ts +41 -0
  71. package/dist/lens.js +99 -0
  72. package/dist/lights.d.ts +50 -5
  73. package/dist/lights.js +101 -22
  74. package/dist/material-pipelines.d.ts +37 -4
  75. package/dist/material-pipelines.js +102 -5
  76. package/dist/materials.d.ts +23 -0
  77. package/dist/materials.js +44 -1
  78. package/dist/outline-plugin.d.ts +48 -0
  79. package/dist/outline-plugin.js +529 -0
  80. package/dist/outline.d.ts +15 -0
  81. package/dist/outline.js +42 -0
  82. package/dist/overlays.js +1 -0
  83. package/dist/picking.d.ts +17 -0
  84. package/dist/picking.js +40 -11
  85. package/dist/pixel-perfect-plugin.js +10 -2
  86. package/dist/plugin.d.ts +58 -6
  87. package/dist/plugin.js +191 -24
  88. package/dist/post-common.d.ts +2 -0
  89. package/dist/post-common.js +3 -0
  90. package/dist/post-nodes.d.ts +2 -1
  91. package/dist/post-nodes.js +111 -19
  92. package/dist/post-plugin.js +5 -3
  93. package/dist/post-shaders.js +51 -6
  94. package/dist/projection.d.ts +24 -0
  95. package/dist/projection.js +87 -0
  96. package/dist/readback.d.ts +1 -0
  97. package/dist/readback.js +1 -0
  98. package/dist/render-scale.js +1 -0
  99. package/dist/screen-effects.d.ts +97 -0
  100. package/dist/screen-effects.js +192 -0
  101. package/dist/shaders.d.ts +13 -0
  102. package/dist/shaders.js +131 -29
  103. package/dist/shadow-catcher.d.ts +9 -0
  104. package/dist/shadow-catcher.js +133 -0
  105. package/dist/shadows.d.ts +25 -1
  106. package/dist/shadows.js +97 -19
  107. package/dist/standard-fields.d.ts +1 -0
  108. package/dist/standard-fields.js +5 -0
  109. package/dist/standard.js +12 -0
  110. package/dist/stats.d.ts +45 -1
  111. package/dist/stats.js +91 -3
  112. package/dist/surface-model.d.ts +57 -0
  113. package/dist/surface-model.js +289 -0
  114. package/dist/surface-shaders.d.ts +3 -0
  115. package/dist/surface-shaders.js +160 -0
  116. package/dist/surface.d.ts +40 -0
  117. package/dist/surface.js +86 -0
  118. package/dist/target.d.ts +1 -13
  119. package/dist/target.js +1 -26
  120. package/dist/testing.d.ts +10 -0
  121. package/dist/testing.js +33 -1
  122. package/dist/tier.d.ts +13 -0
  123. package/dist/tier.js +14 -0
  124. package/dist/timer.d.ts +2 -0
  125. package/dist/timer.js +7 -1
  126. package/dist/variants.d.ts +37 -0
  127. package/dist/variants.js +104 -0
  128. package/dist/view-visibility-plugin.d.ts +61 -0
  129. package/dist/view-visibility-plugin.js +267 -0
  130. package/dist/view-visibility.d.ts +28 -0
  131. package/dist/view-visibility.js +46 -0
  132. package/dist/view.d.ts +29 -1
  133. package/dist/view.js +57 -34
  134. package/package.json +15 -11
@@ -0,0 +1,133 @@
1
+ import { t } from "@aethervtt/shard-core";
2
+ import { definePlugin } from "@aethervtt/shard-runtime";
3
+ import { defineMaterial } from "./materials.js";
4
+ import { Shaders } from "./plugin.js";
5
+ import { registerShaders } from "./shaders.js";
6
+ const SHADOW_CATCHER_SHADERS = {
7
+ "shard::pbr::catcher": `
8
+ import shard::view::view;
9
+ import shard::pbr::types::VertexOutput;
10
+ import shard::pbr::lights::{ lights, clusters, cluster_bits, next_light, directional, light_falloff, LIGHT_SPOT, NO_SHADOW, CLUSTER_COUNT, MAX_PER_CLUSTER };
11
+ import shard::pbr::shadows::{ directional_shadow, spot_shadow, point_shadow };
12
+ import shard::pbr::lighting::cluster_of;
13
+
14
+ const FLAG_RECEIVER: u32 = 4u;
15
+
16
+ fn brightest(c: vec3f) -> f32 { return max(c.r, max(c.g, c.b)); }
17
+
18
+ /**
19
+ * How much of the shadow-casting light reaches the surface: the light of every shadow-casting
20
+ * light with shadows, over that light without them. 1 when none cast shadows, or when the mesh
21
+ * doesn't receive them. Shadow catchers (and the dice tray, 0054) turn 1 \u2212 this into alpha.
22
+ */
23
+ fn shadow_visibility(in: VertexOutput) -> f32 {
24
+ if ((in.flags & FLAG_RECEIVER) == 0u) { return 1.0; }
25
+ let n = normalize(in.world_normal);
26
+ let p = in.world_position;
27
+ let view_depth = -(view.view * vec4f(p, 1.0)).z;
28
+ var lit = 0.0;
29
+ var total = 0.0;
30
+ for (var i = 0u; i < directional.count; i++) {
31
+ let light = directional.lights[i];
32
+ if (light.shadowed == 0u) { continue; }
33
+ let e = max(dot(n, normalize(light.direction)), 0.0) * brightest(light.color);
34
+ total += e;
35
+ lit += e * directional_shadow(p, n, view_depth, in.clip.xy);
36
+ }
37
+ let cluster = cluster_of(in.clip, view_depth);
38
+ // Baseline (0064): the cluster's bits; keep the body in step with the loop below.
39
+ @if(BASELINE) if (cluster >= 0) {
40
+ var mask = cluster_bits[cluster];
41
+ var index = 0u;
42
+ while (next_light(&mask, &index)) {
43
+ let light = lights[index];
44
+ if (light.shadow == NO_SHADOW) { continue; }
45
+ let to_light = light.position - p;
46
+ let d2 = max(dot(to_light, to_light), 1e-4);
47
+ let d = sqrt(d2);
48
+ let l = to_light / d;
49
+ var attenuation = light_falloff(light, d, d2);
50
+ if (light.kind == LIGHT_SPOT) {
51
+ let spot = clamp(dot(-l, light.direction) * light.spot_scale + light.spot_offset, 0.0, 1.0);
52
+ attenuation *= spot * spot;
53
+ }
54
+ let e = max(dot(n, l), 0.0) * attenuation * brightest(light.color);
55
+ if (e <= 0.0) { continue; }
56
+ var shadow = 1.0;
57
+ if (light.kind == LIGHT_SPOT) {
58
+ shadow = spot_shadow(light.shadow, p, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
59
+ } else {
60
+ shadow = point_shadow(light.shadow, p, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
61
+ }
62
+ total += e;
63
+ lit += e * shadow;
64
+ }
65
+ }
66
+ @if(!BASELINE) if (cluster >= 0) {
67
+ let count = clusters[cluster];
68
+ let base = CLUSTER_COUNT + u32(cluster) * MAX_PER_CLUSTER;
69
+ for (var k = 0u; k < count; k++) {
70
+ let light = lights[clusters[base + k]];
71
+ if (light.shadow == NO_SHADOW) { continue; }
72
+ let to_light = light.position - p;
73
+ let d2 = max(dot(to_light, to_light), 1e-4);
74
+ let d = sqrt(d2);
75
+ let l = to_light / d;
76
+ var attenuation = light_falloff(light, d, d2);
77
+ if (light.kind == LIGHT_SPOT) {
78
+ let spot = clamp(dot(-l, light.direction) * light.spot_scale + light.spot_offset, 0.0, 1.0);
79
+ attenuation *= spot * spot;
80
+ }
81
+ let e = max(dot(n, l), 0.0) * attenuation * brightest(light.color);
82
+ if (e <= 0.0) { continue; }
83
+ var shadow = 1.0;
84
+ if (light.kind == LIGHT_SPOT) {
85
+ shadow = spot_shadow(light.shadow, p, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
86
+ } else {
87
+ shadow = point_shadow(light.shadow, p, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, in.clip.xy);
88
+ }
89
+ total += e;
90
+ lit += e * shadow;
91
+ }
92
+ }
93
+ return select(1.0, lit / total, total > 0.0);
94
+ }`,
95
+ "shard::material::shadow_catcher": `
96
+ import shard::pbr::types::VertexOutput;
97
+ import shard::pbr::catcher::shadow_visibility;
98
+ import material::shadow_catcher::ShadowCatcher;
99
+
100
+ /** No color; alpha opacity \xD7 (1 \u2212 visibility) over every shadow-casting light. */
101
+ override fn shade(in: VertexOutput) -> vec4f {
102
+ return vec4f(0.0, 0.0, 0.0, ShadowCatcher.opacity * (1.0 - shadow_visibility(in)));
103
+ }`
104
+ };
105
+ const ShadowCatcher = defineMaterial("render/ShadowCatcher", {
106
+ extends: "none",
107
+ blend: "premultiplied",
108
+ fields: {
109
+ opacity: t.f32({
110
+ default: 0.6,
111
+ min: 0,
112
+ max: 1,
113
+ description: "Alpha of full shadow. Partial shadow (penumbra, one of two lights) is less."
114
+ })
115
+ },
116
+ shader: "shard::material::shadow_catcher",
117
+ description: "Invisible except for the shadows it receives: under a camera clearing to alpha 0 they fall on the page (a dice tray floor). Give its mesh NotShadowCaster."
118
+ });
119
+ const shadowCatcherPlugin = definePlugin({
120
+ name: "render/shadow-catcher",
121
+ dependencies: ["render/forward"],
122
+ provides: [ShadowCatcher],
123
+ build() {
124
+ },
125
+ ready(app) {
126
+ registerShaders(app.world.resource(Shaders), SHADOW_CATCHER_SHADERS);
127
+ }
128
+ });
129
+ export {
130
+ SHADOW_CATCHER_SHADERS,
131
+ ShadowCatcher,
132
+ shadowCatcherPlugin
133
+ };
package/dist/shadows.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type World } from '@aethervtt/shard-core';
2
2
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
3
  import type { NodeContext } from './graph.js';
4
- import { type DrawList } from './instances.js';
4
+ import { type DrawList, type InstanceStore } from './instances.js';
5
5
  import { type LightRecord, type LightStore } from './lights.js';
6
6
  import { type MaterialPipelines } from './material-pipelines.js';
7
7
  import type { CameraData } from './view.js';
@@ -15,10 +15,28 @@ export declare function pointTanHalf(size: number): number;
15
15
  export interface ShadowViewDraw {
16
16
  viewProj: Float32Array;
17
17
  frustum: Float32Array;
18
+ /** What casters are culled against: the frustum, but open toward the light for cascades. */
19
+ cullPlanes: Float32Array;
18
20
  draws: DrawList;
19
21
  /** Dynamic offset of this view's matrix in the frame's shadow uniforms. */
20
22
  offset: number;
23
+ /** Whether its layer holds a complete draw (0055: cached shadows keep it). */
24
+ drawn: boolean;
25
+ /** The matrix and `InstanceStore.shadowEpoch` it was drawn with. */
26
+ drawnViewProj: Float32Array;
27
+ drawnEpoch: number;
28
+ /** Set by the queue: draw it this frame. */
29
+ redraw: boolean;
21
30
  }
31
+ /**
32
+ * Whether a shadow view must draw this frame (0055). Views of `shadowUpdate: 'always'` lights
33
+ * always do. A cached view draws when its layer isn't complete, its matrix changed (the light or
34
+ * the cascade's fit moved), what casts shadows changed (`shadowEpoch`), poses deform, or a
35
+ * caster inside its cull planes moved.
36
+ */
37
+ export declare function shadowRedraw(view: ShadowViewDraw, cached: boolean, store: InstanceStore): boolean;
38
+ /** Records that a shadow view's layer now holds a complete draw of this frame's state. */
39
+ export declare function markShadowDrawn(view: ShadowViewDraw, store: InstanceStore): void;
22
40
  /** A camera's cascades: fitted each frame, one depth layer each. */
23
41
  export declare class Cascades {
24
42
  count: number;
@@ -98,11 +116,17 @@ export declare class ShadowPassUniforms {
98
116
  bindGroup: GPUBindGroup | undefined;
99
117
  private bound;
100
118
  private readonly gpu;
119
+ /** What the buffer holds, so unchanged shadow views write nothing (0055). */
120
+ private sent;
121
+ private sentCount;
122
+ private sentVersion;
123
+ private sentGeneration;
101
124
  constructor(gpu: GpuContext);
102
125
  reset(): void;
103
126
  /** Adds a view matrix; returns its dynamic offset. */
104
127
  push(viewProj: Float32Array): number;
105
128
  upload(layout: GPUBindGroupLayout, globals: GpuBuffer): void;
129
+ private unchanged;
106
130
  }
107
131
  export declare function shadowViewLayout(gpu: GpuContext): GPUBindGroupLayout;
108
132
  /**
package/dist/shadows.js CHANGED
@@ -2,7 +2,11 @@ import { defineResource, frustum, mat4 } from "@aethervtt/shard-core";
2
2
  import { GpuBuffer } from "@aethervtt/shard-gpu";
3
3
  import { Culler } from "./culling.js";
4
4
  import { GpuAssetsResource } from "./gpu-assets.js";
5
- import { createDrawList, InstanceFlags, Instances } from "./instances.js";
5
+ import {
6
+ createDrawList,
7
+ InstanceFlags,
8
+ Instances
9
+ } from "./instances.js";
6
10
  import { setShadowIndex } from "./lights.js";
7
11
  import {
8
12
  materialVariant,
@@ -10,6 +14,7 @@ import {
10
14
  variantBlend,
11
15
  variantCull
12
16
  } from "./material-pipelines.js";
17
+ import { bindingDimension } from "./tier.js";
13
18
  const MAX_CASCADES = 4;
14
19
  const MAX_SHADOWED_SPOTS = 8;
15
20
  const MAX_SHADOWED_POINTS = 4;
@@ -31,7 +36,30 @@ function pointTanHalf(size) {
31
36
  }
32
37
  const VIEW_STRIDE = 256;
33
38
  function shadowView() {
34
- return { viewProj: mat4.create(), frustum: frustum.create(), draws: createDrawList(), offset: 0 };
39
+ return {
40
+ viewProj: mat4.create(),
41
+ frustum: frustum.create(),
42
+ cullPlanes: frustum.create(),
43
+ draws: createDrawList(),
44
+ offset: 0,
45
+ drawn: false,
46
+ drawnViewProj: mat4.create(),
47
+ drawnEpoch: -1,
48
+ redraw: true
49
+ };
50
+ }
51
+ function shadowRedraw(view, cached, store) {
52
+ if (!cached || !view.drawn || view.drawnEpoch !== store.shadowEpoch || store.deforming)
53
+ return true;
54
+ const a = view.viewProj;
55
+ const b = view.drawnViewProj;
56
+ for (let i = 0; i < 16; i++) if (a[i] !== b[i]) return true;
57
+ return store.movedCasterIn(view.cullPlanes);
58
+ }
59
+ function markShadowDrawn(view, store) {
60
+ view.drawn = true;
61
+ view.drawnViewProj.set(view.viewProj);
62
+ view.drawnEpoch = store.shadowEpoch;
35
63
  }
36
64
  class Cascades {
37
65
  count = 0;
@@ -46,11 +74,13 @@ class Cascades {
46
74
  ensureTexture(gpu, size, label) {
47
75
  if (!this.texture || this.size !== size || this.generation !== gpu.generation) {
48
76
  this.texture?.destroy();
77
+ for (const view of this.views) view.drawn = false;
49
78
  this.texture = gpu.device.createTexture({
50
79
  label: `${label}/cascades`,
51
80
  size: [size, size, MAX_CASCADES],
52
81
  format: "depth32float",
53
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
82
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
83
+ ...bindingDimension(gpu, "2d-array")
54
84
  });
55
85
  this.size = size;
56
86
  this.generation = gpu.generation;
@@ -160,6 +190,7 @@ function fitCascades(cascades, cam, sun, size, cull) {
160
190
  set(4, -basis[6], -basis[7], -basis[8], lz + radius);
161
191
  set(5, 0, 0, 0, 1);
162
192
  const view = cascades.views[i];
193
+ view.cullPlanes.set(p);
163
194
  const any = cull(view, p, scratchBox);
164
195
  let zNear = lz - radius;
165
196
  if (any) {
@@ -228,15 +259,18 @@ class LocalShadows {
228
259
  label,
229
260
  size: [size, size, layers],
230
261
  format: "depth32float",
231
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
262
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
263
+ ...bindingDimension(gpu, "2d-array")
232
264
  });
233
265
  if (fresh || !this.spotTexture || spotLayers > this.spotLayers) {
234
266
  this.spotTexture?.destroy();
267
+ for (const view of this.spotViews) view.drawn = false;
235
268
  this.spotLayers = Math.max(spotLayers, fresh ? 1 : this.spotLayers);
236
269
  this.spotTexture = make("shadows/spots", this.spotLayers);
237
270
  }
238
271
  if (fresh || !this.pointTexture || pointLayers > this.pointLayers) {
239
272
  this.pointTexture?.destroy();
273
+ for (const view of this.pointViews) view.drawn = false;
240
274
  this.pointLayers = Math.max(pointLayers, fresh ? 6 : this.pointLayers);
241
275
  this.pointTexture = make("shadows/points", this.pointLayers);
242
276
  }
@@ -317,6 +351,7 @@ function fitLocalShadows(shadows, size) {
317
351
  const view = shadows.spotViews[i];
318
352
  mat4.multiply(view.viewProj, scratchProj, scratchView);
319
353
  frustum.fromViewProjection(view.frustum, view.viewProj);
354
+ view.cullPlanes.set(view.frustum);
320
355
  shadows.spotParams[i * 4] = tanHalf2;
321
356
  shadows.spotParams[i * 4 + 1] = near;
322
357
  }
@@ -335,6 +370,7 @@ function fitLocalShadows(shadows, size) {
335
370
  const view = shadows.pointViews[i * 6 + f];
336
371
  mat4.multiply(view.viewProj, scratchProj, scratchView);
337
372
  frustum.fromViewProjection(view.frustum, view.viewProj);
373
+ view.cullPlanes.set(view.frustum);
338
374
  }
339
375
  }
340
376
  }
@@ -377,6 +413,11 @@ class ShadowPassUniforms {
377
413
  bindGroup;
378
414
  bound = "";
379
415
  gpu;
416
+ /** What the buffer holds, so unchanged shadow views write nothing (0055). */
417
+ sent = new Float32Array(0);
418
+ sentCount = 0;
419
+ sentVersion = -1;
420
+ sentGeneration = -1;
380
421
  constructor(gpu) {
381
422
  this.gpu = gpu;
382
423
  this.buffer = new GpuBuffer(gpu, {
@@ -400,7 +441,15 @@ class ShadowPassUniforms {
400
441
  return this.count++ * VIEW_STRIDE;
401
442
  }
402
443
  upload(layout, globals) {
403
- if (this.count > 0) this.buffer.write(this.data, 0, 0, this.count * VIEW_STRIDE / 4);
444
+ const floats = this.count * VIEW_STRIDE / 4;
445
+ if (this.count > 0 && !this.unchanged(floats)) {
446
+ this.buffer.write(this.data, 0, 0, floats);
447
+ if (this.sent.length < this.data.length) this.sent = new Float32Array(this.data.length);
448
+ this.sent.set(this.data.subarray(0, floats));
449
+ this.sentCount = this.count;
450
+ this.sentVersion = this.buffer.version;
451
+ this.sentGeneration = this.gpu.generation;
452
+ }
404
453
  const key = `${this.buffer.version}/${globals.version}/${this.gpu.generation}`;
405
454
  if (!this.bindGroup || this.bound !== key) {
406
455
  this.bindGroup = this.gpu.device.createBindGroup({
@@ -414,6 +463,15 @@ class ShadowPassUniforms {
414
463
  this.bound = key;
415
464
  }
416
465
  }
466
+ unchanged(floats) {
467
+ if (this.sentCount < this.count) return false;
468
+ if (this.sentVersion !== this.buffer.version || this.sentGeneration !== this.gpu.generation)
469
+ return false;
470
+ const data = this.data;
471
+ const sent = this.sent;
472
+ for (let i = 0; i < floats; i++) if (data[i] !== sent[i]) return false;
473
+ return true;
474
+ }
417
475
  }
418
476
  function shadowViewLayout(gpu) {
419
477
  return gpu.layouts.bindGroupLayout({
@@ -472,7 +530,8 @@ function drawShadowCasters(ctx, target, view, offset, uniforms, pipelines, shado
472
530
  const draws = view.draws;
473
531
  for (let d = 0; d < draws.length; d++) {
474
532
  const item = draws.items[d];
475
- const material = item.batch.material;
533
+ const fallback = pipelines.failing(item.batch.material.type);
534
+ const material = fallback ? pipelines.proxy(item.batch.material) : item.batch.material;
476
535
  const type = material.type;
477
536
  const variant = materialVariant(material);
478
537
  const masked = type.standard && variantBlend(variant) === "mask";
@@ -481,21 +540,38 @@ function drawShadowCasters(ctx, target, view, offset, uniforms, pipelines, shado
481
540
  if (!pipeline) {
482
541
  const module = pipelines.module(world, gpu, type, 8, "shard::pbr::shadow", void 0);
483
542
  if (!module) {
484
- gpu.pipelines.skipped++;
543
+ if (!fallback && pipelines.failing(type)) d--;
544
+ else gpu.pipelines.skipped++;
545
+ continue;
546
+ }
547
+ pipeline = pipelines.create(
548
+ gpu,
549
+ key,
550
+ {
551
+ label: `shadow/${type.name}/${masked ? "mask" : "opaque"}/${variantCull(variant)}`,
552
+ layout: gpu.layouts.pipelineLayout({
553
+ label: `shadow/${type.name}`,
554
+ bindGroupLayouts: [shadowView2, assets.layoutOf(type), store.layout]
555
+ }),
556
+ vertex: { module, entryPoint: "vs", buffers: SHADOW_BUFFERS },
557
+ fragment: masked ? { module, entryPoint: "fs_mask", targets: [] } : void 0,
558
+ primitive: {
559
+ topology: "triangle-list",
560
+ cullMode: variantCull(variant),
561
+ frontFace: "ccw"
562
+ },
563
+ depthStencil: {
564
+ format: "depth32float",
565
+ depthWriteEnabled: true,
566
+ depthCompare: "greater"
567
+ }
568
+ },
569
+ type
570
+ );
571
+ if (!pipeline) {
572
+ if (!fallback && pipelines.failing(type)) d--;
485
573
  continue;
486
574
  }
487
- pipeline = pipelines.create(gpu, key, {
488
- label: `shadow/${type.name}/${masked ? "mask" : "opaque"}/${variantCull(variant)}`,
489
- layout: gpu.layouts.pipelineLayout({
490
- label: `shadow/${type.name}`,
491
- bindGroupLayouts: [shadowView2, assets.layoutOf(type), store.layout]
492
- }),
493
- vertex: { module, entryPoint: "vs", buffers: SHADOW_BUFFERS },
494
- fragment: masked ? { module, entryPoint: "fs_mask", targets: [] } : void 0,
495
- primitive: { topology: "triangle-list", cullMode: variantCull(variant), frontFace: "ccw" },
496
- depthStencil: { format: "depth32float", depthWriteEnabled: true, depthCompare: "greater" }
497
- });
498
- if (!pipeline) continue;
499
575
  }
500
576
  const mat = assets.material(world, material);
501
577
  if (!mat?.bindGroup) continue;
@@ -555,8 +631,10 @@ export {
555
631
  drawShadowCasters,
556
632
  fitCascades,
557
633
  fitLocalShadows,
634
+ markShadowDrawn,
558
635
  packShadowData,
559
636
  pointTanHalf,
637
+ shadowRedraw,
560
638
  shadowViewLayout,
561
639
  sliceSphere
562
640
  };
@@ -15,6 +15,7 @@ export declare const STANDARD_FIELDS: {
15
15
  emissive: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
16
16
  emissiveLuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
17
17
  doubleSided: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
18
+ deferUntilReady: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
18
19
  alphaMode: import("@aethervtt/shard-core").FieldType<"additive" | "alpha" | "mask" | "opaque" | "premultiplied", "u8">;
19
20
  alphaCutoff: import("@aethervtt/shard-core").FieldType<number, "f32">;
20
21
  normalScale: import("@aethervtt/shard-core").FieldType<number, "f32">;
@@ -42,6 +42,11 @@ const STANDARD_FIELDS = {
42
42
  description: "Emitted luminance. 0 = not emissive."
43
43
  }),
44
44
  doubleSided: t.bool({ description: "Draw back faces too (no culling)." }),
45
+ deferUntilReady: t.bool({
46
+ // The asset layer reads it; shaders don't.
47
+ gpu: false,
48
+ description: "Baseline tier (0064): while a texture it binds waits for a copy in its slot's color space, skip its draws instead of showing the slot's loading fallback."
49
+ }),
45
50
  alphaMode: t.enum(["opaque", "mask", "alpha", "additive", "premultiplied"], {
46
51
  description: "opaque ignores alpha; mask discards pixels below alphaCutoff; alpha blends (transparent, drawn after opaque, sorted back to front); additive adds light (glows); premultiplied expects color already multiplied by alpha."
47
52
  }),
package/dist/standard.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { definePlugin } from "@aethervtt/shard-runtime";
2
2
  import { atmospherePlugin } from "./atmosphere-plugin.js";
3
+ import { cutawayPlugin } from "./cutaway-plugin.js";
3
4
  import { deferredPlugin } from "./deferred-plugin.js";
4
5
  import { renderDescribePlugin } from "./describe-plugin.js";
5
6
  import { dynamicResolutionPlugin } from "./dynamic-resolution.js";
@@ -7,10 +8,15 @@ import { environmentPlugin } from "./environment-plugin.js";
7
8
  import { forwardCorePlugin } from "./forward.js";
8
9
  import { fxaaPlugin } from "./fxaa.js";
9
10
  import { gizmosPlugin } from "./gizmos-plugin.js";
11
+ import { interiorPlugin } from "./interior-plugin.js";
12
+ import { lensPlugin } from "./lens-plugin.js";
13
+ import { outlinePlugin } from "./outline-plugin.js";
10
14
  import { pickingPlugin } from "./picking-plugin.js";
11
15
  import { pixelPerfectPlugin } from "./pixel-perfect-plugin.js";
12
16
  import { postPlugin } from "./post-plugin.js";
17
+ import { shadowCatcherPlugin } from "./shadow-catcher.js";
13
18
  import { skinningPlugin } from "./skinning-plugin.js";
19
+ import { viewVisibilityPlugin } from "./view-visibility-plugin.js";
14
20
  function forwardPlugin(options = {}) {
15
21
  return definePlugin({
16
22
  name: "render/standard",
@@ -26,6 +32,12 @@ function forwardPlugin(options = {}) {
26
32
  deferredPlugin,
27
33
  skinningPlugin,
28
34
  pixelPerfectPlugin,
35
+ lensPlugin,
36
+ outlinePlugin,
37
+ cutawayPlugin,
38
+ viewVisibilityPlugin,
39
+ interiorPlugin,
40
+ shadowCatcherPlugin,
29
41
  dynamicResolutionPlugin,
30
42
  renderDescribePlugin
31
43
  );
package/dist/stats.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type GpuUploads, type UploadCategory } from '@aethervtt/shard-gpu';
1
2
  export interface ViewStats {
2
3
  visible: number;
3
4
  culled: number;
@@ -8,7 +9,50 @@ export interface ViewStats {
8
9
  /** Pipeline changes in the opaque pass (sorting keeps these near the number of material types). */
9
10
  pipelineSwitches?: number;
10
11
  }
11
- export declare const RenderStats: import("@aethervtt/shard-core").ResourceDef<Map<string, ViewStats>>;
12
+ /** What one frame (or a window of frames) wrote to the GPU and rebuilt (0055). */
13
+ export interface FrameRecord {
14
+ /** Bytes written, by what they were for. */
15
+ bytes: Record<UploadCategory, number>;
16
+ /** Scene data: every byte but `view` (camera and per-frame uniforms). */
17
+ sceneBytes: number;
18
+ /** Buffers and textures created. */
19
+ created: number;
20
+ /** Structure chunks rebuilt (0055). */
21
+ chunksRebuilt: number;
22
+ /** Meshes uploaded to the GPU: new ones, and ones whose data changed. */
23
+ meshesRebuilt: number;
24
+ /** Shadow maps (cascades and local shadow views) drawn. */
25
+ shadowMapsRendered: number;
26
+ }
27
+ /** Counts a system adds to during a frame; they land in `lastFrame` when the frame is submitted. */
28
+ export interface FrameCounts {
29
+ chunksRebuilt: number;
30
+ meshesRebuilt: number;
31
+ shadowMapsRendered: number;
32
+ }
33
+ /** Frames `RenderStatsMap.recent` sums over. */
34
+ export declare const RECENT_FRAMES = 60;
35
+ /**
36
+ * Per-view counts from the last frame (a Map by view name), plus what the last frame wrote to the
37
+ * GPU and rebuilt (`lastFrame`) and the sum over the last 60 rendered frames (`recent`).
38
+ */
39
+ export declare class RenderStatsMap extends Map<string, ViewStats> {
40
+ readonly lastFrame: FrameRecord;
41
+ readonly recent: FrameRecord;
42
+ /** Counted during the frame being built. */
43
+ readonly current: FrameCounts;
44
+ /** Frames recorded. */
45
+ frames: number;
46
+ private readonly ring;
47
+ private readonly row;
48
+ private readonly seen;
49
+ /**
50
+ * Closes a frame: `uploads` is the owner's running total (`gpu.uploads(owner)`); the difference
51
+ * since the last call is this frame's. Allocates nothing.
52
+ */
53
+ endFrame(uploads: GpuUploads | undefined): void;
54
+ }
55
+ export declare const RenderStats: import("@aethervtt/shard-core").ResourceDef<RenderStatsMap>;
12
56
  export interface GpuMemoryData {
13
57
  /** Textures uploaded by the forward renderer. */
14
58
  textures: number;
package/dist/stats.js CHANGED
@@ -1,7 +1,93 @@
1
1
  import { defineResource } from "@aethervtt/shard-core";
2
+ import {
3
+ UPLOAD_CATEGORIES,
4
+ Upload
5
+ } from "@aethervtt/shard-gpu";
6
+ const RECENT_FRAMES = 60;
7
+ const FIELDS = UPLOAD_CATEGORIES.length + 4;
8
+ function frameRecord() {
9
+ const bytes = {};
10
+ for (const c of UPLOAD_CATEGORIES) bytes[c] = 0;
11
+ return {
12
+ bytes,
13
+ sceneBytes: 0,
14
+ created: 0,
15
+ chunksRebuilt: 0,
16
+ meshesRebuilt: 0,
17
+ shadowMapsRendered: 0
18
+ };
19
+ }
20
+ class RenderStatsMap extends Map {
21
+ lastFrame = frameRecord();
22
+ recent = frameRecord();
23
+ /** Counted during the frame being built. */
24
+ current = { chunksRebuilt: 0, meshesRebuilt: 0, shadowMapsRendered: 0 };
25
+ /** Frames recorded. */
26
+ frames = 0;
27
+ ring = new Float64Array(RECENT_FRAMES * FIELDS);
28
+ row = new Float64Array(FIELDS);
29
+ seen = new Float64Array(UPLOAD_CATEGORIES.length + 1);
30
+ /**
31
+ * Closes a frame: `uploads` is the owner's running total (`gpu.uploads(owner)`); the difference
32
+ * since the last call is this frame's. Allocates nothing.
33
+ */
34
+ endFrame(uploads) {
35
+ const row = this.row;
36
+ const seen = this.seen;
37
+ const n = UPLOAD_CATEGORIES.length;
38
+ for (let i = 0; i < n; i++) {
39
+ const total = uploads ? uploads.bytes[i] : seen[i];
40
+ row[i] = total - seen[i];
41
+ seen[i] = total;
42
+ }
43
+ const created = uploads ? uploads.created : seen[n];
44
+ row[n] = created - seen[n];
45
+ seen[n] = created;
46
+ const cur = this.current;
47
+ row[n + 1] = cur.chunksRebuilt;
48
+ row[n + 2] = cur.meshesRebuilt;
49
+ row[n + 3] = cur.shadowMapsRendered;
50
+ cur.chunksRebuilt = cur.meshesRebuilt = cur.shadowMapsRendered = 0;
51
+ const ring = this.ring;
52
+ const slot = this.frames % RECENT_FRAMES * FIELDS;
53
+ this.frames++;
54
+ const last = this.lastFrame;
55
+ const recent = this.recent;
56
+ let lastScene = 0;
57
+ let recentScene = 0;
58
+ for (let i = 0; i < FIELDS; i++) {
59
+ const sum = recentField(recent, i) - ring[slot + i] + row[i];
60
+ ring[slot + i] = row[i];
61
+ setField(last, i, row[i]);
62
+ setField(recent, i, sum);
63
+ if (i < n && i !== Upload.view) {
64
+ lastScene += row[i];
65
+ recentScene += sum;
66
+ }
67
+ }
68
+ last.sceneBytes = lastScene;
69
+ recent.sceneBytes = recentScene;
70
+ }
71
+ }
72
+ function recentField(r, i) {
73
+ const n = UPLOAD_CATEGORIES.length;
74
+ if (i < n) return r.bytes[UPLOAD_CATEGORIES[i]];
75
+ if (i === n) return r.created;
76
+ if (i === n + 1) return r.chunksRebuilt;
77
+ if (i === n + 2) return r.meshesRebuilt;
78
+ return r.shadowMapsRendered;
79
+ }
80
+ function setField(r, i, v) {
81
+ const n = UPLOAD_CATEGORIES.length;
82
+ if (i < n) r.bytes[UPLOAD_CATEGORIES[i]] = v;
83
+ else if (i === n) r.created = v;
84
+ else if (i === n + 1) r.chunksRebuilt = v;
85
+ else if (i === n + 2) r.meshesRebuilt = v;
86
+ else r.shadowMapsRendered = v;
87
+ }
2
88
  const RenderStats = defineResource("render/Stats", {
3
- description: "Per-view counts from the last frame: visible, culled, hidden, pending, draw calls.",
4
- init: () => /* @__PURE__ */ new Map()
89
+ description: "Per-view counts from the last frame (visible, culled, hidden, pending, draw calls), and what the last frame and the last 60 wrote to the GPU and rebuilt.",
90
+ init: () => new RenderStatsMap()
5
91
  });
6
92
  const GpuMemory = defineResource("render/GpuMemory", {
7
93
  description: "GPU memory used by uploaded textures.",
@@ -13,6 +99,8 @@ const RenderCounters = defineResource("render/Counters", {
13
99
  });
14
100
  export {
15
101
  GpuMemory,
102
+ RECENT_FRAMES,
16
103
  RenderCounters,
17
- RenderStats
104
+ RenderStats,
105
+ RenderStatsMap
18
106
  };