@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
package/dist/layers.js ADDED
@@ -0,0 +1,49 @@
1
+ import { defineComponent, t } from "@aethervtt/shard-core";
2
+ import { Transform } from "@aethervtt/shard-transform";
3
+ const RenderLayers = defineComponent(
4
+ "render/RenderLayers",
5
+ {
6
+ mask: t.u16({
7
+ default: 1,
8
+ description: "Layer bits, 16 layers (1 is layer 1). A camera draws it if any bit is also in its layers."
9
+ })
10
+ },
11
+ {
12
+ description: "Per-view visuals: a token keeps its Transform on a root, and each child visual (a flat disc, a standee) carries the layer of the camera that shows it."
13
+ }
14
+ );
15
+ const GROUND_BANDS = {
16
+ tiles: 10,
17
+ grid: 20,
18
+ drawings: 30,
19
+ "tokens-flat": 40,
20
+ fog: 50,
21
+ overlay: 60
22
+ };
23
+ const OVERLAY_BAND = GROUND_BANDS.overlay;
24
+ const GroundLayer = defineComponent(
25
+ "render/GroundLayer",
26
+ {
27
+ band: t.i16({
28
+ default: GROUND_BANDS.drawings,
29
+ presets: GROUND_BANDS,
30
+ description: "Stacking band: tiles 10, grid 20, drawings 30, tokens-flat 40, fog 50, overlay 60. Higher draws over lower. Bands from 60 up draw after transparent objects and projected fog."
31
+ }),
32
+ order: t.i32({ description: "Order within the band: higher draws over lower." }),
33
+ level: t.i8({
34
+ min: -16,
35
+ max: 15,
36
+ description: "Structure level index (0067): ground layers sort by level first, so a lower level never draws over an upper one. 0 is the ground level."
37
+ })
38
+ },
39
+ {
40
+ description: "Draws a mesh in the ground phase: coplanar layers in a fixed order, hidden by walls and props, never z-fighting.",
41
+ requires: [Transform]
42
+ }
43
+ );
44
+ export {
45
+ GROUND_BANDS,
46
+ GroundLayer,
47
+ OVERLAY_BAND,
48
+ RenderLayers
49
+ };
@@ -0,0 +1,43 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /** The largest lens target, in pixels: the bound of Aether's ADR-0098. */
3
+ export declare const MAX_LENS_PIXELS = 2097152;
4
+ export declare const LENS_SHADERS: Record<string, string>;
5
+ /** A Lens view's fields this frame, in its target's pixels, and the texture they bend from. */
6
+ export interface LensView {
7
+ /** The view's name last frame. */
8
+ view: string;
9
+ /** Fields that intersect the view. */
10
+ count: number;
11
+ /** The pass's uniforms: region (4), count (4), then center x, y, radius, strength per field. */
12
+ params: Float32Array;
13
+ /** The source of each field. */
14
+ sources: Float64Array;
15
+ /** Target pixels per CSS pixel. */
16
+ pixelRatio: number;
17
+ /** Cut down to MAX_LENS_PIXELS: the fields' bounds were larger. */
18
+ clipped: boolean;
19
+ /** The copy of the region, while a field intersects the view. */
20
+ texture: GPUTexture | undefined;
21
+ generation: number;
22
+ /** post/lens ran for the view last frame. */
23
+ ran: boolean;
24
+ seen: number;
25
+ }
26
+ export declare const LensViews: import("@aethervtt/shard-core").ResourceDef<Map<number, LensView>>;
27
+ export declare const extractLens: import("@aethervtt/shard-core").SystemDef<undefined>;
28
+ /** render.describe's lens section: live fields, and per Lens view what it bent and at what size. */
29
+ export declare function describeLens(world: World): {
30
+ fields: {
31
+ screen: [number, number];
32
+ radius: number;
33
+ strength: number;
34
+ ttlMs: number;
35
+ source: Entity;
36
+ }[];
37
+ views: Record<string, unknown>;
38
+ };
39
+ /**
40
+ * Screen-space lens fields (spec 0063): cameras with Lens bend their own pixels inside the live
41
+ * LensFields, in a post pass that isn't in the graph while no field is.
42
+ */
43
+ export declare const lensPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,304 @@
1
+ import { defineResource, defineSystem, Last } from "@aethervtt/shard-core";
2
+ import { definePlugin } from "@aethervtt/shard-runtime";
3
+ import { addRenderFeatures } from "./features.js";
4
+ import { RenderPhase } from "./graph.js";
5
+ import { Lens, LensFields, LensPath, MAX_LENS_FIELDS } from "./lens.js";
6
+ import { Graph, RenderDescribers, RenderSet, Shaders, Views } from "./plugin.js";
7
+ import { beginPass, idOf, PostCache, tex, uniform } from "./post-common.js";
8
+ import { registerShaders } from "./shaders.js";
9
+ import { cameraOf, extractCameras } from "./view.js";
10
+ const MAX_LENS_PIXELS = 2097152;
11
+ const LENS_SHADERS = {
12
+ "shard::post::lens": `
13
+ struct Lens {
14
+ /** The copied region of the target: origin (xy) and size (zw), in pixels. */
15
+ region: vec4f,
16
+ /** x: how many fields. */
17
+ count: vec4f,
18
+ /** Center (xy) and radius (z) in target pixels, strength (w). */
19
+ fields: array<vec4f, 4>,
20
+ }
21
+
22
+ @group(0) @binding(0) var region_copy: texture_2d<f32>;
23
+ @group(0) @binding(1) var<uniform> lens: Lens;
24
+
25
+ fn texel(p: vec2f) -> vec4f {
26
+ let last = vec2i(lens.region.zw) - 1;
27
+ return textureLoad(region_copy, clamp(vec2i(p), vec2i(0), last), 0);
28
+ }
29
+
30
+ /** Bilinear between the copy's texel centers, clamped to its edge; exact on a center. */
31
+ fn sample_copy(p: vec2f) -> vec4f {
32
+ let q = p - lens.region.xy - 0.5;
33
+ let base = floor(q);
34
+ let t = q - base;
35
+ let top = mix(texel(base), texel(base + vec2f(1.0, 0.0)), t.x);
36
+ let bottom = mix(texel(base + vec2f(0.0, 1.0)), texel(base + vec2f(1.0, 1.0)), t.x);
37
+ return mix(top, bottom, t.y);
38
+ }
39
+
40
+ /** Inside a field, the view's own pixels at displaced coordinates; outside every field, nothing. */
41
+ @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
42
+ var offset = vec2f(0.0);
43
+ var inside = false;
44
+ let n = u32(lens.count.x);
45
+ for (var i = 0u; i < n; i++) {
46
+ let f = lens.fields[i];
47
+ let d = frag.xy - f.xy;
48
+ let u = length(d) / f.z;
49
+ if (u < 1.0) {
50
+ inside = true;
51
+ // Distance u samples from u (1 - s (1 - u)\xB2): unchanged at the rim, never outside it.
52
+ let k = 1.0 - u;
53
+ offset -= d * (f.w * k * k);
54
+ }
55
+ }
56
+ if (!inside) { discard; }
57
+ return sample_copy(frag.xy + offset);
58
+ }`
59
+ };
60
+ const LensViews = defineResource("render/LensViews", {
61
+ description: "Lens cameras' fields in target pixels and their lens targets, by camera entity.",
62
+ init: () => /* @__PURE__ */ new Map()
63
+ });
64
+ const clamp1 = (s) => Number.isNaN(s) ? 0 : Math.min(1, Math.max(-1, s));
65
+ let frame = 0;
66
+ function releaseUnseen(state, entity, map) {
67
+ if (state.seen === frame) return;
68
+ state.texture?.destroy();
69
+ map.delete(entity);
70
+ }
71
+ const extractLens = defineSystem({
72
+ name: "render/extract-lens",
73
+ description: "Maps live LensFields into each Lens view's pixels, and sizes or releases its lens target.",
74
+ run: (_, world) => {
75
+ frame++;
76
+ const views = world.resource(Views).list;
77
+ const fields = world.resource(LensFields).fields;
78
+ const states = world.resource(LensViews);
79
+ for (let v = 0; v < views.length; v++) {
80
+ const view = views[v];
81
+ const cam = cameraOf(view);
82
+ if (!cam || cam.pixelPerfect || !world.has(cam.entity, Lens)) continue;
83
+ let s = states.get(cam.entity);
84
+ if (!s) {
85
+ s = {
86
+ view: view.name,
87
+ count: 0,
88
+ params: new Float32Array(8 + 4 * MAX_LENS_FIELDS),
89
+ sources: new Float64Array(MAX_LENS_FIELDS),
90
+ pixelRatio: 1,
91
+ clipped: false,
92
+ texture: void 0,
93
+ generation: -1,
94
+ ran: false,
95
+ seen: 0
96
+ };
97
+ states.set(cam.entity, s);
98
+ }
99
+ s.seen = frame;
100
+ s.view = view.name;
101
+ s.ran = false;
102
+ s.clipped = false;
103
+ const width = cam.displayWidth;
104
+ const height = cam.displayHeight;
105
+ const ratio = cam.pixelRatio;
106
+ s.pixelRatio = ratio;
107
+ const p = s.params;
108
+ let count = 0;
109
+ let x0 = width;
110
+ let y0 = height;
111
+ let x1 = 0;
112
+ let y1 = 0;
113
+ for (let i = 0; i < fields.length && count < MAX_LENS_FIELDS; i++) {
114
+ const f = fields[i];
115
+ const cx = f.screen[0] * ratio;
116
+ const cy = f.screen[1] * ratio;
117
+ const r = f.radius * ratio;
118
+ if (!(r > 0) || !Number.isFinite(r + cx + cy)) continue;
119
+ const fx0 = Math.max(0, Math.floor(cx - r));
120
+ const fy0 = Math.max(0, Math.floor(cy - r));
121
+ const fx1 = Math.min(width, Math.ceil(cx + r));
122
+ const fy1 = Math.min(height, Math.ceil(cy + r));
123
+ if (fx0 >= fx1 || fy0 >= fy1) continue;
124
+ const o = 8 + count * 4;
125
+ p[o] = cx;
126
+ p[o + 1] = cy;
127
+ p[o + 2] = r;
128
+ p[o + 3] = clamp1(f.strength);
129
+ s.sources[count] = f.source;
130
+ count++;
131
+ if (fx0 < x0) x0 = fx0;
132
+ if (fy0 < y0) y0 = fy0;
133
+ if (fx1 > x1) x1 = fx1;
134
+ if (fy1 > y1) y1 = fy1;
135
+ }
136
+ s.count = count;
137
+ p[4] = count;
138
+ if (count === 0) {
139
+ s.texture?.destroy();
140
+ s.texture = void 0;
141
+ continue;
142
+ }
143
+ let w = x1 - x0;
144
+ let h = y1 - y0;
145
+ if (w * h > MAX_LENS_PIXELS) {
146
+ const k = Math.sqrt(MAX_LENS_PIXELS / (w * h));
147
+ const cw = Math.max(1, Math.floor(w * k));
148
+ const ch = Math.max(1, Math.floor(h * k));
149
+ x0 = Math.min(width - cw, Math.max(0, Math.round(x0 + (w - cw) / 2)));
150
+ y0 = Math.min(height - ch, Math.max(0, Math.round(y0 + (h - ch) / 2)));
151
+ w = cw;
152
+ h = ch;
153
+ s.clipped = true;
154
+ }
155
+ p[0] = x0;
156
+ p[1] = y0;
157
+ p[2] = w;
158
+ p[3] = h;
159
+ }
160
+ states.forEach(releaseUnseen);
161
+ }
162
+ });
163
+ function lensTexture(gpu, s, format) {
164
+ const w = s.params[2];
165
+ const h = s.params[3];
166
+ let t = s.texture;
167
+ if (t && s.generation === gpu.generation && t.format === format) {
168
+ if (t.width >= w && t.height >= h) return t;
169
+ } else t = void 0;
170
+ let tw = Math.max(w, t?.width ?? 0);
171
+ let th = Math.max(h, t?.height ?? 0);
172
+ if (tw * th > MAX_LENS_PIXELS) {
173
+ tw = w;
174
+ th = h;
175
+ }
176
+ s.texture?.destroy();
177
+ s.generation = gpu.generation;
178
+ s.texture = gpu.device.createTexture({
179
+ label: `${s.view}/lens`,
180
+ size: [tw, th],
181
+ format,
182
+ usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
183
+ });
184
+ return s.texture;
185
+ }
186
+ function lensNode(states) {
187
+ const cache = new PostCache();
188
+ let layout;
189
+ let layoutGen = -1;
190
+ return {
191
+ kind: "raw",
192
+ // After the display stage wrote the target (tonemap, FXAA, upscale), before overlays.
193
+ phase: RenderPhase.Display + 20,
194
+ enabled: (view) => {
195
+ const cam = cameraOf(view);
196
+ return cam !== void 0 && (states.get(cam.entity)?.count ?? 0) > 0;
197
+ },
198
+ writes: ["view-target"],
199
+ run: (ctx) => {
200
+ const gpu = ctx.gpu;
201
+ const s = states.get(cameraOf(ctx.view).entity);
202
+ if (!layout || layoutGen !== gpu.generation) {
203
+ layoutGen = gpu.generation;
204
+ layout = gpu.layouts.bindGroupLayout({
205
+ label: "lens",
206
+ entries: [tex(0, "unfilterable-float"), uniform(1)]
207
+ });
208
+ }
209
+ const target = ctx.texture("view-target");
210
+ const format = target.format;
211
+ const pipeline = cache.render(
212
+ ctx,
213
+ `lens/${format}`,
214
+ "shard::post::lens",
215
+ "fs",
216
+ [layout],
217
+ [{ format }]
218
+ );
219
+ if (!pipeline) return;
220
+ const copy = lensTexture(gpu, s, format);
221
+ const p = s.params;
222
+ const x = p[0];
223
+ const y = p[1];
224
+ const w = p[2];
225
+ const h = p[3];
226
+ ctx.encoder.copyTextureToTexture(
227
+ { texture: target, origin: { x, y } },
228
+ { texture: copy },
229
+ { width: w, height: h }
230
+ );
231
+ const params = cache.buffer(gpu, `${ctx.view.name}/lens`, p.byteLength);
232
+ params.write(p);
233
+ const group = cache.group(
234
+ gpu,
235
+ `${ctx.view.name}/lens`,
236
+ `${idOf(copy)}/${params.version}`,
237
+ layout,
238
+ () => [
239
+ { binding: 0, resource: copy.createView() },
240
+ { binding: 1, resource: { buffer: params.buffer } }
241
+ ]
242
+ );
243
+ const pass = beginPass(ctx, "post/lens", target.createView(), true);
244
+ pass.setPipeline(pipeline);
245
+ pass.setBindGroup(0, group);
246
+ pass.setScissorRect(x, y, w, h);
247
+ pass.draw(3);
248
+ pass.end();
249
+ s.ran = true;
250
+ }
251
+ };
252
+ }
253
+ function describeLens(world) {
254
+ const views = {};
255
+ world.resource(LensViews).forEach((s) => {
256
+ const fields = [];
257
+ for (let i = 0; i < s.count; i++) {
258
+ const o = 8 + i * 4;
259
+ fields.push({
260
+ source: s.sources[i],
261
+ screen: [s.params[o] / s.pixelRatio, s.params[o + 1] / s.pixelRatio],
262
+ radius: s.params[o + 2] / s.pixelRatio,
263
+ strength: s.params[o + 3]
264
+ });
265
+ }
266
+ views[s.view] = {
267
+ fields,
268
+ ran: s.ran,
269
+ region: s.count > 0 ? [...s.params.subarray(0, 4)] : null,
270
+ target: s.texture ? [s.texture.width, s.texture.height] : null,
271
+ clipped: s.clipped
272
+ };
273
+ });
274
+ return { fields: world.resource(LensFields).fields.map((f) => ({ ...f })), views };
275
+ }
276
+ const lensPlugin = definePlugin({
277
+ name: "render/lens",
278
+ dependencies: ["render/forward"],
279
+ provides: [LensViews],
280
+ build(app) {
281
+ app.insertResource(LensPath, { installed: true });
282
+ app.world.initResource(LensViews);
283
+ app.addSystems(Last, extractLens.inSet(RenderSet.Extract).after(extractCameras));
284
+ },
285
+ ready(app) {
286
+ registerShaders(app.world.resource(Shaders), LENS_SHADERS);
287
+ app.world.initResource(RenderDescribers).set("lens", describeLens);
288
+ addRenderFeatures(app.world, {
289
+ name: "render/lens",
290
+ description: "Screen-space lens fields (bounded displacement).",
291
+ nodes: ["post/lens"],
292
+ baseline: { strategy: "The same fragment passes" }
293
+ });
294
+ app.world.resource(Graph).addNode("post/lens", lensNode(app.world.resource(LensViews)));
295
+ }
296
+ });
297
+ export {
298
+ LENS_SHADERS,
299
+ LensViews,
300
+ MAX_LENS_PIXELS,
301
+ describeLens,
302
+ extractLens,
303
+ lensPlugin
304
+ };
package/dist/lens.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /** Fields `LensFields` holds at once. */
3
+ export declare const MAX_LENS_FIELDS = 4;
4
+ /** A screen-space field that bends what's behind it (0063). */
5
+ export interface LensField {
6
+ /** Center, in the target's CSS pixels. */
7
+ screen: [number, number];
8
+ /** In CSS pixels. */
9
+ radius: number;
10
+ /** -1..1: above 0 magnifies the middle, below 0 pulls the surroundings in. 0 bends nothing. */
11
+ strength: number;
12
+ /** Ms it has left: counts down each frame, and refreshing sets it again. At 0 it drops. */
13
+ ttlMs: number;
14
+ /** What published it (a die), so it can clear its own. */
15
+ source: Entity;
16
+ }
17
+ export declare const LensFields: import("@aethervtt/shard-core").ResourceDef<{
18
+ fields: LensField[];
19
+ }>;
20
+ export declare const Lens: import("@aethervtt/shard-core").ComponentDef<{}>;
21
+ /** Present when lensPlugin is installed; without it Lens cameras render unbent. */
22
+ export declare const LensPath: import("@aethervtt/shard-core").ResourceDef<{
23
+ installed: true;
24
+ }>;
25
+ /**
26
+ * Publishes or refreshes `source`'s field: a field with the same source is replaced. Returns false
27
+ * when `MAX_LENS_FIELDS` other sources hold fields already.
28
+ */
29
+ export declare function publishLensField(world: World, field: LensField): boolean;
30
+ /** Drops `source`'s fields, or every field. Publishers call it on dismissal and teardown. */
31
+ export declare function clearLensFields(world: World, source?: Entity): void;
32
+ /**
33
+ * Copies `from`'s fields into `to` (another app on the device, 0052), for the host to call each
34
+ * frame they're live. Copies, not shares: each app counts its fields down on its own. `offset` is
35
+ * where `to`'s target sits in `from`'s CSS pixels (a table canvas below a page-wide dice canvas's
36
+ * top bar, say); it's subtracted from each field's center.
37
+ */
38
+ export declare function forwardLensFields(from: World, to: World, offset?: readonly [number, number]): void;
39
+ /** The frame demand live fields hold, so they expire on time and the app idles after (0052). */
40
+ export declare const LENS_DEMAND = "render/lens-fields";
41
+ export declare const expireLensFields: import("@aethervtt/shard-core").SystemDef<undefined>;
package/dist/lens.js ADDED
@@ -0,0 +1,99 @@
1
+ import {
2
+ defineComponent,
3
+ defineResource,
4
+ defineSystem
5
+ } from "@aethervtt/shard-core";
6
+ import { FrameDemand, Time } from "@aethervtt/shard-runtime";
7
+ import { Camera3d } from "./camera.js";
8
+ const MAX_LENS_FIELDS = 4;
9
+ const LensFields = defineResource("render/LensFields", {
10
+ hostWritable: true,
11
+ description: "Screen-space fields effects publish (at most 4): center and radius in CSS pixels, strength, ms to live, source entity. Views with Lens bend their own pixels inside them (lensPlugin).",
12
+ init: () => ({ fields: [] })
13
+ });
14
+ const Lens = defineComponent(
15
+ "render/Lens",
16
+ {},
17
+ {
18
+ description: "Tag: this camera's view bends its own pixels inside the live LensFields, after the display stage. Pixels outside every field are left exactly as rendered.",
19
+ requires: [Camera3d]
20
+ }
21
+ );
22
+ const LensPath = defineResource("render/LensPath", {
23
+ description: "Present when lens fields (lensPlugin) are installed."
24
+ });
25
+ function publishLensField(world, field) {
26
+ const fields = world.initResource(LensFields).fields;
27
+ let i = 0;
28
+ while (i < fields.length && fields[i].source !== field.source) i++;
29
+ if (i === MAX_LENS_FIELDS) return false;
30
+ const f = fields[i];
31
+ if (f) {
32
+ f.screen[0] = field.screen[0];
33
+ f.screen[1] = field.screen[1];
34
+ f.radius = field.radius;
35
+ f.strength = field.strength;
36
+ f.ttlMs = field.ttlMs;
37
+ } else fields.push({ ...field, screen: [field.screen[0], field.screen[1]] });
38
+ world.touchResource(LensFields);
39
+ return true;
40
+ }
41
+ function clearLensFields(world, source) {
42
+ const value = world.tryResource(LensFields);
43
+ if (!value || value.fields.length === 0) return;
44
+ if (source === void 0) value.fields.length = 0;
45
+ else value.fields = value.fields.filter((f) => f.source !== source);
46
+ world.touchResource(LensFields);
47
+ }
48
+ const NO_OFFSET = [0, 0];
49
+ function forwardLensFields(from, to, offset = NO_OFFSET) {
50
+ const source = from.tryResource(LensFields)?.fields ?? [];
51
+ const out = to.initResource(LensFields).fields;
52
+ const n = Math.min(source.length, MAX_LENS_FIELDS);
53
+ for (let i = 0; i < n; i++) {
54
+ const s = source[i];
55
+ const d = out[i];
56
+ if (d) {
57
+ d.radius = s.radius;
58
+ d.strength = s.strength;
59
+ d.ttlMs = s.ttlMs;
60
+ d.source = s.source;
61
+ } else out.push({ ...s, screen: [0, 0] });
62
+ const f = out[i];
63
+ f.screen[0] = s.screen[0] - offset[0];
64
+ f.screen[1] = s.screen[1] - offset[1];
65
+ }
66
+ if (out.length === 0 && n === 0) return;
67
+ out.length = n;
68
+ to.touchResource(LensFields);
69
+ }
70
+ const LENS_DEMAND = "render/lens-fields";
71
+ const expireLensFields = defineSystem({
72
+ name: "render/expire-lens-fields",
73
+ description: "Counts LensFields down by the frame time and drops expired ones; holds frames while any live.",
74
+ run: (_, world) => {
75
+ const fields = world.tryResource(LensFields)?.fields;
76
+ let n = 0;
77
+ if (fields && fields.length > 0) {
78
+ const ms = world.resource(Time).delta * 1e3;
79
+ for (let i = 0; i < fields.length; i++) {
80
+ const f = fields[i];
81
+ f.ttlMs -= ms;
82
+ if (f.ttlMs > 0 && n < MAX_LENS_FIELDS) fields[n++] = f;
83
+ }
84
+ fields.length = n;
85
+ }
86
+ world.tryResource(FrameDemand)?.set(LENS_DEMAND, n > 0);
87
+ }
88
+ });
89
+ export {
90
+ LENS_DEMAND,
91
+ Lens,
92
+ LensFields,
93
+ LensPath,
94
+ MAX_LENS_FIELDS,
95
+ clearLensFields,
96
+ expireLensFields,
97
+ forwardLensFields,
98
+ publishLensField
99
+ };
package/dist/lights.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type Entity, type World } from '@aethervtt/shard-core';
1
+ import { type ComponentDef, type Entity, type World } from '@aethervtt/shard-core';
2
2
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
3
+ import { DataStore } from './data-store.js';
3
4
  /** Luminous power presets in lumens, for point and spot lights. */
4
5
  export declare const LuminousPowerPresets: {
5
6
  readonly candle: 12;
@@ -10,18 +11,20 @@ export declare const LuminousPowerPresets: {
10
11
  export type LuminousPowerPreset = keyof typeof LuminousPowerPresets;
11
12
  /** Lumens for a preset name or a number. */
12
13
  export declare const lumens: (value: LuminousPowerPreset | number) => number;
14
+ export declare const SHADOW_UPDATES: readonly ['always', 'on-change'];
13
15
  export declare const CascadeSettings: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
14
16
  count: import("@aethervtt/shard-core").FieldType<number, "u8">;
15
17
  maxDistance: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
18
  splitLambda: import("@aethervtt/shard-core").FieldType<number, "f32">;
17
19
  }>, "object">;
18
- export declare const DirectionalLight: import("@aethervtt/shard-core").ComponentDef<{
20
+ export declare const DirectionalLight: ComponentDef<{
19
21
  readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
20
22
  readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
21
23
  readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
22
24
  readonly shadowSoftness: import("@aethervtt/shard-core").FieldType<number, "f32">;
23
25
  readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
24
26
  readonly illuminance: import("@aethervtt/shard-core").FieldType<number, "f32">;
27
+ readonly shadowUpdate: import("@aethervtt/shard-core").FieldType<"always" | "on-change", "u8">;
25
28
  readonly cascades: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
26
29
  count: import("@aethervtt/shard-core").FieldType<number, "u8">;
27
30
  maxDistance: import("@aethervtt/shard-core").FieldType<number, "f32">;
@@ -29,7 +32,8 @@ export declare const DirectionalLight: import("@aethervtt/shard-core").Component
29
32
  }>, "object">;
30
33
  readonly angularDiameter: import("@aethervtt/shard-core").FieldType<number, "f32">;
31
34
  }>;
32
- export declare const PointLight: import("@aethervtt/shard-core").ComponentDef<{
35
+ export declare const FALLOFFS: readonly ['physical', 'tabletop'];
36
+ export declare const PointLight: ComponentDef<{
33
37
  readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
34
38
  readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
35
39
  readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
@@ -38,8 +42,17 @@ export declare const PointLight: import("@aethervtt/shard-core").ComponentDef<{
38
42
  readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
39
43
  readonly range: import("@aethervtt/shard-core").FieldType<number, "f32">;
40
44
  readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
45
+ readonly blockedByWalls: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
46
+ readonly shadowUpdate: import("@aethervtt/shard-core").FieldType<"always" | "on-change", "u8">;
47
+ readonly falloff: import("@aethervtt/shard-core").FieldType<"physical" | "tabletop", "u8">;
48
+ readonly bright: import("@aethervtt/shard-core").FieldType<number, "f32">;
41
49
  }>;
42
- export declare const SpotLight: import("@aethervtt/shard-core").ComponentDef<{
50
+ /**
51
+ * Tabletop falloff (0057): 1 inside `bright`, linear to 0 at `range`. Mirrors `light_falloff`
52
+ * in `shard::pbr::lights`.
53
+ */
54
+ export declare function tabletopFalloff(distance: number, bright: number, range: number): number;
55
+ export declare const SpotLight: ComponentDef<{
43
56
  readonly shadows: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
44
57
  readonly shadowBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
45
58
  readonly shadowNormalBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
@@ -48,6 +61,8 @@ export declare const SpotLight: import("@aethervtt/shard-core").ComponentDef<{
48
61
  readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
49
62
  readonly range: import("@aethervtt/shard-core").FieldType<number, "f32">;
50
63
  readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
64
+ readonly blockedByWalls: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
65
+ readonly shadowUpdate: import("@aethervtt/shard-core").FieldType<"always" | "on-change", "u8">;
51
66
  readonly innerAngle: import("@aethervtt/shard-core").FieldType<number, "f32">;
52
67
  readonly outerAngle: import("@aethervtt/shard-core").FieldType<number, "f32">;
53
68
  }>;
@@ -68,6 +83,11 @@ export interface LightingSettingsValue {
68
83
  shadowMapSize: number;
69
84
  maxShadowedPoints: number;
70
85
  maxShadowedSpots: number;
86
+ /**
87
+ * Baseline tier (0064): the point and spot lights a view shades, the nearest first; at most 128.
88
+ * Past it the farthest go unshaded, and `render/light-budget` says how many.
89
+ */
90
+ baselineMaxLights: number;
71
91
  }
72
92
  export declare const LightingSettings: import("@aethervtt/shard-core").ResourceDef<LightingSettingsValue>;
73
93
  /** Floats per light record in the light buffer. */
@@ -100,7 +120,21 @@ export interface LightRecord {
100
120
  outerAngle: number;
101
121
  radius: number;
102
122
  alive: boolean;
123
+ /** `shadowUpdate: 'on-change'`: the light's shadow maps are cached. */
124
+ cachedShadow: boolean;
125
+ /** Its wall-blocking row (0069), or -1: see `setLightRow`. */
126
+ row: number;
103
127
  }
128
+ /**
129
+ * Where a light's wall-blocking row (0069) rides in its record, as row + 1 (0: none): a word its
130
+ * kind never reads. A point light's spot scale (read only for spots) and a spot's tabletop bright
131
+ * radius (spots always fall off physically). The Light struct is unchanged, so shaders without
132
+ * `BLOCKED_LIGHTS` link the same code.
133
+ */
134
+ export declare const LIGHT_ROW_WORD: {
135
+ readonly point: 12;
136
+ readonly spot: 19;
137
+ };
104
138
  /**
105
139
  * Point and spot lights in a storage buffer (`array<Light>`). Each light owns a slot; only lights
106
140
  * whose data changed this frame are uploaded (coalesced runs), and the counters say how many.
@@ -118,9 +152,13 @@ export declare class LightStore {
118
152
  uploadedBytes: number;
119
153
  readonly buffer: GpuBuffer;
120
154
  /** Directional lights: count + up to 4 × (direction, shadow cascade flag, color × lux, disk radius). */
121
- readonly directional: GpuBuffer;
155
+ /** Directional lights: `@data(uniform)`, a uniform block on baseline. */
156
+ readonly directional: DataStore;
122
157
  readonly directionalData: Float32Array<ArrayBuffer>;
123
158
  readonly directionalU32: Uint32Array<ArrayBuffer>;
159
+ /** What the directional buffer holds, so an unchanged frame writes nothing (0055). */
160
+ private readonly directionalSent;
161
+ private directionalStale;
124
162
  /** The first directional light with shadows: entity, and its settings. */
125
163
  shadowSun: {
126
164
  entity: Entity;
@@ -131,6 +169,8 @@ export declare class LightStore {
131
169
  bias: number;
132
170
  normalBias: number;
133
171
  softness: number;
172
+ /** `shadowUpdate: 'on-change'`: cascades are cached. */
173
+ cached: boolean;
134
174
  } | null;
135
175
  directionalCount: number;
136
176
  private generation;
@@ -156,5 +196,10 @@ export declare const extractLights: import("@aethervtt/shard-core").SystemDef<{
156
196
  }>;
157
197
  /** Frees a light's slot when its component goes away (including despawn). */
158
198
  export declare function observeLightRemovals(world: World): void;
199
+ /**
200
+ * Gives a light its wall-blocking row (0069), or none with -1 (marks it dirty if it changed).
201
+ * Structure's interiorLightingPlugin calls it after extraction.
202
+ */
203
+ export declare function setLightRow(store: LightStore, r: LightRecord, row: number): void;
159
204
  /** Sets a light's shadow index (marks it dirty if it changed). */
160
205
  export declare function setShadowIndex(store: LightStore, r: LightRecord, index: number): void;