@aethervtt/shard-render 0.0.1 → 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 (170) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.js +43 -2
  5. package/dist/atmosphere-nodes.d.ts +9 -0
  6. package/dist/atmosphere-nodes.js +157 -30
  7. package/dist/atmosphere-plugin.js +8 -2
  8. package/dist/atmosphere-shaders.js +17 -5
  9. package/dist/baseline/atmosphere.d.ts +16 -0
  10. package/dist/baseline/atmosphere.js +301 -0
  11. package/dist/baseline/data-texture.d.ts +36 -0
  12. package/dist/baseline/data-texture.js +181 -0
  13. package/dist/baseline/environment.d.ts +14 -0
  14. package/dist/baseline/environment.js +368 -0
  15. package/dist/baseline/exposure.d.ts +25 -0
  16. package/dist/baseline/exposure.js +133 -0
  17. package/dist/baseline/lights.d.ts +33 -0
  18. package/dist/baseline/lights.js +133 -0
  19. package/dist/baseline/textures.d.ts +97 -0
  20. package/dist/baseline/textures.js +203 -0
  21. package/dist/camera.d.ts +2 -0
  22. package/dist/camera.js +4 -0
  23. package/dist/crowd.d.ts +21 -0
  24. package/dist/crowd.js +121 -0
  25. package/dist/culling-shaders.js +21 -2
  26. package/dist/culling.d.ts +11 -2
  27. package/dist/culling.js +44 -18
  28. package/dist/cutaway-plugin.d.ts +48 -0
  29. package/dist/cutaway-plugin.js +364 -0
  30. package/dist/cutaway.d.ts +27 -0
  31. package/dist/cutaway.js +49 -0
  32. package/dist/data-store.d.ts +69 -0
  33. package/dist/data-store.js +105 -0
  34. package/dist/debug-shaders.js +17 -3
  35. package/dist/debug-views.js +4 -2
  36. package/dist/decals-plugin.d.ts +5 -0
  37. package/dist/decals-plugin.js +54 -0
  38. package/dist/decals.d.ts +69 -0
  39. package/dist/decals.js +326 -0
  40. package/dist/deferred-shaders.js +2 -0
  41. package/dist/deferred.js +28 -4
  42. package/dist/deform.d.ts +5 -4
  43. package/dist/deform.js +4 -9
  44. package/dist/describe-plugin.d.ts +4 -2
  45. package/dist/describe-plugin.js +75 -3
  46. package/dist/display-nodes.js +37 -13
  47. package/dist/dynamic-resolution.d.ts +2 -0
  48. package/dist/dynamic-resolution.js +6 -1
  49. package/dist/environment-plugin.js +17 -2
  50. package/dist/environment-state.d.ts +7 -3
  51. package/dist/environment-state.js +20 -9
  52. package/dist/environment.js +7 -1
  53. package/dist/features.d.ts +49 -0
  54. package/dist/features.js +106 -0
  55. package/dist/foliage-budget.d.ts +64 -0
  56. package/dist/foliage-budget.js +108 -0
  57. package/dist/foliage-path.d.ts +19 -0
  58. package/dist/foliage-path.js +7 -0
  59. package/dist/foliage-shaders.d.ts +11 -0
  60. package/dist/foliage-shaders.js +504 -0
  61. package/dist/foliage.d.ts +194 -0
  62. package/dist/foliage.js +1027 -0
  63. package/dist/forward.d.ts +38 -2
  64. package/dist/forward.js +608 -186
  65. package/dist/fxaa.js +12 -2
  66. package/dist/gizmos-plugin.js +7 -0
  67. package/dist/gizmos.d.ts +3 -3
  68. package/dist/gizmos.js +11 -13
  69. package/dist/gpu-assets.d.ts +58 -4
  70. package/dist/gpu-assets.js +191 -8
  71. package/dist/graph.d.ts +45 -0
  72. package/dist/graph.js +169 -19
  73. package/dist/health.d.ts +58 -0
  74. package/dist/health.js +112 -0
  75. package/dist/index.d.ts +40 -11
  76. package/dist/index.js +343 -9
  77. package/dist/instances.d.ts +119 -8
  78. package/dist/instances.js +387 -78
  79. package/dist/interior-plugin.d.ts +121 -0
  80. package/dist/interior-plugin.js +465 -0
  81. package/dist/interior.d.ts +24 -0
  82. package/dist/interior.js +13 -0
  83. package/dist/layers.d.ts +35 -0
  84. package/dist/layers.js +49 -0
  85. package/dist/lens-plugin.d.ts +43 -0
  86. package/dist/lens-plugin.js +304 -0
  87. package/dist/lens.d.ts +41 -0
  88. package/dist/lens.js +99 -0
  89. package/dist/lights.d.ts +50 -5
  90. package/dist/lights.js +101 -22
  91. package/dist/material-pipelines.d.ts +37 -4
  92. package/dist/material-pipelines.js +102 -5
  93. package/dist/materials.d.ts +23 -0
  94. package/dist/materials.js +44 -1
  95. package/dist/outline-plugin.d.ts +48 -0
  96. package/dist/outline-plugin.js +529 -0
  97. package/dist/outline.d.ts +15 -0
  98. package/dist/outline.js +42 -0
  99. package/dist/overlay-registry.d.ts +3 -1
  100. package/dist/overlay-registry.js +3 -1
  101. package/dist/overlays.js +6 -2
  102. package/dist/perf-overlay.d.ts +9 -0
  103. package/dist/perf-overlay.js +125 -0
  104. package/dist/picking.d.ts +17 -0
  105. package/dist/picking.js +40 -11
  106. package/dist/pixel-perfect-plugin.js +10 -2
  107. package/dist/plugin.d.ts +69 -7
  108. package/dist/plugin.js +223 -25
  109. package/dist/post-common.d.ts +22 -2
  110. package/dist/post-common.js +82 -2
  111. package/dist/post-nodes.d.ts +7 -1
  112. package/dist/post-nodes.js +235 -111
  113. package/dist/post-plugin.js +5 -3
  114. package/dist/post-scene.d.ts +130 -0
  115. package/dist/post-scene.js +164 -0
  116. package/dist/post-shaders.js +85 -62
  117. package/dist/post.js +11 -1
  118. package/dist/projection.d.ts +24 -0
  119. package/dist/projection.js +87 -0
  120. package/dist/readback.d.ts +1 -0
  121. package/dist/readback.js +1 -0
  122. package/dist/render-scale.js +1 -0
  123. package/dist/ribbons-plugin.d.ts +5 -0
  124. package/dist/ribbons-plugin.js +59 -0
  125. package/dist/ribbons.d.ts +44 -0
  126. package/dist/ribbons.js +288 -0
  127. package/dist/scenario.d.ts +70 -0
  128. package/dist/scenario.js +125 -0
  129. package/dist/screen-effects.d.ts +97 -0
  130. package/dist/screen-effects.js +192 -0
  131. package/dist/shaders.d.ts +13 -0
  132. package/dist/shaders.js +189 -34
  133. package/dist/shadow-catcher.d.ts +9 -0
  134. package/dist/shadow-catcher.js +133 -0
  135. package/dist/shadows.d.ts +27 -2
  136. package/dist/shadows.js +100 -20
  137. package/dist/standard-fields.d.ts +1 -0
  138. package/dist/standard-fields.js +5 -0
  139. package/dist/standard.js +18 -0
  140. package/dist/stats.d.ts +45 -1
  141. package/dist/stats.js +91 -3
  142. package/dist/surface-model.d.ts +57 -0
  143. package/dist/surface-model.js +289 -0
  144. package/dist/surface-shaders.d.ts +3 -0
  145. package/dist/surface-shaders.js +160 -0
  146. package/dist/surface.d.ts +40 -0
  147. package/dist/surface.js +86 -0
  148. package/dist/target.d.ts +1 -13
  149. package/dist/target.js +1 -26
  150. package/dist/testing.d.ts +11 -0
  151. package/dist/testing.js +39 -1
  152. package/dist/tier.d.ts +13 -0
  153. package/dist/tier.js +14 -0
  154. package/dist/timer.d.ts +18 -1
  155. package/dist/timer.js +73 -20
  156. package/dist/variants.d.ts +37 -0
  157. package/dist/variants.js +104 -0
  158. package/dist/vfx-draw.d.ts +37 -0
  159. package/dist/vfx-draw.js +291 -0
  160. package/dist/vfx-state.d.ts +42 -0
  161. package/dist/vfx-state.js +81 -0
  162. package/dist/vfx.d.ts +69 -0
  163. package/dist/vfx.js +103 -0
  164. package/dist/view-visibility-plugin.d.ts +61 -0
  165. package/dist/view-visibility-plugin.js +267 -0
  166. package/dist/view-visibility.d.ts +28 -0
  167. package/dist/view-visibility.js +46 -0
  168. package/dist/view.d.ts +29 -1
  169. package/dist/view.js +57 -34
  170. package/package.json +23 -11
@@ -0,0 +1,42 @@
1
+ import { defineComponent, defineResource, onAdd, t } from "@aethervtt/shard-core";
2
+ import { warnFeatureMissing } from "./features.js";
3
+ const OUTLINE_OCCLUSION = ["hide", "show", "dim"];
4
+ const Outline = defineComponent(
5
+ "render/Outline",
6
+ {
7
+ color: t.color({
8
+ default: [1, 0.78, 0.2, 1],
9
+ description: "Outline color (linear), with alpha."
10
+ }),
11
+ width: t.f32({
12
+ default: 2,
13
+ min: 0,
14
+ max: 32,
15
+ unit: "px",
16
+ description: "Width in CSS pixels, outside the silhouette, at any zoom and display density."
17
+ }),
18
+ occluded: t.enum(OUTLINE_OCCLUSION, {
19
+ description: "Where something hides the outlined object: hide the outline there, show it anyway, or dim it."
20
+ })
21
+ },
22
+ {
23
+ description: "Outlines a renderable, or every renderable under this entity. Selection and hover are two Outlines with different colors; the host sets and clears them."
24
+ }
25
+ );
26
+ const OutlinePath = defineResource("render/OutlinePath", {
27
+ description: "Present when the outline pass is installed."
28
+ });
29
+ const MAX_OUTLINE_STYLES = 16;
30
+ function observeOutlinesWithoutPass(world) {
31
+ world.observe(onAdd(Outline), ({ world: w }) => {
32
+ if (!w.hasResource(OutlinePath))
33
+ warnFeatureMissing(w, "An entity has Outline", "outlinePlugin", "it draws no outline");
34
+ });
35
+ }
36
+ export {
37
+ MAX_OUTLINE_STYLES,
38
+ OUTLINE_OCCLUSION,
39
+ Outline,
40
+ OutlinePath,
41
+ observeOutlinesWithoutPass
42
+ };
@@ -1,6 +1,8 @@
1
1
  import type { Entity, World } from '@aethervtt/shard-core';
2
2
  import type { GizmoStore } from './gizmos.js';
3
- export declare const OVERLAYS: readonly ['bounds', 'lights', 'cameras', 'cascades', 'normals', 'axes', 'labels'];
3
+ export declare const OVERLAYS: readonly ['bounds', 'lights', 'cameras', 'cascades', 'normals', 'axes', 'labels',
4
+ /** Frame time, the top 8 spans, and the GPU frame (0074). */
5
+ 'perf'];
4
6
  export type Overlay = (typeof OVERLAYS)[number];
5
7
  /** An overlay another package draws (physics colliders, the navmesh, UI rects). */
6
8
  export interface OverlayDef {
@@ -5,7 +5,9 @@ const OVERLAYS = [
5
5
  "cascades",
6
6
  "normals",
7
7
  "axes",
8
- "labels"
8
+ "labels",
9
+ /** Frame time, the top 8 spans, and the GPU frame (0074). */
10
+ "perf"
9
11
  ];
10
12
  const extraOverlays = /* @__PURE__ */ new Map();
11
13
  function defineOverlay(def) {
package/dist/overlays.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import { DirectionalLight, Lights } from "./lights.js";
16
16
  import { defineOverlay, OVERLAYS, registeredOverlays } from "./overlay-registry.js";
17
17
  const extraOverlays = registeredOverlays();
18
+ import { drawPerfOverlay } from "./perf-overlay.js";
18
19
  import { Views } from "./plugin.js";
19
20
  import { Cameras, cameraOf } from "./view.js";
20
21
  import {
@@ -34,7 +35,8 @@ function scenePath(world, entity) {
34
35
  return v?.path || void 0;
35
36
  }
36
37
  const DebugOverlays = defineResource("render/DebugOverlays", {
37
- description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, and entity labels.",
38
+ hostWritable: true,
39
+ description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, entity labels, and perf (frame time, top spans, GPU frame).",
38
40
  init: () => ({
39
41
  bounds: false,
40
42
  lights: false,
@@ -43,6 +45,7 @@ const DebugOverlays = defineResource("render/DebugOverlays", {
43
45
  normals: false,
44
46
  axes: false,
45
47
  labels: false,
48
+ perf: false,
46
49
  extra: {},
47
50
  filter: { components: [], path: "" },
48
51
  name: scenePath
@@ -226,7 +229,7 @@ const drawOverlays = defineSystem({
226
229
  g.overlay.clear();
227
230
  let extra = false;
228
231
  for (const name in o.extra) if (o.extra[name] && extraOverlays.has(name)) extra = true;
229
- if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || extra))
232
+ if (!(o.bounds || o.lights || o.cameras || o.cascades || o.normals || o.axes || o.labels || o.perf || extra))
230
233
  return;
231
234
  g.beginOverlays();
232
235
  try {
@@ -249,6 +252,7 @@ function draw(world, o, g) {
249
252
  }
250
253
  if (o.lights) drawLights(world, o, g, defs);
251
254
  const primary = primaryCamera(world);
255
+ if (o.perf) drawPerfOverlay(world, g, primary);
252
256
  if (o.cameras) {
253
257
  for (const [entity, cam] of world.resource(Cameras)) {
254
258
  if (entity === primary || !passes(world, o, entity, defs)) continue;
@@ -0,0 +1,9 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { GizmoStore } from './gizmos.js';
3
+ /** The overlay's text and colours: refreshed four times a second, so it's readable and cheap. */
4
+ export declare function perfOverlayLines(world: World): {
5
+ lines: string[];
6
+ colors: number[][];
7
+ };
8
+ /** Draws the perf overlay for the primary camera. Gizmo labels center on their anchor, above it. */
9
+ export declare function drawPerfOverlay(world: World, g: GizmoStore, camera: Entity | undefined): void;
@@ -0,0 +1,125 @@
1
+ import { ProfilerResource, spanStats } from "@aethervtt/shard-core";
2
+ import {
3
+ gpuPassOverlap,
4
+ isGpuPass,
5
+ measureSlices,
6
+ PerfBudgets,
7
+ PerfScenario,
8
+ perfBudgetState,
9
+ Time
10
+ } from "@aethervtt/shard-runtime";
11
+ import { PassCosts } from "./ablation.js";
12
+ import { LABEL_FONT, labelWidth } from "./gizmo-font.js";
13
+ import { Cameras } from "./view.js";
14
+ const LINES = 8;
15
+ const PASS_LINES = 4;
16
+ const REFRESH_S = 0.25;
17
+ const MARGIN = 8;
18
+ const COLOR = [1, 1, 1, 1];
19
+ const HEADER = [0.55, 0.9, 1, 1];
20
+ const GREY = [0.6, 0.6, 0.6, 1];
21
+ const RED = [1, 0.35, 0.3, 1];
22
+ const LABEL_OPTIONS = { depthTest: false };
23
+ const states = /* @__PURE__ */ new WeakMap();
24
+ const pad = (ms) => ms.toFixed(2).padStart(6);
25
+ const percent = (share) => `${Math.round(share * 100)}%`.padStart(4);
26
+ function perfOverlayLines(world) {
27
+ const now = world.tryResource(Time)?.elapsed ?? 0;
28
+ let state = states.get(world);
29
+ if (state && now - state.at < REFRESH_S && now >= state.at) return state;
30
+ const profiler = world.tryResource(ProfilerResource);
31
+ const lines = [];
32
+ const colors = [];
33
+ const line = (text, color) => {
34
+ lines.push(text);
35
+ colors.push(color);
36
+ };
37
+ if (profiler) {
38
+ const frame = spanStats(profiler, "frame");
39
+ const gpu = spanStats(profiler, "gpu:frame");
40
+ line(
41
+ `frame ${frame ? pad(frame.avg) : " -"} ms p95 ${frame ? pad(frame.p95) : " -"}` + (profiler.gpu.status === "unavailable" ? " gpu n/a" : ` gpu ${gpu ? pad(gpu.avg) : " -"} ms`),
42
+ HEADER
43
+ );
44
+ const overlapping = gpuPassOverlap(profiler)?.overlapping ?? false;
45
+ const spans = [];
46
+ const passes = [];
47
+ for (const name2 of profiler.names()) {
48
+ if (name2 === "frame" || name2 === "gpu:frame" || name2.startsWith("schedule/") || name2.startsWith("commands/"))
49
+ continue;
50
+ const entry = { name: name2, avg: profiler.timing(name2).avg };
51
+ if (overlapping && name2.startsWith("gpu:")) {
52
+ if (isGpuPass(name2)) passes.push(entry);
53
+ } else spans.push(entry);
54
+ }
55
+ spans.sort((a, b) => b.avg - a.avg || (a.name < b.name ? -1 : 1));
56
+ for (let i = 0; i < Math.min(LINES, spans.length); i++) {
57
+ line(`${pad(spans[i].avg)} ms ${spans[i].name}`, COLOR);
58
+ }
59
+ if (overlapping) {
60
+ const ablation = world.tryResource(PassCosts)?.latest;
61
+ if (ablation && ablation.passes.length > 0) {
62
+ line("gpu passes by ablation (timestamps overlap)", GREY);
63
+ const ranked = [...ablation.passes].sort((a, b) => b.ms - a.ms);
64
+ for (let i = 0; i < Math.min(PASS_LINES, ranked.length); i++) {
65
+ line(`${pad(ranked[i].ms)} ms gpu:${ranked[i].pass} ablated`, COLOR);
66
+ }
67
+ } else {
68
+ line("gpu passes overlapping: perf.ablate ranks them", GREY);
69
+ passes.sort((a, b) => a.name < b.name ? -1 : 1);
70
+ for (let i = 0; i < Math.min(PASS_LINES, passes.length); i++) {
71
+ line(`${pad(passes[i].avg)} ms ${passes[i].name} overlapping`, GREY);
72
+ }
73
+ }
74
+ }
75
+ const name = world.tryResource(PerfScenario)?.name;
76
+ const scenario = name ? world.tryResource(PerfBudgets)?.budgets.scenarios[name] : void 0;
77
+ if (name && scenario) {
78
+ const machine = perfBudgetState(world)?.detection.machine ?? null;
79
+ line(`scenario ${name}${machine ? ` on ${machine}` : ""}: share of frame / budget`, HEADER);
80
+ for (const s of measureSlices(profiler, scenario, machine).slices) {
81
+ const measured = s.measuredShare === null ? " -" : percent(s.measuredShare);
82
+ line(
83
+ `${s.track} ${measured} / ${percent(s.share)} ${s.slice}`,
84
+ s.verdict === "over" ? RED : s.verdict === "unmeasured" ? GREY : COLOR
85
+ );
86
+ }
87
+ }
88
+ }
89
+ state = { at: now, lines, colors };
90
+ states.set(world, state);
91
+ return state;
92
+ }
93
+ const point = new Float32Array(3);
94
+ function unproject(inv, width, height, px, py) {
95
+ const x = px / width * 2 - 1;
96
+ const y = 1 - py / height * 2;
97
+ const z = 0.5;
98
+ const w = inv[3] * x + inv[7] * y + inv[11] * z + inv[15];
99
+ point[0] = (inv[0] * x + inv[4] * y + inv[8] * z + inv[12]) / w;
100
+ point[1] = (inv[1] * x + inv[5] * y + inv[9] * z + inv[13]) / w;
101
+ point[2] = (inv[2] * x + inv[6] * y + inv[10] * z + inv[14]) / w;
102
+ return point;
103
+ }
104
+ function drawPerfOverlay(world, g, camera) {
105
+ if (camera === void 0) return;
106
+ const cam = world.resource(Cameras).get(camera);
107
+ if (!cam || cam.width <= 0 || cam.height <= 0) return;
108
+ const { lines, colors } = perfOverlayLines(world);
109
+ const step = LABEL_FONT.cellHeight + 4;
110
+ for (let i = 0; i < lines.length; i++) {
111
+ const text = lines[i];
112
+ const px = MARGIN + labelWidth(text) / 2;
113
+ const py = MARGIN + (i + 1) * step;
114
+ g.label(
115
+ unproject(cam.invViewProj, cam.width, cam.height, px, py),
116
+ text,
117
+ colors[i],
118
+ LABEL_OPTIONS
119
+ );
120
+ }
121
+ }
122
+ export {
123
+ drawPerfOverlay,
124
+ perfOverlayLines
125
+ };
package/dist/picking.d.ts CHANGED
@@ -12,7 +12,22 @@ export interface PickHit {
12
12
  normal: [number, number, number];
13
13
  /** Meters from the camera (from the ray origin, for raycasts). */
14
14
  distance: number;
15
+ /**
16
+ * What within the entity was hit, when something knows (0059): a tilemap gives
17
+ * `{ kind: 'tile', layer, x, y, tile }`, the tile named as its palette names it.
18
+ */
19
+ detail?: PickDetail;
15
20
  }
21
+ /** Detail of a pick within an entity: `kind` says what the rest means. */
22
+ export interface PickDetail {
23
+ kind: string;
24
+ [key: string]: unknown;
25
+ }
26
+ /**
27
+ * Turns a hit into detail about what within the entity it hit (a tile of a tilemap), or
28
+ * undefined when the hit isn't its kind. Runs for GPU picks and raycasts alike.
29
+ */
30
+ export type PickDetailer = (world: World, hit: PickHit) => PickDetail | undefined;
16
31
  interface PickRequest {
17
32
  x: number;
18
33
  y: number;
@@ -35,6 +50,8 @@ export interface PickingState {
35
50
  pending: Map<string, PickRequest[]>;
36
51
  drawers: Map<string, PickDrawer>;
37
52
  blockers: Map<string, PickBlocker>;
53
+ /** Detail for hits, by who adds it (tilemaps). */
54
+ detailers: Map<string, PickDetailer>;
38
55
  /** Views whose pick pass skipped a draw (a pipeline still compiling): their picks wait a frame. */
39
56
  incomplete: Set<string>;
40
57
  }
package/dist/picking.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { defineResource, mat4, ShardError } from "@aethervtt/shard-core";
2
2
  import { GlobalTransform } from "@aethervtt/shard-transform";
3
- import { Meshes } from "./assets.js";
4
- import { drawMaterials, ForwardStateResource, PASS_PICK } from "./forward.js";
3
+ import { Materials, Meshes } from "./assets.js";
4
+ import { addRenderFeatures } from "./features.js";
5
+ import { drawMaterials, ForwardStateResource, PASS_PICK, PASS_PICK_GROUND } from "./forward.js";
5
6
  import { RenderPhase } from "./graph.js";
6
- import { Lod, Mesh3d } from "./instances.js";
7
+ import { Lod, Mesh3d, MeshMaterial } from "./instances.js";
7
8
  import { DebugOverlays } from "./overlays.js";
8
9
  import { Graph, Views } from "./plugin.js";
9
10
  import { cameraOf } from "./view.js";
@@ -14,6 +15,7 @@ const Picking = defineResource("render/Picking", {
14
15
  pending: /* @__PURE__ */ new Map(),
15
16
  drawers: /* @__PURE__ */ new Map(),
16
17
  blockers: /* @__PURE__ */ new Map(),
18
+ detailers: /* @__PURE__ */ new Map(),
17
19
  incomplete: /* @__PURE__ */ new Set()
18
20
  })
19
21
  });
@@ -69,6 +71,8 @@ function pickNode(world) {
69
71
  drawMaterials(ctx, state, pv, cam, cam.draws, PASS_PICK);
70
72
  if (cam.deferred) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_PICK);
71
73
  drawMaterials(ctx, state, pv, cam, cam.transparent, PASS_PICK);
74
+ drawMaterials(ctx, state, pv, cam, cam.ground, PASS_PICK_GROUND);
75
+ drawMaterials(ctx, state, pv, cam, cam.overlay, PASS_PICK_GROUND);
72
76
  for (const draw of picking.drawers.values()) draw(ctx, cam);
73
77
  if (ctx.gpu.pipelines.skipped > skipped) picking.incomplete.add(ctx.view.name);
74
78
  }
@@ -147,13 +151,15 @@ function pickReadbackNode(world) {
147
151
  const dx = position[0] - eye[0];
148
152
  const dy = position[1] - eye[1];
149
153
  const dz = position[2] - eye[2];
150
- r.resolve({
151
- entity,
152
- path: w.isAlive(entity) ? name(w, entity) : void 0,
153
- position,
154
- normal: [n[0], n[1], n[2]],
155
- distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
156
- });
154
+ r.resolve(
155
+ withDetail(w, {
156
+ entity,
157
+ path: w.isAlive(entity) ? name(w, entity) : void 0,
158
+ position,
159
+ normal: [n[0], n[1], n[2]],
160
+ distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
161
+ })
162
+ );
157
163
  });
158
164
  buffer.unmap();
159
165
  buffer.destroy();
@@ -173,6 +179,12 @@ function addPickNodes(world) {
173
179
  graph.declare({ name: "pick-id", format: "r32uint", usage });
174
180
  graph.declare({ name: "pick-normal", format: "rgba32float", usage });
175
181
  graph.declare({ name: "pick-depth", format: "depth32float" });
182
+ addRenderFeatures(world, {
183
+ name: "render/picking",
184
+ description: "Entity picking from id and normal targets, read back per request.",
185
+ nodes: ["picking", "picking/readback"],
186
+ baseline: { strategy: "The same targets, read back with copies" }
187
+ });
176
188
  graph.addNode("picking", pickNode(world));
177
189
  graph.addNode("picking/readback", pickReadbackNode(world));
178
190
  }
@@ -235,11 +247,14 @@ function worldBox(bb, m, out) {
235
247
  }
236
248
  function buildBvh(world, tick, tables) {
237
249
  const items = [];
250
+ const materials = world.tryResource(Materials);
238
251
  for (const table of world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility] }).tables) {
239
252
  const g = table.column(GlobalTransform, "matrix");
240
253
  const vis = table.column(ComputedVisibility, "visible");
254
+ const refs = table.has(MeshMaterial) ? table.column(MeshMaterial, "material") : void 0;
241
255
  for (let i = 0; i < table.count; i++) {
242
256
  if (!vis[i]) continue;
257
+ if (refs && materials?.get(refs[i])?.type.pickable === false) continue;
243
258
  const entity = table.entities[i];
244
259
  const mesh = meshOf(world, entity);
245
260
  if (!mesh) continue;
@@ -442,7 +457,21 @@ function raycast(world, origin, direction, options = {}) {
442
457
  }
443
458
  }
444
459
  hits.sort((a, b) => a.distance - b.distance);
445
- return options.all ? hits : hits.slice(0, 1);
460
+ const out = options.all ? hits : hits.slice(0, 1);
461
+ for (const hit of out) withDetail(world, hit);
462
+ return out;
463
+ }
464
+ function withDetail(world, hit) {
465
+ const detailers = world.tryResource(Picking)?.detailers;
466
+ if (!detailers || detailers.size === 0 || !world.isAlive(hit.entity)) return hit;
467
+ for (const detailer of detailers.values()) {
468
+ const detail = detailer(world, hit);
469
+ if (detail) {
470
+ hit.detail = detail;
471
+ break;
472
+ }
473
+ }
474
+ return hit;
446
475
  }
447
476
  export {
448
477
  BvhResource,
@@ -1,4 +1,5 @@
1
1
  import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { addRenderFeatures } from "./features.js";
2
3
  import { RenderPhase } from "./graph.js";
3
4
  import { PixelPerfectPath } from "./pixel-perfect.js";
4
5
  import { Graph, Shaders } from "./plugin.js";
@@ -6,7 +7,7 @@ import { registerShaders } from "./shaders.js";
6
7
  const PIXEL_PERFECT_SHADERS = {
7
8
  "shard::post::pixel_upscale": `
8
9
  struct Upscale {
9
- /** offset (xy), scale, 0. */
10
+ /** offset (xy), scale, the bars' alpha (0 for a camera clearing to alpha, 0052). */
10
11
  params: vec4f,
11
12
  }
12
13
 
@@ -17,7 +18,7 @@ struct Upscale {
17
18
  @fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
18
19
  let p = (frag.xy - upscale.params.xy) / upscale.params.z;
19
20
  let size = vec2f(textureDimensions(source));
20
- if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0, 1.0); }
21
+ if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0, upscale.params.w); }
21
22
  return textureLoad(source, vec2i(floor(p)), 0);
22
23
  }`
23
24
  };
@@ -92,6 +93,7 @@ function pixelUpscaleNode() {
92
93
  params[0] = layout.offsetX;
93
94
  params[1] = layout.offsetY;
94
95
  params[2] = layout.scale;
96
+ params[3] = ctx.view.data.alphaOutput === true ? 0 : 1;
95
97
  gpu.device.queue.writeBuffer(buffer, 0, params);
96
98
  const source = layout.target.texture();
97
99
  let cached = groups.get(ctx.view.name);
@@ -125,6 +127,12 @@ const pixelPerfectPlugin = definePlugin({
125
127
  },
126
128
  ready(app) {
127
129
  registerShaders(app.world.resource(Shaders), PIXEL_PERFECT_SHADERS);
130
+ addRenderFeatures(app.world, {
131
+ name: "render/pixel-perfect",
132
+ description: "Pixel-art cameras: low-resolution render, integer upscale.",
133
+ nodes: ["pixel-upscale"],
134
+ baseline: { strategy: "The same fragment pass" }
135
+ });
128
136
  app.world.resource(Graph).addNode("pixel-upscale", pixelUpscaleNode());
129
137
  }
130
138
  });
package/dist/plugin.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type World } from '@aethervtt/shard-core';
2
- import { type GpuContext } from '@aethervtt/shard-gpu';
2
+ import { type GpuBackendChoice, type GpuContext, type Surface, type SurfaceAlpha, type Webgl2ContextOptions } from '@aethervtt/shard-gpu';
3
3
  import { type Plugin } from '@aethervtt/shard-runtime';
4
4
  import { type ShaderBake, ShaderLibrary } from '@aethervtt/shard-shader';
5
5
  import { type CapturedBuffer, type CapturedImage, RenderGraph, type RenderView } from './graph.js';
6
- import { type RenderTarget } from './target.js';
6
+ import type { RenderTarget } from './target.js';
7
7
  export declare const Gpu: import("@aethervtt/shard-core").ResourceDef<GpuContext>;
8
8
  export declare const Graph: import("@aethervtt/shard-core").ResourceDef<RenderGraph>;
9
9
  export declare const Shaders: import("@aethervtt/shard-core").ResourceDef<ShaderLibrary>;
@@ -29,16 +29,43 @@ export declare const RenderSet: {
29
29
  readonly Graph: import("@aethervtt/shard-core").SystemSetDef;
30
30
  };
31
31
  export interface RenderPluginOptions {
32
- /** Render into this canvas. Omit for headless (views use offscreen targets). */
32
+ /**
33
+ * Render into this canvas: the plugin adds it as a surface of `gpu` (or of a device it makes).
34
+ * Omit for headless (views use offscreen targets).
35
+ */
33
36
  canvas?: HTMLCanvasElement | OffscreenCanvas;
34
- /** An existing GPU context (e.g. Dawn in Node). Otherwise one is created in `ready`. */
37
+ /**
38
+ * With `canvas`: 'premultiplied' composites the canvas over the page, where cameras clearing to
39
+ * alpha 0 show what's under it (0052). Default 'opaque'.
40
+ */
41
+ alpha?: SurfaceAlpha;
42
+ /** Render into a surface already on `gpu` (`gpu.addSurface`). Disposing the app removes it. */
43
+ surface?: Surface;
44
+ /**
45
+ * An existing GPU context (e.g. Dawn in Node, or one shared with other apps). Otherwise one is
46
+ * created in `ready` and destroyed with the app.
47
+ */
35
48
  gpu?: GpuContext;
49
+ /** The name this app's GPU objects count under in `gpu.stats(owner)`. Default `render:<n>`. */
50
+ owner?: string;
36
51
  /**
37
52
  * Headless stand-in for the window: cameras without a target render here. Used by the CLI for
38
- * screenshots. Ignored when there's a canvas.
53
+ * screenshots. Ignored when there's a canvas or surface.
39
54
  */
40
55
  target?: RenderTarget;
41
56
  features?: GPUFeatureName[];
57
+ /**
58
+ * The graphics API, when the plugin makes the device (0064). 'auto' (default): WebGPU where it
59
+ * works, else WebGL2 on the baseline tier. 'webgpu' or 'webgl2' insist on one.
60
+ */
61
+ backend?: GpuBackendChoice;
62
+ /** WebGL2 settings, for tests and tools (the 'minimum' profile, GL error checks). */
63
+ webgl2?: Webgl2ContextOptions;
64
+ /**
65
+ * 'baseline' runs the baseline tier even where the full one would (on WebGPU, a
66
+ * compatibility-mode device), when the plugin makes the device.
67
+ */
68
+ tier?: 'baseline';
42
69
  /** An existing shader library to share (e.g. a preview rendering the game's own shaders). */
43
70
  shaders?: ShaderLibrary;
44
71
  /**
@@ -57,8 +84,15 @@ export interface RenderPluginOptions {
57
84
  * so the renderer's description grows with what's installed.
58
85
  */
59
86
  export declare const RenderDescribers: import("@aethervtt/shard-core").ResourceDef<Map<string, (world: World) => unknown>>;
60
- export declare const RenderOptions: import("@aethervtt/shard-core").ResourceDef<Required<Pick<RenderPluginOptions, "windowView">>>;
87
+ export interface RenderOptionsValue {
88
+ windowView: boolean;
89
+ /** What this app's GPU objects count under in `gpu.stats` (0052). */
90
+ owner: string;
91
+ }
92
+ export declare const RenderOptions: import("@aethervtt/shard-core").ResourceDef<RenderOptionsValue>;
61
93
  export declare function renderPlugin(options?: RenderPluginOptions): Plugin;
94
+ /** The name this app's GPU objects count under in `gpu.stats` (0052). */
95
+ export declare function renderOwner(world: World): string;
62
96
  /** A screenshot of a view (default: the window) after the next frame renders it. */
63
97
  export declare function captureView(world: World, view?: string): Promise<CapturedImage>;
64
98
  /**
@@ -67,6 +101,11 @@ export declare function captureView(world: World, view?: string): Promise<Captur
67
101
  * perspective, fog, bloom…), `depth`, G-buffer channels, effect buffers.
68
102
  */
69
103
  export declare function captureBuffer(world: World, view: string, buffer: string): Promise<CapturedBuffer>;
104
+ /** Every buffer and texture on the device by category: the ledger's total (0055, 0074). */
105
+ export declare function gpuMemory(world: World): {
106
+ bytes: number;
107
+ byCategory: Record<string, number>;
108
+ };
70
109
  /** What the renderer is doing: graph order, culled nodes, views, pending pipelines. For agents. */
71
110
  export declare function describeRender(world: World): {
72
111
  order: {
@@ -100,7 +139,12 @@ export declare function describeRender(world: World): {
100
139
  taaResets: number;
101
140
  originShifts: number;
102
141
  };
103
- memory: import("./stats.js").GpuMemoryData;
142
+ memory: {
143
+ textures: number;
144
+ textureBytes: number;
145
+ bytes: number;
146
+ byCategory: Record<string, number>;
147
+ };
104
148
  recentErrors: {
105
149
  code: string;
106
150
  message: string;
@@ -108,4 +152,22 @@ export declare function describeRender(world: World): {
108
152
  path?: string;
109
153
  details?: unknown[];
110
154
  }[];
155
+ features: {
156
+ name: string;
157
+ nodes: string[];
158
+ baseline: string;
159
+ }[];
160
+ health: import("./health.js").RenderHealthValue;
161
+ backend: import("@aethervtt/shard-gpu").GpuBackendName;
162
+ tier: import("@aethervtt/shard-gpu").GpuTier;
163
+ capabilities: import("@aethervtt/shard-gpu").GpuCapabilities;
164
+ shaderCache: import("@aethervtt/shard-gpu").ShaderCacheStats | undefined;
165
+ limits: {
166
+ maxTextureDimension2D: number;
167
+ maxColorAttachments: number;
168
+ maxSampledTexturesPerShaderStage: number;
169
+ maxInterStageShaderVariables: number;
170
+ maxUniformBufferBindingSize: number;
171
+ };
172
+ reasons: readonly import("@aethervtt/shard-gpu").GraphicsReason[];
111
173
  } & Record<string, unknown>;