@aethervtt/shard-render 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. package/package.json +19 -11
@@ -9,7 +9,7 @@ import { Exposure } from "./camera.js";
9
9
  import { addRenderFeatures } from "./features.js";
10
10
  import { drawMaterials, ForwardStateResource, PASS_PREPASS, viewBindGroup } from "./forward.js";
11
11
  import { RenderPhase } from "./graph.js";
12
- import { Graph, Views } from "./plugin.js";
12
+ import { Gpu, Graph, Views } from "./plugin.js";
13
13
  import { AutoExposure, cocParams, hasEffect, needsPrepass, PostEffect } from "./post.js";
14
14
  import {
15
15
  beginPass,
@@ -18,8 +18,11 @@ import {
18
18
  HDR,
19
19
  idOf,
20
20
  PostCache,
21
+ postFormat,
22
+ roundsRg11,
21
23
  sampler,
22
24
  scratch,
25
+ targetsFor,
23
26
  tex,
24
27
  uniform
25
28
  } from "./post-common.js";
@@ -44,7 +47,16 @@ const prepassNode = {
44
47
  if (v.cam.deferred) drawMaterials(ctx, state, v.pv, v.cam, v.cam.forwardOnly, PASS_PREPASS);
45
48
  }
46
49
  };
47
- const FOG_KEYS = ["post/fog", "post/fog/alpha", "post/fog/interior", "post/fog/alpha/interior"];
50
+ const FOG_KEYS = [
51
+ "post/fog",
52
+ "post/fog/alpha",
53
+ "post/fog/interior",
54
+ "post/fog/alpha/interior",
55
+ "post/fog/rg11",
56
+ "post/fog/alpha/rg11",
57
+ "post/fog/interior/rg11",
58
+ "post/fog/alpha/interior/rg11"
59
+ ];
48
60
  const SSAO_STEPS = [
49
61
  [1, 4],
50
62
  [2, 4],
@@ -53,7 +65,7 @@ const SSAO_STEPS = [
53
65
  const invProj = mat4.create();
54
66
  function ssaoNode(phase, deferred) {
55
67
  const cache = new PostCache();
56
- let layouts;
68
+ let layout;
57
69
  let layoutGen = -1;
58
70
  return {
59
71
  kind: "raw",
@@ -63,41 +75,27 @@ function ssaoNode(phase, deferred) {
63
75
  return cam !== void 0 && cam.deferred === deferred && (cam.post.effects & PostEffect.Ssao) !== 0;
64
76
  },
65
77
  reads: ["ssao-normal", "ssao-depth"],
66
- writes: ["ssao", "ssao-half"],
78
+ writes: ["ssao"],
67
79
  run: (ctx) => {
68
80
  const v = forwardView(ctx);
69
81
  if (!v) return;
70
82
  const gpu = ctx.gpu;
71
- if (!layouts || layoutGen !== gpu.generation) {
83
+ if (!layout || layoutGen !== gpu.generation) {
72
84
  layoutGen = gpu.generation;
73
- layouts = {
74
- gtao: gpu.layouts.bindGroupLayout({
75
- label: "ssao/gtao",
76
- entries: [uniform(0), depthTex(gpu, 1), tex(2, "unfilterable-float"), uniform(3)]
77
- }),
78
- up: gpu.layouts.bindGroupLayout({
79
- label: "ssao/upsample",
80
- entries: [uniform(0), depthTex(gpu, 1), uniform(3), tex(4, "unfilterable-float")]
81
- })
82
- };
85
+ layout = gpu.layouts.bindGroupLayout({
86
+ label: "ssao/gtao",
87
+ entries: [uniform(0), depthTex(gpu, 1), tex(2, "unfilterable-float"), uniform(3)]
88
+ });
83
89
  }
84
90
  const gtao = cache.render(
85
91
  ctx,
86
92
  "ssao/gtao",
87
93
  "shard::post::ssao",
88
94
  "gtao",
89
- [layouts.gtao],
95
+ [layout],
90
96
  [{ format: "rg16float" }]
91
97
  );
92
- const up = cache.render(
93
- ctx,
94
- "ssao/upsample",
95
- "shard::post::ssao",
96
- "upsample",
97
- [layouts.up],
98
- [{ format: "r8unorm" }]
99
- );
100
- if (!gtao || !up) return;
98
+ if (!gtao) return;
101
99
  const { cam, pv } = v;
102
100
  const s = cam.post.ssao;
103
101
  const steps = SSAO_STEPS[s.quality] ?? SSAO_STEPS[1];
@@ -113,13 +111,12 @@ function ssaoNode(phase, deferred) {
113
111
  params.write(scratch, 0, 0, 24);
114
112
  const depth = ctx.texture("ssao-depth");
115
113
  const normal = ctx.texture("ssao-normal");
116
- const half = ctx.texture("ssao-half");
117
114
  const out = ctx.texture("ssao");
118
- const g1 = cache.group(
115
+ const group = cache.group(
119
116
  gpu,
120
117
  `${ctx.view.name}/gtao`,
121
118
  `${idOf(depth)}/${idOf(normal)}/${pv.uniform.version}/${params.version}`,
122
- layouts.gtao,
119
+ layout,
123
120
  () => [
124
121
  { binding: 0, resource: { buffer: pv.uniform.buffer } },
125
122
  { binding: 1, resource: depth.createView() },
@@ -127,26 +124,9 @@ function ssaoNode(phase, deferred) {
127
124
  { binding: 3, resource: { buffer: params.buffer } }
128
125
  ]
129
126
  );
130
- let pass = beginPass(ctx, "ssao", half.createView());
127
+ const pass = beginPass(ctx, "ssao", out.createView());
131
128
  pass.setPipeline(gtao);
132
- pass.setBindGroup(0, g1);
133
- pass.draw(3);
134
- pass.end();
135
- const g2 = cache.group(
136
- gpu,
137
- `${ctx.view.name}/ssao-up`,
138
- `${idOf(depth)}/${idOf(half)}/${pv.uniform.version}/${params.version}`,
139
- layouts.up,
140
- () => [
141
- { binding: 0, resource: { buffer: pv.uniform.buffer } },
142
- { binding: 1, resource: depth.createView() },
143
- { binding: 3, resource: { buffer: params.buffer } },
144
- { binding: 4, resource: half.createView() }
145
- ]
146
- );
147
- pass = beginPass(ctx, "ssao/upsample", out.createView());
148
- pass.setPipeline(up);
149
- pass.setBindGroup(0, g2);
129
+ pass.setBindGroup(0, group);
150
130
  pass.draw(3);
151
131
  pass.end();
152
132
  pv.ao = out;
@@ -160,7 +140,8 @@ function fogNode() {
160
140
  return {
161
141
  kind: "render",
162
142
  phase: RenderPhase.Post,
163
- enabled: hasEffect(PostEffect.Fog),
143
+ // With aerial perspective, the atmosphere's composite applies fog in its own pass.
144
+ enabled: (view) => ((cameraOf(view)?.post.effects ?? 0) & (PostEffect.Fog | PostEffect.Atmosphere)) === PostEffect.Fog,
164
145
  reads: ["fog-in", "depth", "environment", "clusters"],
165
146
  writes: ["fog-out"],
166
147
  color: [{ resource: "fog-out", clear: { r: 0, g: 0, b: 0, a: 1 } }],
@@ -177,14 +158,16 @@ function fogNode() {
177
158
  });
178
159
  }
179
160
  const alpha = v.cam.alphaOutput;
161
+ const out = ctx.texture("fog-out");
162
+ const rg11 = out.format === "rg11b10ufloat";
180
163
  const pipeline = cache.render(
181
164
  ctx,
182
- FOG_KEYS[(alpha ? 1 : 0) + (state.interiorMode !== 0 ? 2 : 0)],
165
+ FOG_KEYS[(alpha ? 1 : 0) + (state.interiorMode !== 0 ? 2 : 0) + (rg11 ? 4 : 0)],
183
166
  "shard::post::fog",
184
167
  "fs",
185
168
  [state.layouts.view, layout],
186
- HDR,
187
- { TRANSPARENT: alpha }
169
+ targetsFor(out),
170
+ { TRANSPARENT: alpha, RG11_ROUND: roundsRg11(gpu, out) }
188
171
  );
189
172
  if (!pipeline) return;
190
173
  const f = v.cam.post.fog;
@@ -254,12 +237,11 @@ function taaNode() {
254
237
  "shard::post::taa",
255
238
  "fs",
256
239
  [layout],
257
- [{ format: "rgba16float" }, { format: "rgba16float" }],
240
+ HDR,
258
241
  { TRANSPARENT: alpha }
259
242
  );
260
243
  if (!pipeline) return;
261
244
  const input = ctx.texture("taa-in");
262
- const out = ctx.texture("taa-out");
263
245
  let h = histories.get(ctx.view.name);
264
246
  if (!h || h.generation !== gpu.generation || h.textures[0].width !== input.width || h.textures[0].height !== input.height) {
265
247
  for (const t of h?.textures ?? []) t.destroy();
@@ -267,7 +249,7 @@ function taaNode() {
267
249
  label: `${ctx.view.name}/taa-history-${i}`,
268
250
  size: [input.width, input.height],
269
251
  format: "rgba16float",
270
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
252
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
271
253
  });
272
254
  h = { textures: [make(0), make(1)], index: 0, valid: false, generation: gpu.generation };
273
255
  histories.set(ctx.view.name, h);
@@ -301,7 +283,6 @@ function taaNode() {
301
283
  const pass = ctx.encoder.beginRenderPass({
302
284
  label: `${ctx.view.name}/taa`,
303
285
  colorAttachments: [
304
- { view: out.createView(), loadOp: "clear", storeOp: "store", clearValue: [0, 0, 0, 1] },
305
286
  { view: write.createView(), loadOp: "clear", storeOp: "store", clearValue: [0, 0, 0, 1] }
306
287
  ],
307
288
  timestampWrites: ctx.timestamps("post/taa")
@@ -310,6 +291,7 @@ function taaNode() {
310
291
  pass.setBindGroup(0, group);
311
292
  pass.draw(3);
312
293
  pass.end();
294
+ ctx.provide("taa-out", write);
313
295
  h.index = 1 - h.index;
314
296
  h.valid = true;
315
297
  }
@@ -344,13 +326,15 @@ function motionBlurNode() {
344
326
  ]
345
327
  });
346
328
  }
329
+ const out = ctx.texture("motion-blur-out");
347
330
  const pipeline = cache.render(
348
331
  ctx,
349
- "post/motion-blur",
332
+ out.format === "rg11b10ufloat" ? "post/motion-blur/rg11" : "post/motion-blur",
350
333
  "shard::post::motion_blur",
351
334
  "fs",
352
335
  [layout],
353
- HDR
336
+ targetsFor(out),
337
+ { RG11_ROUND: roundsRg11(gpu, out) }
354
338
  );
355
339
  if (!pipeline) return;
356
340
  const m = v.cam.post.motionBlur;
@@ -407,14 +391,24 @@ function dofNode() {
407
391
  });
408
392
  }
409
393
  const prepare = cache.render(ctx, "dof/prepare", "shard::post::dof", "prepare", [layout], HDR);
410
- const gather = cache.render(ctx, "dof/gather", "shard::post::dof", "gather", [layout], HDR);
394
+ const blur = ctx.texture("dof-blur");
395
+ const out = ctx.texture("dof-out");
396
+ const gather = cache.render(
397
+ ctx,
398
+ blur.format === "rg11b10ufloat" ? "dof/gather/rg11" : "dof/gather",
399
+ "shard::post::dof",
400
+ "gather",
401
+ [layout],
402
+ targetsFor(blur)
403
+ );
411
404
  const composite = cache.render(
412
405
  ctx,
413
- "dof/composite",
406
+ out.format === "rg11b10ufloat" ? "dof/composite/rg11" : "dof/composite",
414
407
  "shard::post::dof",
415
408
  "composite",
416
409
  [layout],
417
- HDR
410
+ targetsFor(out),
411
+ { RG11_ROUND: roundsRg11(gpu, out) }
418
412
  );
419
413
  if (!prepare || !gather || !composite) return;
420
414
  const params = cache.buffer(gpu, `${ctx.view.name}/dof`, 16);
@@ -422,8 +416,6 @@ function dofNode() {
422
416
  const input = ctx.texture("dof-in");
423
417
  const depth = ctx.texture("depth");
424
418
  const half = ctx.texture("dof-half");
425
- const blur = ctx.texture("dof-blur");
426
- const out = ctx.texture("dof-out");
427
419
  const group = (slot, secondary) => cache.group(
428
420
  gpu,
429
421
  `${ctx.view.name}/dof/${slot}`,
@@ -465,7 +457,8 @@ function bloomNode() {
465
457
  phase: RenderPhase.Post + 40,
466
458
  enabled: hasEffect(PostEffect.Bloom),
467
459
  reads: ["bloom-in"],
468
- writes: ["bloom-out", "bloom"],
460
+ // The tonemap composites level 0 over the image (and auto exposure meters the same mix).
461
+ writes: ["bloom"],
469
462
  run: (ctx) => {
470
463
  const v = forwardView(ctx);
471
464
  if (!v) return;
@@ -474,48 +467,40 @@ function bloomNode() {
474
467
  layoutGen = gpu.generation;
475
468
  layout = gpu.layouts.bindGroupLayout({
476
469
  label: "bloom",
477
- entries: [tex(0), sampler(1), uniform(2), tex(3, "unfilterable-float")]
470
+ entries: [tex(0), sampler(1), uniform(2)]
478
471
  });
479
472
  }
480
473
  const add = { srcFactor: "one", dstFactor: "one", operation: "add" };
474
+ const chain = ctx.texture("bloom");
475
+ const rg11 = chain.format === "rg11b10ufloat";
481
476
  const prefilter = cache.render(
482
477
  ctx,
483
- "bloom/prefilter",
478
+ rg11 ? "bloom/prefilter/rg11" : "bloom/prefilter",
484
479
  "shard::post::bloom",
485
480
  "prefilter",
486
481
  [layout],
487
- HDR
482
+ targetsFor(chain)
488
483
  );
489
484
  const down = cache.render(
490
485
  ctx,
491
- "bloom/downsample",
486
+ rg11 ? "bloom/downsample/rg11" : "bloom/downsample",
492
487
  "shard::post::bloom",
493
488
  "downsample",
494
489
  [layout],
495
- HDR
490
+ targetsFor(chain)
496
491
  );
497
492
  const up = cache.render(
498
493
  ctx,
499
- "bloom/upsample",
494
+ rg11 ? "bloom/upsample/rg11" : "bloom/upsample",
500
495
  "shard::post::bloom",
501
496
  "upsample",
502
497
  [layout],
503
- [{ format: "rgba16float", blend: { color: add, alpha: add } }]
504
- );
505
- const composite = cache.render(
506
- ctx,
507
- "bloom/composite",
508
- "shard::post::bloom",
509
- "composite",
510
- [layout],
511
- HDR
498
+ [{ format: chain.format, blend: { color: add, alpha: add } }]
512
499
  );
513
- if (!prefilter || !down || !up || !composite) return;
500
+ if (!prefilter || !down || !up) return;
514
501
  const { cam } = v;
515
502
  const b = cam.post.bloom;
516
503
  const input = ctx.texture("bloom-in");
517
- const chain = ctx.texture("bloom");
518
- const out = ctx.texture("bloom-out");
519
504
  const levels = Math.min(chain.mipLevelCount, bloomLevels(cam));
520
505
  const buffer = cache.buffer(gpu, `${ctx.view.name}/bloom`, 256 * 20);
521
506
  const slot = (i2, w, h) => {
@@ -534,7 +519,6 @@ function bloomNode() {
534
519
  slot(n++, input.width, input.height);
535
520
  for (let k = 1; k < levels; k++) slot(n++, ...mipSize(k - 1));
536
521
  for (let k = levels - 2; k >= 0; k--) slot(n++, ...mipSize(k + 1));
537
- slot(n++, chain.width, chain.height);
538
522
  buffer.write(params, 0, 0, n * 64);
539
523
  const view = (k) => chain.createView({ baseMipLevel: k, mipLevelCount: 1 });
540
524
  const group = (i2, source, mip) => cache.group(
@@ -545,8 +529,7 @@ function bloomNode() {
545
529
  () => [
546
530
  { binding: 0, resource: source === chain ? view(mip) : source.createView() },
547
531
  { binding: 1, resource: cache.sampler(gpu) },
548
- { binding: 2, resource: { buffer: buffer.buffer, offset: i2 * 256, size: 32 } },
549
- { binding: 3, resource: input.createView() }
532
+ { binding: 2, resource: { buffer: buffer.buffer, offset: i2 * 256, size: 32 } }
550
533
  ]
551
534
  );
552
535
  let i = 0;
@@ -560,10 +543,31 @@ function bloomNode() {
560
543
  draw("post/bloom", prefilter, view(0), input, 0);
561
544
  for (let k = 1; k < levels; k++) draw("post/bloom-down", down, view(k), chain, k - 1);
562
545
  for (let k = levels - 2; k >= 0; k--) draw("post/bloom-up", up, view(k), chain, k + 1, true);
563
- draw("post/bloom-composite", composite, out.createView(), chain, 0);
564
546
  }
565
547
  };
566
548
  }
549
+ function bloomGlow(cam, chain, out, at) {
550
+ const b = cam.post.bloom;
551
+ out[at] = chain ? b.intensity : 0;
552
+ out[at + 1] = chain ? chain.mipLevelCount : 1;
553
+ out[at + 2] = chain && b.threshold <= 0 ? 1 : 0;
554
+ out[at + 3] = 0;
555
+ }
556
+ const whites = /* @__PURE__ */ new WeakMap();
557
+ function whiteTexture(gpu) {
558
+ let t = whites.get(gpu.device);
559
+ if (!t) {
560
+ t = gpu.device.createTexture({
561
+ label: "post/white",
562
+ size: [1, 1],
563
+ format: "rgba8unorm",
564
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
565
+ });
566
+ gpu.device.queue.writeTexture({ texture: t }, new Uint8Array([255, 255, 255, 255]), {}, [1, 1]);
567
+ whites.set(gpu.device, t);
568
+ }
569
+ return t;
570
+ }
567
571
  const METER_READBACKS = 3;
568
572
  const ExposureMeters = defineResource("render/ExposureMeters", {
569
573
  description: "Auto exposure per camera: the last metered EV100 and the adapted one.",
@@ -613,21 +617,24 @@ function autoExposureNode(baseline) {
613
617
  kind: "raw",
614
618
  phase: RenderPhase.Post + 50,
615
619
  enabled: hasEffect(PostEffect.AutoExposure),
616
- reads: ["post-hdr"],
620
+ reads: ["post-hdr", "bloom"],
617
621
  sideEffects: true,
618
622
  run: (ctx) => {
619
623
  const v = forwardView(ctx);
620
624
  if (!v) return;
621
625
  const gpu = ctx.gpu;
626
+ const bloom = (v.cam.post.effects & PostEffect.Bloom) !== 0;
627
+ const chain = bloom ? ctx.texture("bloom") : void 0;
622
628
  if (baseline) {
623
629
  const layout2 = baseline.meterLayout(gpu);
624
630
  const pipeline2 = cache.render(
625
631
  ctx,
626
- "post/exposure-meter",
632
+ bloom ? "post/exposure-meter/bloom" : "post/exposure-meter",
627
633
  "shard::post::baseline::meter",
628
634
  "main",
629
635
  [layout2],
630
- [{ format: "rg16float" }]
636
+ [{ format: "rg16float" }],
637
+ { BLOOM: bloom }
631
638
  );
632
639
  if (!pipeline2) return;
633
640
  let m = meterViews.get(ctx.view.name);
@@ -636,7 +643,8 @@ function autoExposureNode(baseline) {
636
643
  meterViews.set(ctx.view.name, m);
637
644
  }
638
645
  const input2 = ctx.texture("post-hdr");
639
- const key = `${idOf(input2)}/${v.pv.uniform.version}`;
646
+ const level02 = chain ?? whiteTexture(gpu);
647
+ const key = `${idOf(input2)}/${v.pv.uniform.version}/${idOf(level02)}`;
640
648
  if (!m.group || m.groupKey !== key) {
641
649
  m.groupKey = key;
642
650
  m.group = gpu.device.createBindGroup({
@@ -645,7 +653,9 @@ function autoExposureNode(baseline) {
645
653
  entries: [
646
654
  { binding: 0, resource: { buffer: v.pv.uniform.buffer } },
647
655
  { binding: 1, resource: input2.createView() },
648
- { binding: 2, resource: { buffer: m.params } }
656
+ { binding: 2, resource: { buffer: m.params } },
657
+ { binding: 3, resource: level02.createView({ mipLevelCount: 1 }) },
658
+ { binding: 4, resource: cache.sampler(gpu) }
649
659
  ]
650
660
  });
651
661
  }
@@ -653,7 +663,15 @@ function autoExposureNode(baseline) {
653
663
  scratch[1] = METER_BINS_PER_EV;
654
664
  scratch[2] = v.cam.post.exposure.metering;
655
665
  scratch[3] = 0;
656
- const k2 = baseline.renderMeter(ctx, m, pipeline2, m.group, scratch.subarray(0, 4));
666
+ bloomGlow(v.cam, chain, scratch, 4);
667
+ const k2 = baseline.renderMeter(
668
+ ctx,
669
+ m,
670
+ pipeline2,
671
+ m.group,
672
+ scratch.subarray(0, 4),
673
+ scratch.subarray(4, 8)
674
+ );
657
675
  if (k2 === void 0) return;
658
676
  const meters2 = ctx.world.resource(ExposureMeters);
659
677
  const entity2 = v.cam.entity;
@@ -685,14 +703,21 @@ function autoExposureNode(baseline) {
685
703
  uniform(0, C),
686
704
  { binding: 1, visibility: C, texture: { sampleType: "unfilterable-float" } },
687
705
  { binding: 2, visibility: C, buffer: { type: "storage" } },
688
- uniform(3, C)
706
+ uniform(3, C),
707
+ { binding: 4, visibility: C, texture: { sampleType: "float" } },
708
+ { binding: 5, visibility: C, sampler: { type: "filtering" } }
689
709
  ]
690
710
  });
691
711
  perView.clear();
692
712
  }
693
- const pipeline = cache.compute(ctx, "post/exposure", "shard::post::exposure", "meter", [
694
- layout
695
- ]);
713
+ const pipeline = cache.compute(
714
+ ctx,
715
+ bloom ? "post/exposure/bloom" : "post/exposure",
716
+ "shard::post::exposure",
717
+ "meter",
718
+ [layout],
719
+ { BLOOM: bloom }
720
+ );
696
721
  if (!pipeline) return;
697
722
  let state = perView.get(ctx.view.name);
698
723
  if (!state || state.generation !== gpu.generation) {
@@ -720,20 +745,24 @@ function autoExposureNode(baseline) {
720
745
  scratch[1] = METER_BINS_PER_EV;
721
746
  scratch[2] = e.metering;
722
747
  scratch[3] = 0;
723
- const params = cache.buffer(gpu, `${ctx.view.name}/exposure`, 16);
724
- params.write(scratch, 0, 0, 4);
748
+ bloomGlow(v.cam, chain, scratch, 4);
749
+ const params = cache.buffer(gpu, `${ctx.view.name}/exposure`, 32);
750
+ params.write(scratch, 0, 0, 8);
725
751
  const input = ctx.texture("post-hdr");
726
752
  const histogram = state.histogram;
753
+ const level0 = chain ?? whiteTexture(gpu);
727
754
  const group = cache.group(
728
755
  gpu,
729
756
  `${ctx.view.name}/exposure`,
730
- `${idOf(input)}/${idOf(histogram)}/${v.pv.uniform.version}/${params.version}`,
757
+ `${idOf(input)}/${idOf(histogram)}/${v.pv.uniform.version}/${params.version}/${idOf(level0)}`,
731
758
  layout,
732
759
  () => [
733
760
  { binding: 0, resource: { buffer: v.pv.uniform.buffer } },
734
761
  { binding: 1, resource: input.createView() },
735
762
  { binding: 2, resource: { buffer: histogram } },
736
- { binding: 3, resource: { buffer: params.buffer } }
763
+ { binding: 3, resource: { buffer: params.buffer } },
764
+ { binding: 4, resource: level0.createView({ mipLevelCount: 1 }) },
765
+ { binding: 5, resource: cache.sampler(gpu) }
737
766
  ]
738
767
  );
739
768
  gpu.device.queue.writeBuffer(histogram, 0, zero);
@@ -825,13 +854,15 @@ const halfSize = { divide: 2 };
825
854
  function addPostNodes(world, baseline) {
826
855
  const graph = world.resource(Graph);
827
856
  world.initResource(ExposureMeters);
828
- graph.declare({ name: "prepass-normal", format: "rgba16float" });
857
+ graph.declare({ name: "prepass-normal", format: "rg16float" });
829
858
  graph.declare({ name: "velocity", format: "rg16float" });
830
859
  graph.declare({ name: "prepass-depth", format: "depth32float" });
831
- graph.declare({ name: "ssao", format: "r8unorm" });
832
- graph.declare({ name: "ssao-half", format: "rg16float", size: halfSize });
833
- graph.declare({ name: "post-a", format: "rgba16float" });
834
- graph.declare({ name: "post-b", format: "rgba16float" });
860
+ graph.declare({ name: "ssao", format: "rg16float", size: halfSize });
861
+ const gpu = world.resource(Gpu);
862
+ const chainFormat = (view) => postFormat(gpu, view);
863
+ graph.declare({ name: "post-a", format: chainFormat });
864
+ graph.declare({ name: "post-b", format: chainFormat });
865
+ graph.declare({ name: "taa-history", format: "rgba16float" });
835
866
  graph.declare({ name: "dof-half", format: "rgba16float", size: halfSize });
836
867
  graph.declare({ name: "dof-blur", format: "rgba16float", size: halfSize });
837
868
  graph.declare({
@@ -935,6 +966,7 @@ export {
935
966
  POST_NODES,
936
967
  adaptExposure,
937
968
  addPostNodes,
969
+ bloomGlow,
938
970
  bloomLevels,
939
971
  describePost,
940
972
  histogramEv
@@ -0,0 +1,130 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ /** Each post effect's component and settings, by the name `?effects=` uses. */
3
+ export declare const POST_EFFECTS: {
4
+ readonly bloom: readonly [import("@aethervtt/shard-core").ComponentDef<{
5
+ readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
6
+ readonly threshold: import("@aethervtt/shard-core").FieldType<number, "f32">;
7
+ readonly knee: import("@aethervtt/shard-core").FieldType<number, "f32">;
8
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
9
+ }>, {
10
+ readonly intensity: 0.12;
11
+ }];
12
+ readonly exposure: readonly [import("@aethervtt/shard-core").ComponentDef<{
13
+ readonly minEv: import("@aethervtt/shard-core").FieldType<number, "f32">;
14
+ readonly maxEv: import("@aethervtt/shard-core").FieldType<number, "f32">;
15
+ readonly compensation: import("@aethervtt/shard-core").FieldType<number, "f32">;
16
+ readonly speedUp: import("@aethervtt/shard-core").FieldType<number, "f32">;
17
+ readonly speedDown: import("@aethervtt/shard-core").FieldType<number, "f32">;
18
+ readonly metering: import("@aethervtt/shard-core").FieldType<"average" | "center" | "spot", "u8">;
19
+ }>, {
20
+ readonly compensation: 0.5;
21
+ }];
22
+ readonly dof: readonly [import("@aethervtt/shard-core").ComponentDef<{
23
+ readonly mode: import("@aethervtt/shard-core").FieldType<"bokeh" | "gaussian", "u8">;
24
+ readonly focusDistance: import("@aethervtt/shard-core").FieldType<number, "f32">;
25
+ readonly maxBlur: import("@aethervtt/shard-core").FieldType<number, "f32">;
26
+ }>, {
27
+ readonly focusDistance: 9;
28
+ readonly maxBlur: 0.012;
29
+ }];
30
+ readonly 'motion-blur': readonly [import("@aethervtt/shard-core").ComponentDef<{
31
+ readonly samples: import("@aethervtt/shard-core").FieldType<number, "u32">;
32
+ readonly maxBlur: import("@aethervtt/shard-core").FieldType<number, "f32">;
33
+ }>, {}];
34
+ readonly taa: readonly [import("@aethervtt/shard-core").ComponentDef<{
35
+ readonly mode: import("@aethervtt/shard-core").FieldType<"fxaa" | "msaa" | "none" | "taa", "u8">;
36
+ }>, {
37
+ readonly mode: 'taa';
38
+ }];
39
+ readonly fxaa: readonly [import("@aethervtt/shard-core").ComponentDef<{
40
+ readonly mode: import("@aethervtt/shard-core").FieldType<"fxaa" | "msaa" | "none" | "taa", "u8">;
41
+ }>, {
42
+ readonly mode: 'fxaa';
43
+ }];
44
+ readonly noaa: readonly [import("@aethervtt/shard-core").ComponentDef<{
45
+ readonly mode: import("@aethervtt/shard-core").FieldType<"fxaa" | "msaa" | "none" | "taa", "u8">;
46
+ }>, {
47
+ readonly mode: 'none';
48
+ }];
49
+ readonly ssao: readonly [import("@aethervtt/shard-core").ComponentDef<{
50
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32">;
51
+ readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
52
+ readonly quality: import("@aethervtt/shard-core").FieldType<"high" | "low" | "medium", "u8">;
53
+ }>, {
54
+ readonly radius: 0.6;
55
+ }];
56
+ readonly fog: readonly [import("@aethervtt/shard-core").ComponentDef<{
57
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
58
+ readonly density: import("@aethervtt/shard-core").FieldType<number, "f32">;
59
+ readonly heightFalloff: import("@aethervtt/shard-core").FieldType<number, "f32">;
60
+ readonly start: import("@aethervtt/shard-core").FieldType<number, "f32">;
61
+ readonly sunScattering: import("@aethervtt/shard-core").FieldType<number, "f32">;
62
+ readonly mode: import("@aethervtt/shard-core").FieldType<"add" | "default", "u8">;
63
+ }>, {
64
+ readonly density: 0.015;
65
+ readonly heightFalloff: 0.08;
66
+ readonly sunScattering: 0.6;
67
+ }];
68
+ readonly grading: readonly [import("@aethervtt/shard-core").ComponentDef<{
69
+ readonly temperature: import("@aethervtt/shard-core").FieldType<number, "f32">;
70
+ readonly tint: import("@aethervtt/shard-core").FieldType<number, "f32">;
71
+ readonly saturation: import("@aethervtt/shard-core").FieldType<number, "f32">;
72
+ readonly contrast: import("@aethervtt/shard-core").FieldType<number, "f32">;
73
+ readonly lift: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
74
+ readonly gamma: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
75
+ readonly gain: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec3, "f32">;
76
+ readonly lut: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
77
+ }>, {
78
+ readonly temperature: 0.1;
79
+ readonly saturation: 1.1;
80
+ readonly contrast: 1.05;
81
+ }];
82
+ readonly vignette: readonly [import("@aethervtt/shard-core").ComponentDef<{
83
+ readonly intensity: import("@aethervtt/shard-core").FieldType<number, "f32">;
84
+ readonly smoothness: import("@aethervtt/shard-core").FieldType<number, "f32">;
85
+ }>, {
86
+ readonly intensity: 0.25;
87
+ }];
88
+ };
89
+ export type PostEffectName = keyof typeof POST_EFFECTS;
90
+ /** The full stack: every effect but the other anti-aliasing modes (TAA is on). */
91
+ export declare const ALL_POST_EFFECTS: readonly PostEffectName[];
92
+ /** The components for effect names; unknown names are skipped. */
93
+ export declare function postEffects(names: readonly string[]): unknown[];
94
+ /** A ball on a circle round the courtyard. */
95
+ export declare const PostSpin: import("@aethervtt/shard-core").ComponentDef<{
96
+ readonly radius: import("@aethervtt/shard-core").FieldType<number, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<number> | undefined) => import("@aethervtt/shard-core").FieldType<number, "f32">);
97
+ readonly speed: import("@aethervtt/shard-core").FieldType<number, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<number> | undefined) => import("@aethervtt/shard-core").FieldType<number, "f32">);
98
+ readonly height: import("@aethervtt/shard-core").FieldType<number, "f32"> & ((options?: import("@aethervtt/shard-core").FieldOptions<number> | undefined) => import("@aethervtt/shard-core").FieldType<number, "f32">);
99
+ }>;
100
+ /** Moves PostSpin balls along their circles by `Time.elapsed`. */
101
+ export declare const postSpin: import("@aethervtt/shard-core").SystemDef<{
102
+ q: import("@aethervtt/shard-core").Query;
103
+ }>;
104
+ /** The courtyard's moving parts: PostSpin and its system. Add it with the render plugins. */
105
+ export declare const postScenePlugin: import("@aethervtt/shard-runtime").Plugin;
106
+ /** Where the courtyard is looked at from: 9 m out, 2.2 m up. */
107
+ export declare const POST_EYE: [number, number, number];
108
+ export declare const POST_TARGET: [number, number, number];
109
+ /**
110
+ * The `post-stack` camera path: from POST_EYE, swinging 30° either side of the courtyard's axis
111
+ * once every 20 s and drifting 1.5 m in and out, always looking at POST_TARGET. `seconds` along it.
112
+ */
113
+ export declare function postCamera(seconds: number): {
114
+ eye: [number, number, number];
115
+ target: [number, number, number];
116
+ };
117
+ /** The camera's lens: a physical exposure, so auto exposure and DoF read real values. */
118
+ export declare const POST_LENS: readonly [import("@aethervtt/shard-core").ComponentDef<{
119
+ readonly aperture: import("@aethervtt/shard-core").FieldType<number, "f32">;
120
+ readonly shutterSpeed: import("@aethervtt/shard-core").FieldType<number, "f32">;
121
+ readonly iso: import("@aethervtt/shard-core").FieldType<number, "f32">;
122
+ readonly sensorHeight: import("@aethervtt/shard-core").FieldType<number, "f32">;
123
+ readonly focalLength: import("@aethervtt/shard-core").FieldType<number, "f32">;
124
+ }>, {
125
+ readonly aperture: 5.6;
126
+ readonly shutterSpeed: number;
127
+ readonly iso: 100;
128
+ }];
129
+ /** The courtyard, its sun and sky; the camera is the caller's. */
130
+ export declare function spawnPostScene(world: World): void;