@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,192 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ import { FrameDemand, LogResource, Time } from "@aethervtt/shard-runtime";
7
+ const MAX_SCREEN_EFFECTS = 8;
8
+ const MAX_SCREEN_EFFECT_PARAMS = 8;
9
+ const ScreenEffects = defineResource("render/ScreenEffects", {
10
+ hostWritable: true,
11
+ description: "Typed effects published at a screen point (at most 8): kind, center and radius in CSS pixels, ms to live, source entity, params. The app whose view they are in draws them through handlers registered with onScreenEffect.",
12
+ init: () => ({ effects: [] })
13
+ });
14
+ const ScreenEffectHandlers = defineResource("render/ScreenEffectHandlers", {
15
+ description: "Handlers by kind (onScreenEffect) and the screen effects they are drawing.",
16
+ init: () => ({ kinds: /* @__PURE__ */ new Map(), live: [], reported: /* @__PURE__ */ new Set() })
17
+ });
18
+ function invalid(message, path) {
19
+ return new ShardError("render/invalid-screen-effect", message, {
20
+ path,
21
+ hint: "A screen effect has a kind, a radius of at least 0, and at most 8 params."
22
+ });
23
+ }
24
+ function publishScreenEffect(world, effect) {
25
+ if (!effect.kind) throw invalid("A screen effect needs a kind", "kind");
26
+ if (!(effect.radius >= 0)) throw invalid(`Radius ${effect.radius} is below 0`, "radius");
27
+ const keys = Object.keys(effect.params);
28
+ if (keys.length > MAX_SCREEN_EFFECT_PARAMS) {
29
+ throw invalid(`${keys.length} params; at most ${MAX_SCREEN_EFFECT_PARAMS}`, "params");
30
+ }
31
+ const effects = world.initResource(ScreenEffects).effects;
32
+ let i = 0;
33
+ while (i < effects.length && (effects[i].source !== effect.source || effects[i].kind !== effect.kind))
34
+ i++;
35
+ if (i === MAX_SCREEN_EFFECTS) return false;
36
+ const e = effects[i];
37
+ if (e) {
38
+ e.screen[0] = effect.screen[0];
39
+ e.screen[1] = effect.screen[1];
40
+ e.radius = effect.radius;
41
+ e.ttlMs = effect.ttlMs;
42
+ e.params = effect.params;
43
+ } else effects.push({ ...effect, screen: [effect.screen[0], effect.screen[1]] });
44
+ world.touchResource(ScreenEffects);
45
+ return true;
46
+ }
47
+ function clearScreenEffects(world, source) {
48
+ const value = world.tryResource(ScreenEffects);
49
+ if (!value || value.effects.length === 0) return;
50
+ if (source === void 0) value.effects.length = 0;
51
+ else value.effects = value.effects.filter((e) => e.source !== source);
52
+ world.touchResource(ScreenEffects);
53
+ }
54
+ const NO_OFFSET = [0, 0];
55
+ function forwardScreenEffects(from, to, offset = NO_OFFSET) {
56
+ const source = from.tryResource(ScreenEffects)?.effects ?? [];
57
+ const out = to.initResource(ScreenEffects).effects;
58
+ const n = Math.min(source.length, MAX_SCREEN_EFFECTS);
59
+ for (let i = 0; i < n; i++) {
60
+ const s = source[i];
61
+ const d = out[i];
62
+ if (d) {
63
+ d.kind = s.kind;
64
+ d.radius = s.radius;
65
+ d.ttlMs = s.ttlMs;
66
+ d.source = s.source;
67
+ d.params = s.params;
68
+ } else out.push({ ...s, screen: [0, 0] });
69
+ const e = out[i];
70
+ e.screen[0] = s.screen[0] - offset[0];
71
+ e.screen[1] = s.screen[1] - offset[1];
72
+ }
73
+ if (out.length === 0 && n === 0) return;
74
+ out.length = n;
75
+ to.touchResource(ScreenEffects);
76
+ }
77
+ function onScreenEffect(world, kind, handler) {
78
+ world.initResource(ScreenEffectHandlers).kinds.set(kind, handler);
79
+ }
80
+ const SCREEN_EFFECTS_DEMAND = "render/screen-effects";
81
+ const expireScreenEffects = defineSystem({
82
+ name: "render/expire-screen-effects",
83
+ description: "Counts ScreenEffects down by the frame time and drops expired ones; holds frames while any live.",
84
+ run: (_, world) => {
85
+ const effects = world.tryResource(ScreenEffects)?.effects;
86
+ let n = 0;
87
+ if (effects && effects.length > 0) {
88
+ const ms = world.resource(Time).delta * 1e3;
89
+ for (let i = 0; i < effects.length; i++) {
90
+ const e = effects[i];
91
+ e.ttlMs -= ms;
92
+ if (e.ttlMs > 0 && n < MAX_SCREEN_EFFECTS) effects[n++] = e;
93
+ }
94
+ effects.length = n;
95
+ }
96
+ const live = world.tryResource(ScreenEffectHandlers)?.live.length ?? 0;
97
+ world.tryResource(FrameDemand)?.set(SCREEN_EFFECTS_DEMAND, n > 0 || live > 0);
98
+ }
99
+ });
100
+ function despawnAll(world, entities) {
101
+ for (const e of entities) if (world.isAlive(e)) world.despawn(e);
102
+ }
103
+ const runScreenEffects = defineSystem({
104
+ name: "render/screen-effects",
105
+ description: "Runs the handlers registered with onScreenEffect for live ScreenEffects.",
106
+ run: (_, world) => {
107
+ const handlers = world.tryResource(ScreenEffectHandlers);
108
+ const effects = world.tryResource(ScreenEffects)?.effects;
109
+ if (!handlers || handlers.live.length === 0 && (!effects || effects.length === 0)) return;
110
+ const live = handlers.live;
111
+ for (let i = 0; i < live.length; i++) live[i].seen = false;
112
+ const dt = world.resource(Time).delta;
113
+ if (effects) {
114
+ for (let i = 0; i < effects.length; i++) {
115
+ const e = effects[i];
116
+ let found;
117
+ for (let j = 0; j < live.length; j++) {
118
+ const l = live[j];
119
+ if (l.source === e.source && l.kind === e.kind) {
120
+ found = l;
121
+ break;
122
+ }
123
+ }
124
+ if (found) {
125
+ found.seen = true;
126
+ found.last = e;
127
+ found.handler.update?.(world, e, found.entities, dt);
128
+ continue;
129
+ }
130
+ const handler = handlers.kinds.get(e.kind);
131
+ if (!handler) {
132
+ if (!handlers.reported.has(e.kind)) {
133
+ handlers.reported.add(e.kind);
134
+ world.tryResource(LogResource)?.log("warn", `No handler draws "${e.kind}" effects`, {
135
+ code: "render/unhandled-screen-effect",
136
+ hint: "Register one in this app with onScreenEffect(world, kind, handler)."
137
+ });
138
+ }
139
+ continue;
140
+ }
141
+ live.push({
142
+ source: e.source,
143
+ kind: e.kind,
144
+ handler,
145
+ entities: handler.start(world, e) ?? [],
146
+ last: e,
147
+ seen: true
148
+ });
149
+ }
150
+ }
151
+ let kept = 0;
152
+ for (let i = 0; i < live.length; i++) {
153
+ const l = live[i];
154
+ if (l.seen) {
155
+ live[kept++] = l;
156
+ continue;
157
+ }
158
+ if (l.handler.end) l.handler.end(world, l.last, l.entities);
159
+ else despawnAll(world, l.entities);
160
+ }
161
+ live.length = kept;
162
+ }
163
+ });
164
+ function describeScreenEffects(world) {
165
+ const handlers = world.tryResource(ScreenEffectHandlers);
166
+ return {
167
+ effects: (world.tryResource(ScreenEffects)?.effects ?? []).map((e) => ({
168
+ ...e,
169
+ screen: [...e.screen]
170
+ })),
171
+ kinds: handlers ? [...handlers.kinds.keys()].sort() : [],
172
+ drawing: (handlers?.live ?? []).map((l) => ({
173
+ kind: l.kind,
174
+ source: l.source,
175
+ entities: l.entities.length
176
+ }))
177
+ };
178
+ }
179
+ export {
180
+ MAX_SCREEN_EFFECTS,
181
+ MAX_SCREEN_EFFECT_PARAMS,
182
+ SCREEN_EFFECTS_DEMAND,
183
+ ScreenEffectHandlers,
184
+ ScreenEffects,
185
+ clearScreenEffects,
186
+ describeScreenEffects,
187
+ expireScreenEffects,
188
+ forwardScreenEffects,
189
+ onScreenEffect,
190
+ publishScreenEffect,
191
+ runScreenEffects
192
+ };
package/dist/shaders.d.ts CHANGED
@@ -25,6 +25,12 @@ export declare const ViewUniform: import("@aethervtt/shard-core").ComponentDef<{
25
25
  * that sun, applied to its light (spec 0044). All ones without an atmosphere.
26
26
  */
27
27
  readonly sunTransmittance: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
28
+ /**
29
+ * Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
30
+ * unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
31
+ * width w at view depth d is w · x · y · d world units (w · x · y orthographic).
32
+ */
33
+ readonly pixelScale: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<import("@aethervtt/shard-core").Vec4> | undefined) => import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32">);
28
34
  }>;
29
35
  export declare const viewLayout: import("@aethervtt/shard-shader").WgslLayout<{
30
36
  readonly viewProj: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
@@ -51,6 +57,12 @@ export declare const viewLayout: import("@aethervtt/shard-shader").WgslLayout<{
51
57
  * that sun, applied to its light (spec 0044). All ones without an atmosphere.
52
58
  */
53
59
  readonly sunTransmittance: import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number]> | undefined) => import("@aethervtt/shard-core").FieldType<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">);
60
+ /**
61
+ * Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
62
+ * unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
63
+ * width w at view depth d is w · x · y · d world units (w · x · y orthographic).
64
+ */
65
+ readonly pixelScale: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<import("@aethervtt/shard-core").Vec4> | undefined) => import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec4, "f32">);
54
66
  }>;
55
67
  export declare const materialLayout: import("@aethervtt/shard-shader").WgslLayout<{
56
68
  baseColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
@@ -59,6 +71,7 @@ export declare const materialLayout: import("@aethervtt/shard-shader").WgslLayou
59
71
  emissive: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
60
72
  emissiveLuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
61
73
  doubleSided: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
74
+ deferUntilReady: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
62
75
  alphaMode: import("@aethervtt/shard-core").FieldType<"additive" | "alpha" | "mask" | "opaque" | "premultiplied", "u8">;
63
76
  alphaCutoff: import("@aethervtt/shard-core").FieldType<number, "f32">;
64
77
  normalScale: import("@aethervtt/shard-core").FieldType<number, "f32">;
package/dist/shaders.js CHANGED
@@ -26,7 +26,13 @@ const ViewUniform = defineComponent("render/ViewUniform", {
26
26
  * Per directional light (column i): the atmosphere's transmittance (rgb) between the camera and
27
27
  * that sun, applied to its light (spec 0044). All ones without an atmosphere.
28
28
  */
29
- sunTransmittance: t.mat4
29
+ sunTransmittance: t.mat4,
30
+ /**
31
+ * Screen scale (0057): render pixels per CSS pixel (x); world units per render pixel at one
32
+ * unit of view depth, or everywhere when orthographic (y); 1 when orthographic (z). A CSS-pixel
33
+ * width w at view depth d is w · x · y · d world units (w · x · y orthographic).
34
+ */
35
+ pixelScale: t.vec4
30
36
  });
31
37
  const viewLayout = wgslLayout(ViewUniform);
32
38
  const materialLayout = wgslLayout(StandardMaterial);
@@ -77,9 +83,9 @@ const FLAG_VISIBLE: u32 = 1u;
77
83
  const FLAG_CASTER: u32 = 2u;
78
84
  const FLAG_RECEIVER: u32 = 4u;
79
85
 
80
- @group(2) @binding(0) var<storage, read> instances: array<Instance>;
86
+ @data @group(2) @binding(0) var<storage, read> instances: array<Instance>;
81
87
  /** The view's culled slots; a draw's instance_index indexes this. */
82
- @group(2) @binding(1) var<storage, read> visible: array<u32>;
88
+ @data @group(2) @binding(1) var<storage, read> visible: array<u32>;
83
89
 
84
90
  /** Visible-list entries: slot in the low 28 bits, LOD level in the top 4. */
85
91
  const SLOT_MASK: u32 = 0x0fffffffu;
@@ -98,7 +104,7 @@ fn instance_world(inst: Instance, position: vec3f) -> vec3f {
98
104
  }
99
105
 
100
106
  /** Last frame's transform of each slot (three affine rows), for motion vectors. */
101
- @group(2) @binding(2) var<storage, read> previous: array<vec4f>;
107
+ @data @group(2) @binding(2) var<storage, read> previous: array<vec4f>;
102
108
 
103
109
  /** Where an object-space position was last frame, in world space. */
104
110
  fn previous_world(instance_index: u32, position: vec3f) -> vec3f {
@@ -171,9 +177,9 @@ struct Deform {
171
177
  const FLAG_SKINNED: u32 = 32u;
172
178
  const FLAG_MORPH: u32 = 64u;
173
179
 
174
- @group(2) @binding(3) var<storage, read> deforms: array<Deform>;
175
- @group(2) @binding(4) var<storage, read> poses: array<vec4f>;
176
- @group(2) @binding(5) var<storage, read> deform_data: array<u32>;
180
+ @data @group(2) @binding(3) var<storage, read> deforms: array<Deform>;
181
+ @data @group(2) @binding(4) var<storage, read> poses: array<vec4f>;
182
+ @data @group(2) @binding(5) var<storage, read> deform_data: array<u32>;
177
183
 
178
184
  struct Deformed {
179
185
  position: vec3f,
@@ -360,6 +366,7 @@ import shard::view::view;
360
366
  import shard::pbr::types::VertexOutput;
361
367
  import shard::mesh::mesh_vertex_at;
362
368
  import shard::unlit::shading::shade;
369
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
363
370
 
364
371
  @vertex fn vs(
365
372
  @builtin(instance_index) instance_index: u32,
@@ -376,9 +383,11 @@ import shard::unlit::shading::shade;
376
383
  }
377
384
 
378
385
  @fragment fn fs(in: VertexOutput) -> @location(0) vec4f {
386
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
379
387
  let c = shade(in);
380
388
  @if(MASK) if (c.a < 0.5) { discard; }
381
- return vec4f(c.rgb * view.exposure, c.a);
389
+ @if(OPAQUE) return vec4f(c.rgb * view.exposure, 1.0);
390
+ @if(!OPAQUE) return vec4f(c.rgb * view.exposure, c.a);
382
391
  }`,
383
392
  "shard::pbr::brdf": `
384
393
  const PI: f32 = 3.14159265359;
@@ -416,8 +425,24 @@ struct Light {
416
425
  shadow_bias: f32,
417
426
  shadow_normal_bias: f32,
418
427
  shadow_softness: f32,
419
- _pad0: f32,
420
- _pad1: f32,
428
+ /** 0: physical; 1: tabletop (0057). */
429
+ falloff: f32,
430
+ /** Tabletop: full intensity inside this radius. */
431
+ bright: f32,
432
+ }
433
+
434
+ /**
435
+ * Distance attenuation of a point or spot light: physical inverse square windowed to zero at
436
+ * range, or tabletop (0057): full intensity inside bright, linear to zero at range.
437
+ */
438
+ fn light_falloff(light: Light, d: f32, d2: f32) -> f32 {
439
+ if (light.falloff > 0.5) {
440
+ return clamp((light.range - d) / max(light.range - light.bright, 1e-4), 0.0, 1.0);
441
+ }
442
+ let ratio = d / light.range;
443
+ let r4 = ratio * ratio * ratio * ratio;
444
+ let window = clamp(1.0 - r4, 0.0, 1.0);
445
+ return window * window / d2;
421
446
  }
422
447
 
423
448
  struct DirectionalLight {
@@ -446,10 +471,29 @@ const CLUSTER_Z: u32 = 24u;
446
471
  const CLUSTER_COUNT: u32 = 3456u;
447
472
  const MAX_PER_CLUSTER: u32 = 128u;
448
473
 
449
- @group(0) @binding(1) var<storage, read> lights: array<Light>;
474
+ @data(uniform, 128) @group(0) @binding(1) var<storage, read> lights: array<Light>;
450
475
  /** counts[CLUSTER_COUNT], then MAX_PER_CLUSTER light indices per cluster. */
451
- @group(0) @binding(2) var<storage, read> clusters: array<u32>;
452
- @group(0) @binding(3) var<storage, read> directional: DirectionalLights;`,
476
+ @if(!BASELINE) @group(0) @binding(2) var<storage, read> clusters: array<u32>;
477
+ /** Baseline (0064): a bit per packed light, 128 per cluster. */
478
+ @if(BASELINE) @data @group(0) @binding(2) var<storage, read> cluster_bits: array<vec4u>;
479
+ @data(uniform) @group(0) @binding(3) var<storage, read> directional: DirectionalLights;
480
+
481
+ /**
482
+ * Baseline (0064): takes the lowest light left in \`mask\` (a cluster's bits) into \`index\`;
483
+ * false once none are left.
484
+ */
485
+ @if(BASELINE) fn next_light(mask: ptr<function, vec4u>, index: ptr<function, u32>) -> bool {
486
+ for (var w = 0u; w < 4u; w++) {
487
+ let word = (*mask)[w];
488
+ if (word != 0u) {
489
+ let lowest = word & (~word + 1u);
490
+ (*mask)[w] = word ^ lowest;
491
+ *index = w * 32u + u32(round(log2(f32(lowest))));
492
+ return true;
493
+ }
494
+ }
495
+ return false;
496
+ }`,
453
497
  "shard::pbr::shadows": `
454
498
  import shard::color::ign;
455
499
 
@@ -473,7 +517,7 @@ struct ShadowData {
473
517
  spot_params: array<vec4f, 8>,
474
518
  }
475
519
 
476
- @group(0) @binding(4) var<storage, read> shadows: ShadowData;
520
+ @data(uniform) @group(0) @binding(4) var<storage, read> shadows: ShadowData;
477
521
  @group(0) @binding(5) var cascade_maps: texture_depth_2d_array;
478
522
  @group(0) @binding(6) var spot_maps: texture_depth_2d_array;
479
523
  @group(0) @binding(7) var point_maps: texture_depth_2d_array;
@@ -576,9 +620,11 @@ fn point_shadow(index: u32, world: vec3f, n: vec3f, bias: f32, normal_bias: f32,
576
620
  import shard::view::view;
577
621
  import shard::pbr::types::PbrInput;
578
622
  import shard::pbr::brdf::{ PI, d_ggx, v_smith_ggx_correlated, f_schlick };
579
- import shard::pbr::lights::{ lights, clusters, directional, LIGHT_SPOT, NO_SHADOW, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, CLUSTER_COUNT, MAX_PER_CLUSTER };
623
+ import shard::pbr::lights::{ lights, clusters, cluster_bits, next_light, directional, light_falloff, LIGHT_SPOT, NO_SHADOW, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, CLUSTER_COUNT, MAX_PER_CLUSTER };
580
624
  import shard::pbr::shadows::{ directional_shadow, spot_shadow, point_shadow, cascade_index };
581
625
  import shard::pbr::environment::environment_light;
626
+ @if(SKY_VISIBILITY) import shard::interior::interior_ambient;
627
+ @if(BLOCKED_LIGHTS) import shard::interior::{ wall_visibility, interior_receiver };
582
628
 
583
629
  const FLAG_RECEIVER: u32 = 4u;
584
630
 
@@ -642,7 +688,39 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
642
688
  // Point and spot lights from this fragment's cluster: intensity (cd) / d\xB2 \xD7 window.
643
689
  let cluster = cluster_of(frag_coord, view_depth);
644
690
  var count = 0u;
645
- if (cluster >= 0) {
691
+ @if(BLOCKED_LIGHTS) let receiver = interior_receiver(world_position, n);
692
+ @if(BASELINE) if (cluster >= 0) {
693
+ // The cluster's lights, lowest first; keep the body in step with the loop below.
694
+ var mask = cluster_bits[cluster];
695
+ var index = 0u;
696
+ while (next_light(&mask, &index)) {
697
+ count++;
698
+ let light = lights[index];
699
+ let to_light = light.position - world_position;
700
+ let d2 = max(dot(to_light, to_light), 1e-4);
701
+ let d = sqrt(d2);
702
+ let l = to_light / d;
703
+ var attenuation = light_falloff(light, d, d2);
704
+ if (light.kind == LIGHT_SPOT) {
705
+ let cd = dot(-l, light.direction);
706
+ let spot = clamp(cd * light.spot_scale + light.spot_offset, 0.0, 1.0);
707
+ attenuation *= spot * spot;
708
+ }
709
+ if (attenuation <= 0.0) { continue; }
710
+ @if(BLOCKED_LIGHTS) attenuation *= wall_visibility(light, receiver);
711
+ if (receives && light.shadow != NO_SHADOW) {
712
+ if (light.kind == LIGHT_SPOT) {
713
+ attenuation *= spot_shadow(light.shadow, world_position, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
714
+ } else {
715
+ attenuation *= point_shadow(light.shadow, world_position, n, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
716
+ }
717
+ }
718
+ let a_light = clamp(a + light.radius / (2.0 * d), 0.0, 1.0);
719
+ let norm = (a / a_light) * (a / a_light);
720
+ color += brdf(n, v, l, a_light, diffuse_color, f0, norm) * light.color * attenuation;
721
+ }
722
+ }
723
+ @if(!BASELINE) if (cluster >= 0) {
646
724
  count = clusters[cluster];
647
725
  let base = CLUSTER_COUNT + u32(cluster) * MAX_PER_CLUSTER;
648
726
  for (var k = 0u; k < count; k++) {
@@ -651,16 +729,14 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
651
729
  let d2 = max(dot(to_light, to_light), 1e-4);
652
730
  let d = sqrt(d2);
653
731
  let l = to_light / d;
654
- let ratio = d / light.range;
655
- let r4 = ratio * ratio * ratio * ratio;
656
- let window = clamp(1.0 - r4, 0.0, 1.0);
657
- var attenuation = window * window / d2;
732
+ var attenuation = light_falloff(light, d, d2);
658
733
  if (light.kind == LIGHT_SPOT) {
659
734
  let cd = dot(-l, light.direction);
660
735
  let spot = clamp(cd * light.spot_scale + light.spot_offset, 0.0, 1.0);
661
736
  attenuation *= spot * spot;
662
737
  }
663
738
  if (attenuation <= 0.0) { continue; }
739
+ @if(BLOCKED_LIGHTS) attenuation *= wall_visibility(light, receiver);
664
740
  if (receives && light.shadow != NO_SHADOW) {
665
741
  if (light.kind == LIGHT_SPOT) {
666
742
  attenuation *= spot_shadow(light.shadow, world_position, n, light.position, light.shadow_bias, light.shadow_normal_bias, light.shadow_softness, frag_coord.xy);
@@ -683,9 +759,11 @@ fn apply_lighting(p: PbrInput, world_position: vec3f, frag_coord: vec4f, flags:
683
759
  surface.occlusion *= textureLoad(ao_texture, min(vec2i(frag_coord.xy), dims - 1), 0).r;
684
760
  }
685
761
  if (view.envParams.w > 0.5) {
686
- color += environment_light(surface, n, v);
762
+ @if(!SKY_VISIBILITY) color += environment_light(surface, n, v);
763
+ @if(SKY_VISIBILITY) color += interior_ambient(environment_light(surface, n, v), diffuse_color * surface.occlusion, world_position, n);
687
764
  } else {
688
- color += diffuse_color * view.ambient * surface.occlusion;
765
+ @if(!SKY_VISIBILITY) color += diffuse_color * view.ambient * surface.occlusion;
766
+ @if(SKY_VISIBILITY) color += interior_ambient(diffuse_color * view.ambient * surface.occlusion, diffuse_color * surface.occlusion, world_position, n);
689
767
  }
690
768
 
691
769
  let debug = u32(view.clusterParams.w);
@@ -912,6 +990,7 @@ import shard::mesh::mesh_vertex_at;
912
990
  import shard::pbr::material::{ pbr_input, fragment_output };
913
991
  import shard::pbr::standard::material;
914
992
  import shard::pbr::lighting::apply_lighting;
993
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
915
994
 
916
995
  @vertex fn vs(
917
996
  @builtin(instance_index) instance_index: u32,
@@ -928,12 +1007,15 @@ import shard::pbr::lighting::apply_lighting;
928
1007
  }
929
1008
 
930
1009
  @fragment fn fs(in: VertexOutput) -> @location(0) vec4f {
1010
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
931
1011
  let p = pbr_input(in);
932
1012
  if (material.alphaMode == 1u && p.alpha < material.alphaCutoff) { discard; }
933
1013
  let color = (apply_lighting(p, in.world_position, in.clip, in.flags) + p.emissive) * view.exposure;
934
1014
  // Premultiplied blending expects color \xD7 alpha; additive and alpha blend multiply in hardware.
935
1015
  @if(PREMULTIPLY) return fragment_output(vec4f(color * p.alpha, p.alpha));
936
- @if(!PREMULTIPLY) return fragment_output(vec4f(color, p.alpha));
1016
+ // Opaque and masked surfaces cover their pixel, whatever their base color's alpha (0052).
1017
+ @if(OPAQUE) return vec4f(fragment_output(vec4f(color, p.alpha)).rgb, 1.0);
1018
+ @if(!PREMULTIPLY && !OPAQUE) return fragment_output(vec4f(color, p.alpha));
937
1019
  }`,
938
1020
  "shard::post::depth_resolve": `
939
1021
  @group(0) @binding(0) var depth_ms: texture_depth_multisampled_2d;
@@ -950,7 +1032,7 @@ import shard::pbr::types::PbrInput;
950
1032
  @group(0) @binding(10) var env_lut: texture_2d<f32>;
951
1033
  @group(0) @binding(11) var env_sampler: sampler;
952
1034
  /** SH9, convolved with the cosine lobe and divided by \u03C0: dot with the basis = irradiance / \u03C0. */
953
- @group(0) @binding(12) var<storage, read> env_sh: array<vec4f, 9>;
1035
+ @data(uniform) @group(0) @binding(12) var<storage, read> env_sh: array<vec4f, 9>;
954
1036
  @group(0) @binding(13) var env_source: texture_cube<f32>;
955
1037
 
956
1038
  const ENV_SPECULAR_MIPS: f32 = 5.0;
@@ -1065,6 +1147,13 @@ fn apply_lut(c: vec3f) -> vec3f {
1065
1147
  @fragment fn fs(@builtin(position) p: vec4f) -> @location(0) vec4f {
1066
1148
  let texel = textureLoad(hdr, vec2i(p.xy), 0);
1067
1149
  var hdr_color = max(texel.rgb, vec3f(0.0));
1150
+ @if(TRANSPARENT) var alpha = 1.0;
1151
+ @if(TRANSPARENT) {
1152
+ // Premultiplied (0052): light over less coverage raises alpha, so a glow shows over the page;
1153
+ // then the color is un-premultiplied for grading and the curve, and premultiplied again below.
1154
+ alpha = clamp(max(texel.a, max(hdr_color.r, max(hdr_color.g, hdr_color.b))), 0.0, 1.0);
1155
+ hdr_color = hdr_color / max(alpha, 1e-6);
1156
+ }
1068
1157
  let flags = params.mode.z;
1069
1158
  if ((flags & 1u) != 0u) { hdr_color = grade(hdr_color); }
1070
1159
  if ((flags & 2u) != 0u) {
@@ -1080,9 +1169,12 @@ fn apply_lut(c: vec3f) -> vec3f {
1080
1169
  let n = ign(p.xy) + ign(p.xy + vec2f(71.0, 13.0)) - 1.0;
1081
1170
  c = c + vec3f(n / 255.0);
1082
1171
  }
1172
+ // The page composites in display space, so premultiply there; it keeps rgb <= alpha.
1173
+ @if(TRANSPARENT) c = clamp(c, vec3f(0.0), vec3f(1.0)) * alpha;
1083
1174
  // sRGB targets encode in hardware: hand them linear values.
1084
1175
  @if(SRGB_TARGET) c = srgb_to_linear(clamp(c, vec3f(0.0), vec3f(1.0)));
1085
- return vec4f(c, 1.0);
1176
+ @if(TRANSPARENT) return vec4f(c, max(alpha, max(c.r, max(c.g, c.b))));
1177
+ @if(!TRANSPARENT) return vec4f(c, 1.0);
1086
1178
  }`,
1087
1179
  "shard::post::upscale": `
1088
1180
  struct Upscale {
@@ -1094,8 +1186,18 @@ struct Upscale {
1094
1186
  @group(0) @binding(1) var input_sampler: sampler;
1095
1187
  @group(0) @binding(2) var<uniform> upscale: Upscale;
1096
1188
 
1097
- fn at(uv: vec2f) -> vec3f {
1098
- return textureSampleLevel(input, input_sampler, uv, 0.0).rgb;
1189
+ @if(!TRANSPARENT) alias Texel = vec3f;
1190
+ @if(TRANSPARENT) alias Texel = vec4f;
1191
+
1192
+ fn at(uv: vec2f) -> Texel {
1193
+ @if(!TRANSPARENT) return textureSampleLevel(input, input_sampler, uv, 0.0).rgb;
1194
+ @if(TRANSPARENT) return textureSampleLevel(input, input_sampler, uv, 0.0);
1195
+ }
1196
+
1197
+ /** Opaque views write alpha 1; premultiplied ones keep rgb <= alpha after sharpening (0052). */
1198
+ fn out(c: Texel) -> vec4f {
1199
+ @if(!TRANSPARENT) return vec4f(c, 1.0);
1200
+ @if(TRANSPARENT) return vec4f(c.rgb, max(c.a, max(c.r, max(c.g, c.b))));
1099
1201
  }
1100
1202
 
1101
1203
  /**
@@ -1106,7 +1208,7 @@ fn at(uv: vec2f) -> vec3f {
1106
1208
  let uv = frag.xy * upscale.params.xy;
1107
1209
  let c = at(uv);
1108
1210
  let k = upscale.params.z;
1109
- if (k <= 0.0) { return vec4f(c, 1.0); }
1211
+ if (k <= 0.0) { return out(c); }
1110
1212
  let texel = 1.0 / vec2f(textureDimensions(input));
1111
1213
  let n = at(uv + vec2f(0.0, -texel.y));
1112
1214
  let s = at(uv + vec2f(0.0, texel.y));
@@ -1115,7 +1217,7 @@ fn at(uv: vec2f) -> vec3f {
1115
1217
  let lo = min(c, min(min(n, s), min(e, w)));
1116
1218
  let hi = max(c, max(max(n, s), max(e, w)));
1117
1219
  let sharp = c + (4.0 * c - (n + s + e + w)) * (0.25 * k);
1118
- return vec4f(clamp(sharp, lo, hi), 1.0);
1220
+ return out(clamp(sharp, lo, hi));
1119
1221
  }`,
1120
1222
  "shard::post::common": `
1121
1223
  import shard::view::view;
@@ -0,0 +1,9 @@
1
+ export declare const SHADOW_CATCHER_SHADERS: Record<string, string>;
2
+ /**
3
+ * Invisible except for the shadows it receives (0052): color 0, alpha `opacity × (1 − visibility)`
4
+ * over every shadow-casting light. Premultiplied, so it darkens what's behind it by that alpha.
5
+ * Needs `shadowCatcherPlugin` (in `forwardPlugin`).
6
+ */
7
+ export declare const ShadowCatcher: import("./materials.js").MaterialType;
8
+ /** The ShadowCatcher material's shader. */
9
+ export declare const shadowCatcherPlugin: import("@aethervtt/shard-runtime").Plugin;