@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
@@ -32,7 +32,8 @@ export declare const adaptExposure: import("@aethervtt/shard-core").SystemDef<{
32
32
  /** Graph node names of the post chain, in order (for describe and tests). */
33
33
  export declare const POST_NODES: readonly [readonly ['post/fog', 1], readonly ['post/taa', 2], readonly ['post/motion-blur', 4], readonly ['post/dof', 8], readonly ['post/bloom', 16], readonly ['post/exposure', 32], readonly ['post/fxaa', 64]];
34
34
  /** Adds the prepass, SSAO, and the post chain to the graph. */
35
- export declare function addPostNodes(world: World): void;
35
+ /** The post chain's nodes. `baseline`: the baseline tier's exposure meter (0064), when on one. */
36
+ export declare function addPostNodes(world: World, baseline?: typeof import('./baseline/exposure.js')): void;
36
37
  /** The post section of `render.describe`: per view, active effects in order with GPU time. */
37
38
  export declare function describePost(world: World): {
38
39
  views: Record<string, unknown>;
@@ -6,12 +6,14 @@ import {
6
6
  } from "@aethervtt/shard-core";
7
7
  import { Time } from "@aethervtt/shard-runtime";
8
8
  import { Exposure } from "./camera.js";
9
+ import { addRenderFeatures } from "./features.js";
9
10
  import { drawMaterials, ForwardStateResource, PASS_PREPASS, viewBindGroup } from "./forward.js";
10
11
  import { RenderPhase } from "./graph.js";
11
12
  import { Graph, Views } from "./plugin.js";
12
13
  import { AutoExposure, cocParams, hasEffect, needsPrepass, PostEffect } from "./post.js";
13
14
  import {
14
15
  beginPass,
16
+ depthTex,
15
17
  forwardView,
16
18
  HDR,
17
19
  idOf,
@@ -42,6 +44,7 @@ const prepassNode = {
42
44
  if (v.cam.deferred) drawMaterials(ctx, state, v.pv, v.cam, v.cam.forwardOnly, PASS_PREPASS);
43
45
  }
44
46
  };
47
+ const FOG_KEYS = ["post/fog", "post/fog/alpha", "post/fog/interior", "post/fog/alpha/interior"];
45
48
  const SSAO_STEPS = [
46
49
  [1, 4],
47
50
  [2, 4],
@@ -70,11 +73,11 @@ function ssaoNode(phase, deferred) {
70
73
  layouts = {
71
74
  gtao: gpu.layouts.bindGroupLayout({
72
75
  label: "ssao/gtao",
73
- entries: [uniform(0), tex(1, "depth"), tex(2, "unfilterable-float"), uniform(3)]
76
+ entries: [uniform(0), depthTex(gpu, 1), tex(2, "unfilterable-float"), uniform(3)]
74
77
  }),
75
78
  up: gpu.layouts.bindGroupLayout({
76
79
  label: "ssao/upsample",
77
- entries: [uniform(0), tex(1, "depth"), uniform(3), tex(4, "unfilterable-float")]
80
+ entries: [uniform(0), depthTex(gpu, 1), uniform(3), tex(4, "unfilterable-float")]
78
81
  })
79
82
  };
80
83
  }
@@ -170,16 +173,18 @@ function fogNode() {
170
173
  layoutGen = gpu.generation;
171
174
  layout = gpu.layouts.bindGroupLayout({
172
175
  label: "fog",
173
- entries: [tex(0, "unfilterable-float"), tex(1, "depth"), uniform(2)]
176
+ entries: [tex(0, "unfilterable-float"), depthTex(gpu, 1), uniform(2)]
174
177
  });
175
178
  }
179
+ const alpha = v.cam.alphaOutput;
176
180
  const pipeline = cache.render(
177
181
  ctx,
178
- "post/fog",
182
+ FOG_KEYS[(alpha ? 1 : 0) + (state.interiorMode !== 0 ? 2 : 0)],
179
183
  "shard::post::fog",
180
184
  "fs",
181
185
  [state.layouts.view, layout],
182
- HDR
186
+ HDR,
187
+ { TRANSPARENT: alpha }
183
188
  );
184
189
  if (!pipeline) return;
185
190
  const f = v.cam.post.fog;
@@ -233,7 +238,7 @@ function taaNode() {
233
238
  entries: [
234
239
  uniform(0),
235
240
  tex(1, "unfilterable-float"),
236
- tex(2, "depth"),
241
+ depthTex(gpu, 2),
237
242
  tex(3, "unfilterable-float"),
238
243
  tex(4),
239
244
  sampler(5),
@@ -242,13 +247,15 @@ function taaNode() {
242
247
  });
243
248
  histories.clear();
244
249
  }
250
+ const alpha = cameraOf(ctx.view)?.alphaOutput === true;
245
251
  const pipeline = cache.render(
246
252
  ctx,
247
- "post/taa",
253
+ alpha ? "post/taa/alpha" : "post/taa",
248
254
  "shard::post::taa",
249
255
  "fs",
250
256
  [layout],
251
- [{ format: "rgba16float" }, { format: "rgba16float" }]
257
+ [{ format: "rgba16float" }, { format: "rgba16float" }],
258
+ { TRANSPARENT: alpha }
252
259
  );
253
260
  if (!pipeline) return;
254
261
  const input = ctx.texture("taa-in");
@@ -330,7 +337,7 @@ function motionBlurNode() {
330
337
  entries: [
331
338
  uniform(0),
332
339
  tex(1),
333
- tex(2, "depth"),
340
+ depthTex(gpu, 2),
334
341
  tex(3, "unfilterable-float"),
335
342
  uniform(4),
336
343
  sampler(5)
@@ -396,7 +403,7 @@ function dofNode() {
396
403
  layoutGen = gpu.generation;
397
404
  layout = gpu.layouts.bindGroupLayout({
398
405
  label: "dof",
399
- entries: [uniform(0), tex(1), tex(2, "depth"), uniform(3), tex(4), sampler(5)]
406
+ entries: [uniform(0), tex(1), depthTex(gpu, 2), uniform(3), tex(4), sampler(5)]
400
407
  });
401
408
  }
402
409
  const prepare = cache.render(ctx, "dof/prepare", "shard::post::dof", "prepare", [layout], HDR);
@@ -586,12 +593,22 @@ function histogramEv(bins) {
586
593
  }
587
594
  return weight > 0 ? sum / weight : void 0;
588
595
  }
589
- function autoExposureNode() {
596
+ function landReading(meters, entity, index, ev) {
597
+ const m = meters.get(entity);
598
+ if (m && ev !== void 0 && index > m.reading) {
599
+ m.metered = ev;
600
+ m.reading = index;
601
+ m.readings++;
602
+ }
603
+ }
604
+ function autoExposureNode(baseline) {
590
605
  const cache = new PostCache();
591
606
  let layout;
592
607
  let layoutGen = -1;
593
608
  const perView = /* @__PURE__ */ new Map();
609
+ const meterViews = /* @__PURE__ */ new Map();
594
610
  const zero = new Uint32Array(256);
611
+ const bins = new Uint32Array(256);
595
612
  return {
596
613
  kind: "raw",
597
614
  phase: RenderPhase.Post + 50,
@@ -602,6 +619,63 @@ function autoExposureNode() {
602
619
  const v = forwardView(ctx);
603
620
  if (!v) return;
604
621
  const gpu = ctx.gpu;
622
+ if (baseline) {
623
+ const layout2 = baseline.meterLayout(gpu);
624
+ const pipeline2 = cache.render(
625
+ ctx,
626
+ "post/exposure-meter",
627
+ "shard::post::baseline::meter",
628
+ "main",
629
+ [layout2],
630
+ [{ format: "rg16float" }]
631
+ );
632
+ if (!pipeline2) return;
633
+ let m = meterViews.get(ctx.view.name);
634
+ if (!m || m.generation !== gpu.generation) {
635
+ m = baseline.meterView(gpu, ctx.view.name, METER_READBACKS);
636
+ meterViews.set(ctx.view.name, m);
637
+ }
638
+ const input2 = ctx.texture("post-hdr");
639
+ const key = `${idOf(input2)}/${v.pv.uniform.version}`;
640
+ if (!m.group || m.groupKey !== key) {
641
+ m.groupKey = key;
642
+ m.group = gpu.device.createBindGroup({
643
+ label: `${ctx.view.name}/exposure`,
644
+ layout: layout2,
645
+ entries: [
646
+ { binding: 0, resource: { buffer: v.pv.uniform.buffer } },
647
+ { binding: 1, resource: input2.createView() },
648
+ { binding: 2, resource: { buffer: m.params } }
649
+ ]
650
+ });
651
+ }
652
+ scratch[0] = METER_MIN_EV;
653
+ scratch[1] = METER_BINS_PER_EV;
654
+ scratch[2] = v.cam.post.exposure.metering;
655
+ scratch[3] = 0;
656
+ const k2 = baseline.renderMeter(ctx, m, pipeline2, m.group, scratch.subarray(0, 4));
657
+ if (k2 === void 0) return;
658
+ const meters2 = ctx.world.resource(ExposureMeters);
659
+ const entity2 = v.cam.entity;
660
+ const meter2 = meters2.get(entity2);
661
+ const index2 = meter2 ? ++meter2.submitted : 0;
662
+ const readback2 = m.readbacks[k2];
663
+ const busy2 = m.busy;
664
+ ctx.afterSubmit(() => {
665
+ readback2.mapAsync(GPUMapMode.READ).then(
666
+ () => {
667
+ baseline.binMeter(readback2.getMappedRange(), METER_MIN_EV, METER_BINS_PER_EV, bins);
668
+ readback2.unmap();
669
+ busy2[k2] = false;
670
+ landReading(meters2, entity2, index2, histogramEv(bins));
671
+ },
672
+ () => {
673
+ busy2[k2] = false;
674
+ }
675
+ );
676
+ });
677
+ return;
678
+ }
605
679
  const C = GPUShaderStage.COMPUTE;
606
680
  if (!layout || layoutGen !== gpu.generation) {
607
681
  layoutGen = gpu.generation;
@@ -687,12 +761,7 @@ function autoExposureNode() {
687
761
  const ev = histogramEv(new Uint32Array(readback.getMappedRange().slice(0)));
688
762
  readback.unmap();
689
763
  busy[k] = false;
690
- const m = meters.get(entity);
691
- if (m && ev !== void 0 && index > m.reading) {
692
- m.metered = ev;
693
- m.reading = index;
694
- m.readings++;
695
- }
764
+ landReading(meters, entity, index, ev);
696
765
  },
697
766
  () => {
698
767
  busy[k] = false;
@@ -753,7 +822,7 @@ const POST_NODES = [
753
822
  ["post/fxaa", PostEffect.Fxaa]
754
823
  ];
755
824
  const halfSize = { divide: 2 };
756
- function addPostNodes(world) {
825
+ function addPostNodes(world, baseline) {
757
826
  const graph = world.resource(Graph);
758
827
  world.initResource(ExposureMeters);
759
828
  graph.declare({ name: "prepass-normal", format: "rgba16float" });
@@ -774,6 +843,29 @@ function addPostNodes(world) {
774
843
  return cam ? bloomLevels(cam) : 1;
775
844
  }
776
845
  });
846
+ addRenderFeatures(world, {
847
+ name: "render/post",
848
+ description: "The prepass, SSAO, fog, TAA, motion blur, depth of field and bloom.",
849
+ nodes: [
850
+ "prepass",
851
+ "ssao",
852
+ "ssao-deferred",
853
+ "post/fog",
854
+ "post/taa",
855
+ "post/motion-blur",
856
+ "post/dof",
857
+ "post/bloom"
858
+ ],
859
+ baseline: { strategy: "The same fragment passes" }
860
+ });
861
+ addRenderFeatures(world, {
862
+ name: "render/auto-exposure",
863
+ description: "Automatic exposure from a luminance histogram.",
864
+ nodes: ["post/exposure"],
865
+ baseline: {
866
+ strategy: "A 64\xD736 meter image read back and binned into the same histogram on the CPU"
867
+ }
868
+ });
777
869
  graph.addNode("prepass", prepassNode);
778
870
  graph.addNode("ssao", ssaoNode(RenderPhase.Prepass + 50, false));
779
871
  graph.addNode("ssao-deferred", ssaoNode(RenderPhase.Opaque + 20, true));
@@ -782,7 +874,7 @@ function addPostNodes(world) {
782
874
  graph.addNode("post/motion-blur", motionBlurNode());
783
875
  graph.addNode("post/dof", dofNode());
784
876
  graph.addNode("post/bloom", bloomNode());
785
- graph.addNode("post/exposure", autoExposureNode());
877
+ graph.addNode("post/exposure", autoExposureNode(baseline));
786
878
  }
787
879
  const DESCRIBED = [
788
880
  ["ssao", "ssao", PostEffect.Ssao],
@@ -1,7 +1,7 @@
1
1
  import { PostUpdate } from "@aethervtt/shard-core";
2
2
  import { definePlugin } from "@aethervtt/shard-runtime";
3
3
  import { applyPhysicalCameras } from "./camera.js";
4
- import { RenderDescribers, Shaders } from "./plugin.js";
4
+ import { Gpu, RenderDescribers, Shaders } from "./plugin.js";
5
5
  import { PostEffect, PostFeatures } from "./post.js";
6
6
  import { adaptExposure, addPostNodes, describePost, ExposureMeters } from "./post-nodes.js";
7
7
  import { POST_SHADERS } from "./post-shaders.js";
@@ -15,10 +15,12 @@ const postPlugin = definePlugin({
15
15
  app.world.initResource(PostFeatures).effects |= POST_EFFECTS;
16
16
  app.addSystems(PostUpdate, adaptExposure.after(applyPhysicalCameras));
17
17
  },
18
- ready(app) {
18
+ async ready(app) {
19
+ const baseline = app.world.resource(Gpu).tier === "baseline" ? await import("./baseline/exposure.js") : void 0;
19
20
  registerShaders(app.world.resource(Shaders), POST_SHADERS);
21
+ if (baseline) registerShaders(app.world.resource(Shaders), baseline.BASELINE_EXPOSURE_SHADERS);
20
22
  app.world.initResource(RenderDescribers).set("post", (world) => describePost(world));
21
- addPostNodes(app.world);
23
+ addPostNodes(app.world, baseline);
22
24
  }
23
25
  });
24
26
  export {
@@ -69,6 +69,7 @@ fn prepass_output(in: PrepassVertex, n: vec3f) -> PrepassOutput {
69
69
  import shard::prepass::common::{ PrepassVertex, PrepassOutput, prepass_vertex, prepass_output, surface_of };
70
70
  import shard::pbr::material::pbr_input;
71
71
  import shard::pbr::standard::material;
72
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
72
73
 
73
74
  @vertex fn vs(
74
75
  @builtin(instance_index) instance_index: u32,
@@ -84,12 +85,20 @@ import shard::pbr::standard::material;
84
85
 
85
86
  /** Depth, the shaded normal (normal maps included), and velocity. */
86
87
  @fragment fn fs(in: PrepassVertex) -> PrepassOutput {
88
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
87
89
  let p = pbr_input(surface_of(in));
88
90
  @if(MASK) if (p.alpha < material.alphaCutoff) { discard; }
89
91
  return prepass_output(in, p.normal);
92
+ }
93
+
94
+ /** Baseline (0064): depth alone, for what reads depth when the scene is multisampled. */
95
+ @if(BASELINE) @fragment fn fs_depth(in: PrepassVertex) {
96
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
97
+ @if(MASK) if (pbr_input(surface_of(in)).alpha < material.alphaCutoff) { discard; }
90
98
  }`,
91
99
  "shard::prepass::plain": `
92
100
  import shard::prepass::common::{ PrepassVertex, PrepassOutput, prepass_vertex, prepass_output };
101
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
93
102
 
94
103
  @vertex fn vs(
95
104
  @builtin(instance_index) instance_index: u32,
@@ -105,7 +114,13 @@ import shard::prepass::common::{ PrepassVertex, PrepassOutput, prepass_vertex, p
105
114
 
106
115
  /** Materials with their own shading: the geometric normal. */
107
116
  @fragment fn fs(in: PrepassVertex) -> PrepassOutput {
117
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
108
118
  return prepass_output(in, normalize(in.world_normal));
119
+ }
120
+
121
+ /** Baseline (0064): depth alone. */
122
+ @if(BASELINE) @fragment fn fs_depth(in: PrepassVertex) {
123
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
109
124
  }`,
110
125
  "shard::post::fog": `
111
126
  import shard::view::view;
@@ -159,7 +174,10 @@ fn optical_depth(y0: f32, dir_y: f32, t0: f32, t1: f32) -> f32 {
159
174
  sun = light.color * view.sunTransmittance[0].rgb * mix(1.0 / (4.0 * PI), hg, fog.sun);
160
175
  }
161
176
  let inscatter = fog.color.rgb * (sky + sun) * view.exposure;
162
- return vec4f(c.rgb * transmittance + inscatter * (1.0 - transmittance), c.a);
177
+ let color = c.rgb * transmittance + inscatter * (1.0 - transmittance);
178
+ @if(!TRANSPARENT) return vec4f(color, c.a);
179
+ // Premultiplied (0052): the fog is a layer of its own opacity over what's behind it.
180
+ @if(TRANSPARENT) return vec4f(color, c.a * transmittance + (1.0 - transmittance));
163
181
  }`,
164
182
  "shard::post::bloom": `
165
183
  import shard::color::luminance;
@@ -461,12 +479,19 @@ fn sample_history(uv: vec2f) -> vec3f {
461
479
  var hi = vec3f(-1e20);
462
480
  var nearest = 0.0;
463
481
  var nearest_px = px;
482
+ @if(TRANSPARENT) var alpha_lo = 1.0;
483
+ @if(TRANSPARENT) var alpha_hi = 0.0;
464
484
  for (var y = -1; y <= 1; y++) {
465
485
  for (var x = -1; x <= 1; x++) {
466
486
  let q = clamp(px + vec2i(x, y), vec2i(0), size - 1);
467
- let c = rgb_to_ycocg(compress(textureLoad(input, q, 0).rgb));
487
+ let texel = textureLoad(input, q, 0);
488
+ let c = rgb_to_ycocg(compress(texel.rgb));
468
489
  lo = min(lo, c);
469
490
  hi = max(hi, c);
491
+ @if(TRANSPARENT) {
492
+ alpha_lo = min(alpha_lo, texel.a);
493
+ alpha_hi = max(alpha_hi, texel.a);
494
+ }
470
495
  let d = textureLoad(depth_texture, q, 0);
471
496
  if (d > nearest) { nearest = d; nearest_px = q; }
472
497
  }
@@ -487,8 +512,17 @@ fn sample_history(uv: vec2f) -> vec3f {
487
512
  let blended = mix(ycocg_to_rgb(h), current, alpha);
488
513
  let color = expand(max(blended, vec3f(0.0)));
489
514
  var out: TaaOutput;
490
- out.color = vec4f(color, 1.0);
491
- out.history = vec4f(color, 1.0);
515
+ @if(!TRANSPARENT) {
516
+ out.color = vec4f(color, 1.0);
517
+ out.history = vec4f(color, 1.0);
518
+ }
519
+ @if(TRANSPARENT) {
520
+ // Coverage resolves like color: history clamped to the neighborhood, blended at the same rate.
521
+ let past = clamp(textureSampleLevel(history, linear_sampler, prev_uv, 0.0).a, alpha_lo, alpha_hi);
522
+ let coverage = mix(past, textureLoad(input, px, 0).a, alpha);
523
+ out.color = vec4f(color, coverage);
524
+ out.history = vec4f(color, coverage);
525
+ }
492
526
  return out;
493
527
  }`,
494
528
  "shard::post::ssao": `
@@ -614,6 +648,13 @@ fn at(uv: vec2f, o: vec2f, texel: vec2f) -> vec3f {
614
648
  return textureSampleLevel(input, input_sampler, uv + o * texel, 0.0).rgb;
615
649
  }
616
650
 
651
+ /** The output at uv: opaque views write alpha 1; premultiplied ones carry it, rgb <= alpha (0052). */
652
+ fn result(uv: vec2f) -> vec4f {
653
+ let c = textureSampleLevel(input, input_sampler, uv, 0.0);
654
+ @if(!TRANSPARENT) return vec4f(c.rgb, 1.0);
655
+ @if(TRANSPARENT) return vec4f(c.rgb, max(c.a, max(c.r, max(c.g, c.b))));
656
+ }
657
+
617
658
  /** FXAA 3.11 (Lottes), quality preset 12: local contrast, edge direction, end search, blend. */
618
659
  @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
619
660
  let size = vec2f(textureDimensions(input));
@@ -628,7 +669,10 @@ fn at(uv: vec2f, o: vec2f, texel: vec2f) -> vec3f {
628
669
  let lo = min(m, min(min(n, s), min(e, w)));
629
670
  let hi = max(m, max(max(n, s), max(e, w)));
630
671
  let range = hi - lo;
631
- if (range < max(0.0312, hi * 0.125)) { return vec4f(center, 1.0); }
672
+ if (range < max(0.0312, hi * 0.125)) {
673
+ @if(!TRANSPARENT) return vec4f(center, 1.0);
674
+ @if(TRANSPARENT) return result(uv);
675
+ }
632
676
  let nw = luma(at(uv, vec2f(-1.0, -1.0), texel));
633
677
  let ne = luma(at(uv, vec2f(1.0, -1.0), texel));
634
678
  let sw = luma(at(uv, vec2f(-1.0, 1.0), texel));
@@ -689,7 +733,8 @@ fn at(uv: vec2f, o: vec2f, texel: vec2f) -> vec3f {
689
733
  let blend = max(edge_blend, sub_blend);
690
734
  var final_uv = uv;
691
735
  if (horizontal) { final_uv.y += blend * step_len; } else { final_uv.x += blend * step_len; }
692
- return vec4f(textureSampleLevel(input, input_sampler, final_uv, 0.0).rgb, 1.0);
736
+ @if(!TRANSPARENT) return vec4f(textureSampleLevel(input, input_sampler, final_uv, 0.0).rgb, 1.0);
737
+ @if(TRANSPARENT) return result(final_uv);
693
738
  }`
694
739
  };
695
740
  export {
@@ -0,0 +1,24 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /**
3
+ * Projects a world point to CSS pixels on `camera`'s target (origin top-left). Returns false when
4
+ * the point is behind the camera (`out` is left unchanged), or the camera hasn't rendered yet.
5
+ */
6
+ export declare function worldToScreen(world: World, camera: Entity, point: ArrayLike<number>, out: {
7
+ [index: number]: number;
8
+ }): boolean;
9
+ /**
10
+ * The world ray under CSS pixel (x, y) of `camera`'s target: its origin on the near plane and its
11
+ * unit direction. Orthographic rays are parallel, perspective ones start at the near plane.
12
+ */
13
+ export declare function screenToRay(world: World, camera: Entity, x: number, y: number, outOrigin: {
14
+ [index: number]: number;
15
+ }, outDir: {
16
+ [index: number]: number;
17
+ }): void;
18
+ /**
19
+ * Where the ray under CSS pixel (x, y) meets the horizontal plane `y = planeY` (a floor, a ground
20
+ * band). Returns false when it doesn't (parallel, or behind the camera).
21
+ */
22
+ export declare function screenToPlane(world: World, camera: Entity, x: number, y: number, planeY: number, out: {
23
+ [index: number]: number;
24
+ }): boolean;
@@ -0,0 +1,87 @@
1
+ import { mat4 } from "@aethervtt/shard-core";
2
+ import { Cameras } from "./view.js";
3
+ const scratchInv = mat4.create();
4
+ const scratchKey = mat4.create();
5
+ let invValid = false;
6
+ function inverse(viewProj) {
7
+ let same = invValid;
8
+ for (let i = 0; same && i < 16; i++) if (scratchKey[i] !== viewProj[i]) same = false;
9
+ if (!same) {
10
+ mat4.copy(scratchKey, viewProj);
11
+ invValid = mat4.invert(scratchInv, viewProj) !== null;
12
+ }
13
+ return scratchInv;
14
+ }
15
+ function worldToScreen(world, camera, point, out) {
16
+ const cam = world.tryResource(Cameras)?.get(camera);
17
+ if (!cam) return false;
18
+ const m = cam.viewProjNoJitter;
19
+ const x = point[0];
20
+ const y = point[1];
21
+ const z = point[2];
22
+ const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
23
+ const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
24
+ const cw = m[3] * x + m[7] * y + m[11] * z + m[15];
25
+ if (cw <= 1e-9) return false;
26
+ const width = cam.displayWidth / cam.pixelRatio;
27
+ const height = cam.displayHeight / cam.pixelRatio;
28
+ out[0] = (cx / cw * 0.5 + 0.5) * width;
29
+ out[1] = (0.5 - cy / cw * 0.5) * height;
30
+ return true;
31
+ }
32
+ function screenToRay(world, camera, x, y, outOrigin, outDir) {
33
+ const cam = world.tryResource(Cameras)?.get(camera);
34
+ if (!cam) {
35
+ outOrigin[0] = outOrigin[1] = outOrigin[2] = 0;
36
+ outDir[0] = outDir[1] = 0;
37
+ outDir[2] = -1;
38
+ return;
39
+ }
40
+ const inv = inverse(cam.viewProjNoJitter);
41
+ const nx = x / (cam.displayWidth / cam.pixelRatio) * 2 - 1;
42
+ const ny = 1 - y / (cam.displayHeight / cam.pixelRatio) * 2;
43
+ let px = 0;
44
+ let py = 0;
45
+ let pz = 0;
46
+ for (let k = 0; k < 2; k++) {
47
+ const d = k === 0 ? 1 : 0.5;
48
+ const wx = inv[0] * nx + inv[4] * ny + inv[8] * d + inv[12];
49
+ const wy = inv[1] * nx + inv[5] * ny + inv[9] * d + inv[13];
50
+ const wz = inv[2] * nx + inv[6] * ny + inv[10] * d + inv[14];
51
+ const ww = inv[3] * nx + inv[7] * ny + inv[11] * d + inv[15];
52
+ if (k === 0) {
53
+ px = wx / ww;
54
+ py = wy / ww;
55
+ pz = wz / ww;
56
+ } else {
57
+ const dx = wx / ww - px;
58
+ const dy = wy / ww - py;
59
+ const dz = wz / ww - pz;
60
+ const len = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
61
+ outDir[0] = dx / len;
62
+ outDir[1] = dy / len;
63
+ outDir[2] = dz / len;
64
+ }
65
+ }
66
+ outOrigin[0] = px;
67
+ outOrigin[1] = py;
68
+ outOrigin[2] = pz;
69
+ }
70
+ const rayOrigin = new Float64Array(3);
71
+ const rayDir = new Float64Array(3);
72
+ function screenToPlane(world, camera, x, y, planeY, out) {
73
+ screenToRay(world, camera, x, y, rayOrigin, rayDir);
74
+ const dy = rayDir[1];
75
+ if (Math.abs(dy) < 1e-9) return false;
76
+ const t = (planeY - rayOrigin[1]) / dy;
77
+ if (t < 0) return false;
78
+ out[0] = rayOrigin[0] + rayDir[0] * t;
79
+ out[1] = planeY;
80
+ out[2] = rayOrigin[2] + rayDir[2] * t;
81
+ return true;
82
+ }
83
+ export {
84
+ screenToPlane,
85
+ screenToRay,
86
+ worldToScreen
87
+ };
@@ -1,4 +1,5 @@
1
1
  /** Readback helpers: bytes per texel and conversion of any readable format to floats. */
2
2
  export declare const BYTES_PER_TEXEL: Partial<Record<GPUTextureFormat, number>>;
3
+ export declare function halfToFloat(h: number): number;
3
4
  /** Unpacks a readback row of any supported format into 4 floats per pixel. */
4
5
  export declare function toFloats(src: ArrayBuffer, format: GPUTextureFormat, count: number, out: Float32Array, o: number): void;
package/dist/readback.js CHANGED
@@ -91,5 +91,6 @@ function small(v, mantissaBits) {
91
91
  }
92
92
  export {
93
93
  BYTES_PER_TEXEL,
94
+ halfToFloat,
94
95
  toFloats
95
96
  };
@@ -2,6 +2,7 @@ import { defineResource } from "@aethervtt/shard-core";
2
2
  import { DisplayRate } from "@aethervtt/shard-runtime";
3
3
  import { Views } from "./plugin.js";
4
4
  const RenderScale = defineResource("render/RenderScale", {
5
+ hostWritable: true,
5
6
  description: "Render resolution of window cameras (0051): the scene renders at scale \xD7 the window size and is upscaled, UI stays native. auto moves the scale between min and max to hold the frame budget: targetMs, or one display refresh (DisplayRate) when it is 0.",
6
7
  init: () => ({
7
8
  mode: "auto",
@@ -0,0 +1,97 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ /** Effects `ScreenEffects` holds at once. */
3
+ export declare const MAX_SCREEN_EFFECTS = 8;
4
+ /** Keys an effect's params may have. */
5
+ export declare const MAX_SCREEN_EFFECT_PARAMS = 8;
6
+ export type ScreenEffectParams = Record<string, number | string | boolean>;
7
+ /** An effect another app's view should draw at a screen point (0065). */
8
+ export interface ScreenEffect {
9
+ /** What to draw: `fire`, or a kind the consuming app registered. */
10
+ kind: string;
11
+ /** Center, in the target's CSS pixels. */
12
+ screen: [number, number];
13
+ /** In CSS pixels. */
14
+ radius: number;
15
+ /** Ms it has left: counts down each frame, and refreshing sets it again. At 0 it drops. */
16
+ ttlMs: number;
17
+ /** What published it (a die); one effect per (source, kind). */
18
+ source: Entity;
19
+ params: ScreenEffectParams;
20
+ }
21
+ export declare const ScreenEffects: import("@aethervtt/shard-core").ResourceDef<{
22
+ effects: ScreenEffect[];
23
+ }>;
24
+ /** What an app draws for one kind of screen effect. */
25
+ export interface ScreenEffectHandler {
26
+ /** An effect of this kind appeared (a new source): spawn what draws it. */
27
+ start(world: World, effect: ScreenEffect): Entity[] | undefined;
28
+ /** Each frame it stays live (refreshed), with the frame's seconds. */
29
+ update?(world: World, effect: ScreenEffect, entities: readonly Entity[], dt: number): void;
30
+ /** It expired or was cleared. Default: despawn what `start` returned. */
31
+ end?(world: World, effect: ScreenEffect, entities: readonly Entity[]): void;
32
+ }
33
+ interface LiveEffect {
34
+ source: Entity;
35
+ kind: string;
36
+ handler: ScreenEffectHandler;
37
+ entities: Entity[];
38
+ /** The effect as last seen, for `end`. */
39
+ last: ScreenEffect;
40
+ seen: boolean;
41
+ }
42
+ /** This app's handlers, the effects they're drawing, and the unhandled kinds already reported. */
43
+ export declare const ScreenEffectHandlers: import("@aethervtt/shard-core").ResourceDef<{
44
+ kinds: Map<string, ScreenEffectHandler>;
45
+ live: LiveEffect[];
46
+ reported: Set<string>;
47
+ }>;
48
+ /**
49
+ * Publishes or refreshes `source`'s effect of this kind. Returns false when `MAX_SCREEN_EFFECTS`
50
+ * others are live. Throws `render/invalid-screen-effect` for an effect without a kind, a negative
51
+ * radius, or more than 8 params.
52
+ */
53
+ export declare function publishScreenEffect(world: World, effect: ScreenEffect): boolean;
54
+ /** Drops `source`'s effects, or every effect. Publishers call it on dismissal and teardown. */
55
+ export declare function clearScreenEffects(world: World, source?: Entity): void;
56
+ /**
57
+ * Copies `from`'s effects into `to` (another app on the device, 0052), for the host to call each
58
+ * frame, as `forwardLensFields`: copies, each app counting its own down; `offset` is where `to`'s
59
+ * target sits in `from`'s CSS pixels, subtracted from each center.
60
+ */
61
+ export declare function forwardScreenEffects(from: World, to: World, offset?: readonly [number, number]): void;
62
+ /**
63
+ * Registers what this app draws for a kind of screen effect: `start` when one appears, `update`
64
+ * while it's refreshed, `end` when it expires (default: despawn what `start` returned).
65
+ */
66
+ export declare function onScreenEffect(world: World, kind: string, handler: ScreenEffectHandler): void;
67
+ /** The frame demand live effects hold, so they expire on time and the app idles after (0052). */
68
+ export declare const SCREEN_EFFECTS_DEMAND = "render/screen-effects";
69
+ export declare const expireScreenEffects: import("@aethervtt/shard-core").SystemDef<undefined>;
70
+ /**
71
+ * Starts, updates and ends this app's handlers as effects appear, stay and go. An effect of a kind
72
+ * nobody registered is dropped and logged once per kind (`render/unhandled-screen-effect`).
73
+ * Allocates only when an effect starts.
74
+ */
75
+ export declare const runScreenEffects: import("@aethervtt/shard-core").SystemDef<undefined>;
76
+ /** For render.describe: live effects, and which kinds this app draws. */
77
+ export declare function describeScreenEffects(world: World): {
78
+ effects: {
79
+ /** What to draw: `fire`, or a kind the consuming app registered. */
80
+ kind: string;
81
+ /** In CSS pixels. */
82
+ radius: number;
83
+ /** Ms it has left: counts down each frame, and refreshing sets it again. At 0 it drops. */
84
+ ttlMs: number;
85
+ /** What published it (a die); one effect per (source, kind). */
86
+ source: Entity;
87
+ params: ScreenEffectParams;
88
+ screen: number[];
89
+ }[];
90
+ kinds: string[];
91
+ drawing: {
92
+ kind: string;
93
+ source: number;
94
+ entities: number;
95
+ }[];
96
+ };
97
+ export {};