@aethervtt/shard-render 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/assets.d.ts +3 -0
  2. package/dist/assets.js +43 -2
  3. package/dist/atmosphere-nodes.d.ts +9 -0
  4. package/dist/atmosphere-nodes.js +57 -18
  5. package/dist/atmosphere-plugin.js +8 -2
  6. package/dist/baseline/atmosphere.d.ts +16 -0
  7. package/dist/baseline/atmosphere.js +301 -0
  8. package/dist/baseline/data-texture.d.ts +36 -0
  9. package/dist/baseline/data-texture.js +181 -0
  10. package/dist/baseline/environment.d.ts +14 -0
  11. package/dist/baseline/environment.js +368 -0
  12. package/dist/baseline/exposure.d.ts +25 -0
  13. package/dist/baseline/exposure.js +120 -0
  14. package/dist/baseline/lights.d.ts +33 -0
  15. package/dist/baseline/lights.js +133 -0
  16. package/dist/baseline/textures.d.ts +97 -0
  17. package/dist/baseline/textures.js +203 -0
  18. package/dist/camera.d.ts +2 -0
  19. package/dist/camera.js +4 -0
  20. package/dist/culling-shaders.js +21 -2
  21. package/dist/culling.d.ts +11 -2
  22. package/dist/culling.js +44 -18
  23. package/dist/cutaway-plugin.d.ts +48 -0
  24. package/dist/cutaway-plugin.js +364 -0
  25. package/dist/cutaway.d.ts +27 -0
  26. package/dist/cutaway.js +49 -0
  27. package/dist/data-store.d.ts +69 -0
  28. package/dist/data-store.js +105 -0
  29. package/dist/debug-shaders.js +17 -3
  30. package/dist/debug-views.js +4 -2
  31. package/dist/deferred-shaders.js +2 -0
  32. package/dist/deferred.js +24 -3
  33. package/dist/deform.d.ts +5 -4
  34. package/dist/deform.js +4 -9
  35. package/dist/describe-plugin.d.ts +4 -2
  36. package/dist/describe-plugin.js +75 -3
  37. package/dist/display-nodes.js +15 -4
  38. package/dist/dynamic-resolution.d.ts +2 -0
  39. package/dist/dynamic-resolution.js +6 -1
  40. package/dist/environment-plugin.js +17 -2
  41. package/dist/environment-state.d.ts +7 -3
  42. package/dist/environment-state.js +20 -9
  43. package/dist/environment.js +7 -1
  44. package/dist/features.d.ts +49 -0
  45. package/dist/features.js +106 -0
  46. package/dist/forward.d.ts +38 -2
  47. package/dist/forward.js +582 -184
  48. package/dist/fxaa.js +12 -2
  49. package/dist/gizmos-plugin.js +7 -0
  50. package/dist/gizmos.d.ts +3 -3
  51. package/dist/gizmos.js +11 -13
  52. package/dist/gpu-assets.d.ts +58 -4
  53. package/dist/gpu-assets.js +191 -8
  54. package/dist/graph.d.ts +11 -0
  55. package/dist/graph.js +30 -5
  56. package/dist/health.d.ts +58 -0
  57. package/dist/health.js +112 -0
  58. package/dist/index.d.ts +30 -11
  59. package/dist/index.js +286 -9
  60. package/dist/instances.d.ts +119 -8
  61. package/dist/instances.js +387 -78
  62. package/dist/interior-plugin.d.ts +121 -0
  63. package/dist/interior-plugin.js +465 -0
  64. package/dist/interior.d.ts +24 -0
  65. package/dist/interior.js +13 -0
  66. package/dist/layers.d.ts +35 -0
  67. package/dist/layers.js +49 -0
  68. package/dist/lens-plugin.d.ts +43 -0
  69. package/dist/lens-plugin.js +304 -0
  70. package/dist/lens.d.ts +41 -0
  71. package/dist/lens.js +99 -0
  72. package/dist/lights.d.ts +50 -5
  73. package/dist/lights.js +101 -22
  74. package/dist/material-pipelines.d.ts +37 -4
  75. package/dist/material-pipelines.js +102 -5
  76. package/dist/materials.d.ts +23 -0
  77. package/dist/materials.js +44 -1
  78. package/dist/outline-plugin.d.ts +48 -0
  79. package/dist/outline-plugin.js +529 -0
  80. package/dist/outline.d.ts +15 -0
  81. package/dist/outline.js +42 -0
  82. package/dist/overlays.js +1 -0
  83. package/dist/picking.d.ts +17 -0
  84. package/dist/picking.js +40 -11
  85. package/dist/pixel-perfect-plugin.js +10 -2
  86. package/dist/plugin.d.ts +58 -6
  87. package/dist/plugin.js +191 -24
  88. package/dist/post-common.d.ts +2 -0
  89. package/dist/post-common.js +3 -0
  90. package/dist/post-nodes.d.ts +2 -1
  91. package/dist/post-nodes.js +111 -19
  92. package/dist/post-plugin.js +5 -3
  93. package/dist/post-shaders.js +51 -6
  94. package/dist/projection.d.ts +24 -0
  95. package/dist/projection.js +87 -0
  96. package/dist/readback.d.ts +1 -0
  97. package/dist/readback.js +1 -0
  98. package/dist/render-scale.js +1 -0
  99. package/dist/screen-effects.d.ts +97 -0
  100. package/dist/screen-effects.js +192 -0
  101. package/dist/shaders.d.ts +13 -0
  102. package/dist/shaders.js +131 -29
  103. package/dist/shadow-catcher.d.ts +9 -0
  104. package/dist/shadow-catcher.js +133 -0
  105. package/dist/shadows.d.ts +25 -1
  106. package/dist/shadows.js +97 -19
  107. package/dist/standard-fields.d.ts +1 -0
  108. package/dist/standard-fields.js +5 -0
  109. package/dist/standard.js +12 -0
  110. package/dist/stats.d.ts +45 -1
  111. package/dist/stats.js +91 -3
  112. package/dist/surface-model.d.ts +57 -0
  113. package/dist/surface-model.js +289 -0
  114. package/dist/surface-shaders.d.ts +3 -0
  115. package/dist/surface-shaders.js +160 -0
  116. package/dist/surface.d.ts +40 -0
  117. package/dist/surface.js +86 -0
  118. package/dist/target.d.ts +1 -13
  119. package/dist/target.js +1 -26
  120. package/dist/testing.d.ts +10 -0
  121. package/dist/testing.js +33 -1
  122. package/dist/tier.d.ts +13 -0
  123. package/dist/tier.js +14 -0
  124. package/dist/timer.d.ts +2 -0
  125. package/dist/timer.js +7 -1
  126. package/dist/variants.d.ts +37 -0
  127. package/dist/variants.js +104 -0
  128. package/dist/view-visibility-plugin.d.ts +61 -0
  129. package/dist/view-visibility-plugin.js +267 -0
  130. package/dist/view-visibility.d.ts +28 -0
  131. package/dist/view-visibility.js +46 -0
  132. package/dist/view.d.ts +29 -1
  133. package/dist/view.js +57 -34
  134. package/package.json +15 -11
package/dist/materials.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  defineComponent,
3
3
  defineSchema,
4
+ isRedefinable,
4
5
  ShardError
5
6
  } from "@aethervtt/shard-core";
6
7
  import { wgslLayout } from "@aethervtt/shard-shader";
@@ -30,6 +31,12 @@ class MaterialType {
30
31
  textures;
31
32
  /** Texture fields bound as 2D arrays. */
32
33
  arrays;
34
+ /** Texture fields sampled through the sRGB view. */
35
+ colors;
36
+ /** Whether picks can hit draws of this type (MaterialTypeOptions.pickable). */
37
+ pickable = true;
38
+ /** The plugin that registers its shader (MaterialTypeOptions.plugin). */
39
+ plugin;
33
40
  blend;
34
41
  shader;
35
42
  /** Noise graphs the module wraps as `noise_<name>`. */
@@ -37,6 +44,8 @@ class MaterialType {
37
44
  description;
38
45
  /** Increments on every redefinition, so GPU state and pipelines rebuild. */
39
46
  version = 0;
47
+ /** Its definition as a string: defining the name again with an equal one is a no-op (0052). */
48
+ signature = "";
40
49
  modulePath;
41
50
  /** WGSL identifier of the uniform (the PascalName). */
42
51
  varName;
@@ -50,6 +59,7 @@ class MaterialType {
50
59
  this.layout = void 0;
51
60
  this.textures = [];
52
61
  this.arrays = /* @__PURE__ */ new Set();
62
+ this.colors = /* @__PURE__ */ new Set();
53
63
  this.blend = options.blend;
54
64
  this.shader = options.shader;
55
65
  this.noise = [];
@@ -63,13 +73,17 @@ class MaterialType {
63
73
  const textures = [];
64
74
  for (const [key, field] of Object.entries(fields)) {
65
75
  if (isTextureHandle(field)) textures.push(key);
66
- else if (field.storage !== "object") numeric[key] = field;
76
+ else if (field.storage !== "object" || field.kind === "struct" && field.options.wgsl)
77
+ numeric[key] = field;
67
78
  }
68
79
  this.extends = options.extends ?? "standard";
69
80
  this.standardTextures = options.standardTextures ?? true;
81
+ this.pickable = options.pickable ?? true;
82
+ this.plugin = options.plugin;
70
83
  this.schema = schema;
71
84
  this.textures = textures;
72
85
  this.arrays = new Set(options.arrays ?? []);
86
+ this.colors = new Set(options.colors ?? []);
73
87
  this.blend = options.blend;
74
88
  this.shader = options.shader;
75
89
  this.noise = Object.entries(options.noise ?? {}).map(([name, path]) => {
@@ -114,6 +128,7 @@ class MaterialType {
114
128
  const lines = [];
115
129
  let binding = this.bindingBase;
116
130
  if (this.layout) {
131
+ for (const path of this.layout.imports) lines.push(`import ${path};`);
117
132
  lines.push(this.layout.wgsl);
118
133
  lines.push(
119
134
  `@group(1) @binding(${binding++}) var<uniform> ${this.varName}: ${this.layout.structName};`
@@ -178,19 +193,47 @@ function defineMaterial(name, options, standardFields) {
178
193
  ...options.fields ?? {}
179
194
  };
180
195
  const existing = types.get(name);
196
+ const signature = signatureOf(ext, options, fields);
197
+ if (existing && !isRedefinable(name)) {
198
+ if (existing.signature === signature) return existing;
199
+ throw new ShardError(
200
+ "render/registry-conflict",
201
+ `Material type ${name} is already defined, differently`,
202
+ {
203
+ hint: "Two definitions (two apps or bundles) share the name. Rename one, or share the definition."
204
+ }
205
+ );
206
+ }
181
207
  const schema = defineComponent(name, fields, {
182
208
  description: options.description ?? `Material type ${name}${ext === "standard" ? " (extends the standard material)" : ""}.`,
183
209
  serialize: true
184
210
  });
185
211
  if (existing) {
186
212
  existing.assign(options, schema);
213
+ existing.signature = signature;
187
214
  for (const listener of listeners) listener(existing);
188
215
  return existing;
189
216
  }
190
217
  const type = new MaterialType(name, options, schema);
218
+ type.signature = signature;
191
219
  types.set(name, type);
192
220
  return type;
193
221
  }
222
+ function signatureOf(ext, options, fields) {
223
+ return JSON.stringify({
224
+ ext,
225
+ blend: options.blend ?? null,
226
+ shader: options.shader ?? null,
227
+ noise: options.noise ?? {},
228
+ arrays: options.arrays ?? [],
229
+ colors: options.colors ?? [],
230
+ standardTextures: options.standardTextures ?? true,
231
+ pickable: options.pickable ?? true,
232
+ plugin: options.plugin ?? null,
233
+ description: options.description ?? null,
234
+ fields: Object.entries(fields).map(([key, field]) => [key, field.jsonSchema()])
235
+ });
236
+ }
194
237
  function findMaterialType(name) {
195
238
  return types.get(name);
196
239
  }
@@ -0,0 +1,48 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { type DrawList } from './instances.js';
3
+ export declare const OUTLINE_SHADERS: Record<string, string>;
4
+ /** One outline style in a view: what an Outline draws. */
5
+ interface Style {
6
+ color: [number, number, number, number];
7
+ width: number;
8
+ occluded: number;
9
+ }
10
+ /** A view's outlines this frame: the mask's draws and the styles they use. */
11
+ export interface OutlineView {
12
+ view: string;
13
+ camera: Entity;
14
+ draws: DrawList;
15
+ styles: Style[];
16
+ /** Outlined entities (renderables) drawn. */
17
+ count: number;
18
+ /** The style uniforms: colors, then params, then the scale. */
19
+ data: Float32Array;
20
+ textures: OutlineTextures | undefined;
21
+ }
22
+ interface OutlineTextures {
23
+ generation: number;
24
+ width: number;
25
+ height: number;
26
+ mask: GPUTexture;
27
+ depth: GPUTexture;
28
+ a: GPUTexture;
29
+ b: GPUTexture;
30
+ }
31
+ export declare const OutlineViews: import("@aethervtt/shard-core").ResourceDef<Map<string, OutlineView>>;
32
+ /**
33
+ * Gathers each camera view's outlined renderables into its mask draws, after the forward queue
34
+ * (so this frame's visible lists are open) and before the upload.
35
+ */
36
+ export declare const queueOutlines: import("@aethervtt/shard-core").SystemDef<{
37
+ q: import("@aethervtt/shard-core").Query;
38
+ seen: Set<number>;
39
+ live: Set<string>;
40
+ }>;
41
+ /** render.describe's outlines section: per view, its styles and how many renderables they cover. */
42
+ export declare function describeOutlines(world: World): Record<string, unknown>;
43
+ /**
44
+ * Selection and hover outlines (0057): a mask of the outlined renderables, jump flood, and a ring
45
+ * composited on the view target. Not in the graph while no Outline is in view.
46
+ */
47
+ export declare const outlinePlugin: import("@aethervtt/shard-runtime").Plugin;
48
+ export {};
@@ -0,0 +1,529 @@
1
+ import {
2
+ Children,
3
+ defineResource,
4
+ defineSystem,
5
+ Last
6
+ } from "@aethervtt/shard-core";
7
+ import { definePlugin } from "@aethervtt/shard-runtime";
8
+ import { addRenderFeatures } from "./features.js";
9
+ import { ForwardStateResource, forwardQueue, VERTEX_BUFFERS, viewBindGroup } from "./forward.js";
10
+ import { GpuAssetsResource } from "./gpu-assets.js";
11
+ import { RenderPhase } from "./graph.js";
12
+ import {
13
+ createDrawList,
14
+ INSTANCE_FLOATS,
15
+ InstanceFlags,
16
+ InstanceSlot,
17
+ Instances
18
+ } from "./instances.js";
19
+ import { MAX_OUTLINE_STYLES, OUTLINE_OCCLUSION, Outline, OutlinePath } from "./outline.js";
20
+ import { Graph, RenderDescribers, RenderSet, Shaders, Views } from "./plugin.js";
21
+ import { beginPass, depthTex, idOf, PostCache, tex, uniform } from "./post-common.js";
22
+ import { registerShaders } from "./shaders.js";
23
+ import { cameraOf } from "./view.js";
24
+ const OUTLINE_SHADERS = {
25
+ "shard::outline::mask": `
26
+ import shard::view::view;
27
+ import shard::mesh::{ mesh_vertex_at, visible };
28
+
29
+ struct MaskVertex {
30
+ @builtin(position) clip: vec4f,
31
+ @location(0) @interpolate(flat) style: u32,
32
+ }
33
+
34
+ @vertex fn vs(
35
+ @builtin(instance_index) instance_index: u32,
36
+ @builtin(vertex_index) vertex_index: u32,
37
+ @location(0) position: vec3f,
38
+ @location(1) normal: vec3f,
39
+ @location(2) uv: vec2f,
40
+ @location(3) uv1: vec2f,
41
+ @location(4) tangent: vec4f,
42
+ ) -> MaskVertex {
43
+ let m = mesh_vertex_at(instance_index, vertex_index, position, normal, uv, uv1, tangent);
44
+ var out: MaskVertex;
45
+ out.clip = view.viewProjNoJitter * vec4f(m.world_position, 1.0);
46
+ // The pass packs each slot's style into its visible entry's top 4 bits.
47
+ out.style = visible[instance_index] >> 28u;
48
+ return out;
49
+ }
50
+
51
+ /** Style + 1 (0 where nothing is outlined), and the surface's reversed-Z depth. */
52
+ @fragment fn fs(in: MaskVertex) -> @location(0) vec4f {
53
+ return vec4f(f32(in.style) + 1.0, in.clip.z, 0.0, 0.0);
54
+ }`,
55
+ "shard::outline::post": `
56
+ import shard::color::linear_to_srgb;
57
+
58
+ struct Styles {
59
+ /** Per style: color (linear, straight alpha). */
60
+ colors: array<vec4f, ${MAX_OUTLINE_STYLES}>,
61
+ /** Per style: width in target pixels (x), occluded parts: 0 hide, 1 show, 2 dim (y). */
62
+ params: array<vec4f, ${MAX_OUTLINE_STYLES}>,
63
+ /** Render size over target size (xy), to read the scene's depth. */
64
+ scale: vec4f,
65
+ }
66
+
67
+ @group(0) @binding(0) var<uniform> styles: Styles;
68
+ @group(0) @binding(1) var source: texture_2d<f32>;
69
+ @group(0) @binding(2) var scene_depth: texture_depth_2d;
70
+ @group(0) @binding(3) var<uniform> step: vec4f;
71
+ @group(0) @binding(4) var mask: texture_2d<f32>;
72
+
73
+ const NONE = vec4f(-1.0e9, -1.0e9, 0.0, 0.0);
74
+
75
+ /**
76
+ * Seeds: every outlined pixel, as (x, y, style + 1, visible). Parts something hides seed only
77
+ * when their style shows or dims them.
78
+ */
79
+ @fragment fn seed(@builtin(position) frag: vec4f) -> @location(0) vec4f {
80
+ let p = vec2i(frag.xy);
81
+ let m = textureLoad(source, p, 0);
82
+ if (m.x < 0.5) { return NONE; }
83
+ let style = u32(m.x) - 1u;
84
+ let size = vec2i(textureDimensions(scene_depth));
85
+ let q = clamp(vec2i(frag.xy * styles.scale.xy), vec2i(0), size - 1);
86
+ let scene = textureLoad(scene_depth, q, 0);
87
+ // Reversed Z: nearer is larger. The mask draws unjittered, the scene may not: a little slack.
88
+ let seen = m.y >= scene * 0.998 - 1e-7;
89
+ if (!seen && styles.params[style].y < 0.5) { return NONE; }
90
+ return vec4f(frag.xy, m.x, select(0.0, 1.0, seen));
91
+ }
92
+
93
+ /** One jump flood step: the nearest seed among the 3\xD73 neighbours \`step.x\` pixels apart. */
94
+ @fragment fn flood(@builtin(position) frag: vec4f) -> @location(0) vec4f {
95
+ let size = vec2i(textureDimensions(source));
96
+ let s = i32(step.x);
97
+ var best = NONE;
98
+ var best_d = 1.0e20;
99
+ for (var y = -1; y <= 1; y++) {
100
+ for (var x = -1; x <= 1; x++) {
101
+ let q = vec2i(frag.xy) + vec2i(x, y) * s;
102
+ if (any(q < vec2i(0)) || any(q >= size)) { continue; }
103
+ let c = textureLoad(source, q, 0);
104
+ if (c.z < 0.5) { continue; }
105
+ let d = distance(frag.xy, c.xy);
106
+ if (d < best_d) {
107
+ best_d = d;
108
+ best = c;
109
+ }
110
+ }
111
+ }
112
+ return best;
113
+ }
114
+
115
+ /** The ring: pixels outside every silhouette within their nearest seed's width. */
116
+ @fragment fn composite(@builtin(position) frag: vec4f) -> @location(0) vec4f {
117
+ let p = vec2i(frag.xy);
118
+ if (textureLoad(mask, p, 0).x > 0.5) { discard; }
119
+ let c = textureLoad(source, p, 0);
120
+ if (c.z < 0.5) { discard; }
121
+ let style = u32(c.z) - 1u;
122
+ let params = styles.params[style];
123
+ let d = distance(frag.xy, c.xy);
124
+ var a = clamp(params.x + 0.5 - d, 0.0, 1.0);
125
+ if (c.w < 0.5 && params.y > 1.5) { a *= 0.35; }
126
+ let color = styles.colors[style];
127
+ a *= color.a;
128
+ if (a <= 0.0) { discard; }
129
+ var rgb = color.rgb;
130
+ @if(!SRGB_TARGET) { rgb = linear_to_srgb(rgb); }
131
+ return vec4f(rgb * a, a);
132
+ }`
133
+ };
134
+ const OutlineViews = defineResource("render/OutlineViews", {
135
+ description: "Each view's outlines this frame (outline pass, 0057).",
136
+ init: () => /* @__PURE__ */ new Map()
137
+ });
138
+ const STYLE_FLOATS = MAX_OUTLINE_STYLES * 8 + 4;
139
+ let entries = new Uint32Array(64);
140
+ let entryBatches = new Int32Array(64);
141
+ let entryCount = 0;
142
+ function push(slot, style, batch) {
143
+ if (entryCount >= entries.length) {
144
+ const e = new Uint32Array(entries.length * 2);
145
+ e.set(entries);
146
+ entries = e;
147
+ const b = new Int32Array(entryBatches.length * 2);
148
+ b.set(entryBatches);
149
+ entryBatches = b;
150
+ }
151
+ entries[entryCount] = (slot | style << 28) >>> 0;
152
+ entryBatches[entryCount] = batch;
153
+ entryCount++;
154
+ }
155
+ function gather(world, e, style, layers, seen) {
156
+ if (seen.has(e) || !world.isAlive(e)) return;
157
+ seen.add(e);
158
+ const store = world.resource(Instances);
159
+ const slotData = world.tryGet(e, InstanceSlot);
160
+ const slot = slotData ? slotData.slot - 1 : -1;
161
+ if (slot >= 0) {
162
+ const flags = store.u32[slot * INSTANCE_FLOATS + 13];
163
+ const a = store.batchOf[slot];
164
+ const visible = (flags & InstanceFlags.Visible) !== 0;
165
+ if (visible && a !== -1 && (store.layers[slot] & layers) !== 0) {
166
+ const batch = a >= 0 ? a : store.lodSets[-2 - a].batches[0];
167
+ push(slot, style, batch);
168
+ }
169
+ }
170
+ const children = world.tryGet(e, Children);
171
+ if (children) {
172
+ for (const child of children.entities)
173
+ if (child !== null) gather(world, child, style, layers, seen);
174
+ }
175
+ }
176
+ const queueOutlines = defineSystem({
177
+ name: "render/queue-outlines",
178
+ description: "Collects outlined renderables per view for the outline pass (0057).",
179
+ setup: (world) => ({
180
+ q: world.query({ with: [Outline] }),
181
+ seen: /* @__PURE__ */ new Set(),
182
+ live: /* @__PURE__ */ new Set()
183
+ }),
184
+ run: ({ q, seen, live }, world) => {
185
+ const views = world.resource(OutlineViews);
186
+ let any = 0;
187
+ for (const t of q.tables) any += t.count;
188
+ if (any === 0) {
189
+ if (views.size > 0) {
190
+ for (const v of views.values()) v.textures = destroyTextures(v.textures);
191
+ views.clear();
192
+ }
193
+ return;
194
+ }
195
+ const store = world.resource(Instances);
196
+ live.clear();
197
+ for (const view of world.resource(Views).list) {
198
+ const cam = cameraOf(view);
199
+ if (!cam) continue;
200
+ let ov = views.get(view.name);
201
+ if (!ov) {
202
+ ov = {
203
+ view: view.name,
204
+ camera: cam.entity,
205
+ draws: createDrawList(),
206
+ styles: [],
207
+ count: 0,
208
+ data: new Float32Array(STYLE_FLOATS),
209
+ textures: void 0
210
+ };
211
+ views.set(view.name, ov);
212
+ }
213
+ live.add(view.name);
214
+ ov.styles.length = 0;
215
+ entryCount = 0;
216
+ seen.clear();
217
+ for (const table of q.tables) {
218
+ for (let row = 0; row < table.count; row++) {
219
+ const e = table.entities[row];
220
+ const o = world.get(e, Outline);
221
+ let style = ov.styles.findIndex(
222
+ (s) => s.width === o.width && s.occluded === OUTLINE_OCCLUSION.indexOf(o.occluded) && s.color.every((c, i) => c === o.color[i])
223
+ );
224
+ if (style < 0) {
225
+ if (ov.styles.length >= MAX_OUTLINE_STYLES) style = MAX_OUTLINE_STYLES - 1;
226
+ else {
227
+ style = ov.styles.length;
228
+ ov.styles.push({
229
+ color: o.color,
230
+ width: o.width,
231
+ occluded: OUTLINE_OCCLUSION.indexOf(o.occluded)
232
+ });
233
+ }
234
+ }
235
+ gather(world, e, style, cam.layers, seen);
236
+ }
237
+ }
238
+ ov.count = entryCount;
239
+ store.packEntries(ov.draws, entries, entryBatches, entryCount);
240
+ const d = ov.data;
241
+ d.fill(0);
242
+ for (let i = 0; i < ov.styles.length; i++) {
243
+ const s = ov.styles[i];
244
+ d.set(s.color, i * 4);
245
+ d[MAX_OUTLINE_STYLES * 4 + i * 4] = s.width * cam.pixelRatio;
246
+ d[MAX_OUTLINE_STYLES * 4 + i * 4 + 1] = s.occluded;
247
+ }
248
+ d[MAX_OUTLINE_STYLES * 8] = cam.width / Math.max(1, cam.displayWidth);
249
+ d[MAX_OUTLINE_STYLES * 8 + 1] = cam.height / Math.max(1, cam.displayHeight);
250
+ }
251
+ for (const [name, v] of views) {
252
+ if (!live.has(name)) {
253
+ v.textures = destroyTextures(v.textures);
254
+ views.delete(name);
255
+ }
256
+ }
257
+ }
258
+ });
259
+ function destroyTextures(t) {
260
+ if (t) for (const x of [t.mask, t.depth, t.a, t.b]) x.destroy();
261
+ return void 0;
262
+ }
263
+ function textures(gpu, ov, width, height) {
264
+ const t = ov.textures;
265
+ if (t && t.generation === gpu.generation && t.width === width && t.height === height) return t;
266
+ destroyTextures(t);
267
+ const make = (label, format) => gpu.device.createTexture({
268
+ label: `${ov.view}/outline/${label}`,
269
+ size: [width, height],
270
+ format,
271
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
272
+ });
273
+ ov.textures = {
274
+ generation: gpu.generation,
275
+ width,
276
+ height,
277
+ mask: make("mask", "rg32float"),
278
+ depth: make("depth", "depth32float"),
279
+ a: make("flood-a", "rgba32float"),
280
+ b: make("flood-b", "rgba32float")
281
+ };
282
+ return ov.textures;
283
+ }
284
+ function maskPipeline(world, gpu, cache) {
285
+ const state = world.resource(ForwardStateResource);
286
+ const key = gpu.generation * 2 + (state.interiorMode !== 0 ? 1 : 0);
287
+ const cached = cache.get(key);
288
+ if (cached) return cached;
289
+ const module = world.resource(Shaders).module(gpu, { root: "shard::outline::mask" });
290
+ if (!module) {
291
+ gpu.pipelines.skipped++;
292
+ return void 0;
293
+ }
294
+ const store = world.resource(Instances);
295
+ const empty = gpu.layouts.bindGroupLayout({ label: "outline/empty", entries: [] });
296
+ const pipeline = gpu.pipelines.render({
297
+ label: "outline/mask",
298
+ layout: gpu.layouts.pipelineLayout({
299
+ label: "outline/mask",
300
+ bindGroupLayouts: [state.layouts.view, empty, store.layout]
301
+ }),
302
+ vertex: { module, entryPoint: "vs", buffers: VERTEX_BUFFERS },
303
+ fragment: { module, entryPoint: "fs", targets: [{ format: "rg32float" }] },
304
+ primitive: { topology: "triangle-list", cullMode: "none" },
305
+ depthStencil: { format: "depth32float", depthWriteEnabled: true, depthCompare: "greater" }
306
+ });
307
+ if (pipeline) cache.set(key, pipeline);
308
+ return pipeline;
309
+ }
310
+ const PREMULTIPLIED = {
311
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
312
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" }
313
+ };
314
+ const stepData = new Float32Array(4);
315
+ const stepSizes = [];
316
+ const stepWritten = [];
317
+ const stepBuffers = [];
318
+ function outlineNode(world) {
319
+ const views = world.resource(OutlineViews);
320
+ const cache = new PostCache();
321
+ const masks = /* @__PURE__ */ new Map();
322
+ let layout;
323
+ let layoutGen = -1;
324
+ const empty = /* @__PURE__ */ new Map();
325
+ return {
326
+ kind: "raw",
327
+ phase: RenderPhase.Display + 30,
328
+ enabled: (view) => (views.get(view.name)?.draws.length ?? 0) > 0,
329
+ reads: ["depth"],
330
+ writes: ["view-target"],
331
+ run: (ctx) => {
332
+ const gpu = ctx.gpu;
333
+ const ov = views.get(ctx.view.name);
334
+ const cam = cameraOf(ctx.view);
335
+ const state = ctx.world.resource(ForwardStateResource);
336
+ const pv = state.views.get(ctx.view.name);
337
+ const store = ctx.world.resource(Instances);
338
+ const assets = ctx.world.resource(GpuAssetsResource);
339
+ if (!pv || !store.bindGroup) return;
340
+ const target = ctx.texture("view-target");
341
+ const t = textures(gpu, ov, target.width, target.height);
342
+ const mask = maskPipeline(ctx.world, gpu, masks);
343
+ if (!layout || layoutGen !== gpu.generation) {
344
+ layoutGen = gpu.generation;
345
+ layout = gpu.layouts.bindGroupLayout({
346
+ label: "outline",
347
+ entries: [
348
+ uniform(0),
349
+ tex(1, "unfilterable-float"),
350
+ depthTex(gpu, 2),
351
+ uniform(3),
352
+ tex(4, "unfilterable-float")
353
+ ]
354
+ });
355
+ }
356
+ const srgb = target.format.endsWith("-srgb");
357
+ const seed = cache.render(
358
+ ctx,
359
+ "outline/seed",
360
+ "shard::outline::post",
361
+ "seed",
362
+ [layout],
363
+ [{ format: "rgba32float" }],
364
+ { SRGB_TARGET: srgb }
365
+ );
366
+ const flood = cache.render(
367
+ ctx,
368
+ "outline/flood",
369
+ "shard::outline::post",
370
+ "flood",
371
+ [layout],
372
+ [{ format: "rgba32float" }],
373
+ { SRGB_TARGET: srgb }
374
+ );
375
+ const composite = cache.render(
376
+ ctx,
377
+ `outline/composite/${target.format}`,
378
+ "shard::outline::post",
379
+ "composite",
380
+ [layout],
381
+ [{ format: target.format, blend: PREMULTIPLIED }],
382
+ { SRGB_TARGET: srgb }
383
+ );
384
+ if (!mask || !seed || !flood || !composite) return;
385
+ const pass = ctx.encoder.beginRenderPass({
386
+ label: `${ctx.view.name}/outline/mask`,
387
+ colorAttachments: [
388
+ {
389
+ view: t.mask.createView(),
390
+ loadOp: "clear",
391
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
392
+ storeOp: "store"
393
+ }
394
+ ],
395
+ depthStencilAttachment: {
396
+ view: t.depth.createView(),
397
+ depthLoadOp: "clear",
398
+ depthClearValue: 0,
399
+ depthStoreOp: "discard"
400
+ }
401
+ });
402
+ pass.setPipeline(mask);
403
+ pass.setBindGroup(0, viewBindGroup(gpu, ctx.world, pv, cam));
404
+ let group1 = empty.get(gpu.generation);
405
+ if (!group1) {
406
+ group1 = gpu.device.createBindGroup({
407
+ label: "outline/empty",
408
+ layout: gpu.layouts.bindGroupLayout({ label: "outline/empty", entries: [] }),
409
+ entries: []
410
+ });
411
+ empty.clear();
412
+ empty.set(gpu.generation, group1);
413
+ }
414
+ pass.setBindGroup(1, group1);
415
+ pass.setBindGroup(2, store.bindGroup);
416
+ const draws = ov.draws;
417
+ for (let d = 0; d < draws.length; d++) {
418
+ const item = draws.items[d];
419
+ const gm = assets.mesh(item.batch.mesh);
420
+ pass.setVertexBuffer(0, gm.positions);
421
+ pass.setVertexBuffer(1, gm.normals);
422
+ pass.setVertexBuffer(2, gm.uvs);
423
+ pass.setVertexBuffer(3, gm.uvs1);
424
+ pass.setVertexBuffer(4, gm.tangents);
425
+ if (gm.indices) {
426
+ pass.setIndexBuffer(gm.indices, gm.indexFormat);
427
+ pass.drawIndexed(gm.count, item.count, 0, gm.baseVertex, item.first);
428
+ } else pass.draw(gm.count, item.count, 0, item.first);
429
+ }
430
+ pass.end();
431
+ const styles = cache.buffer(gpu, `${ctx.view.name}/outline/styles`, ov.data.byteLength);
432
+ styles.write(ov.data);
433
+ const depth = ctx.texture("depth");
434
+ let widest = 1;
435
+ for (const s of ov.styles) widest = Math.max(widest, s.width * cam.pixelRatio + 1);
436
+ let step = 1;
437
+ while (step * 2 <= widest) step *= 2;
438
+ let steps = 0;
439
+ for (let s = step; s >= 1; s /= 2) {
440
+ const b = cache.buffer(gpu, `outline/step${steps}`, 16);
441
+ if (b.version === 0 || stepSizes[steps] !== s || stepWritten[steps] !== b) {
442
+ stepData[0] = s;
443
+ b.write(stepData);
444
+ stepSizes[steps] = s;
445
+ stepWritten[steps] = b;
446
+ }
447
+ stepBuffers[steps++] = b;
448
+ }
449
+ const bind = (slot, source, stepBuffer) => cache.group(
450
+ gpu,
451
+ `${ctx.view.name}/${slot}`,
452
+ `${idOf(source)}/${idOf(depth)}/${idOf(t.mask)}/${styles.version}/${idOf(stepBuffer)}`,
453
+ layout,
454
+ () => [
455
+ { binding: 0, resource: { buffer: styles.buffer } },
456
+ { binding: 1, resource: source.createView() },
457
+ { binding: 2, resource: depth.createView({ aspect: "depth-only" }) },
458
+ { binding: 3, resource: { buffer: stepBuffer } },
459
+ { binding: 4, resource: t.mask.createView() }
460
+ ]
461
+ );
462
+ let from = t.a;
463
+ let to = t.b;
464
+ const seedPass = beginPass(ctx, "outline/seed", t.a.createView());
465
+ seedPass.setPipeline(seed);
466
+ seedPass.setBindGroup(0, bind("outline/seed", t.mask, stepBuffers[0].buffer));
467
+ seedPass.draw(3);
468
+ seedPass.end();
469
+ for (let i = 0; i < steps; i++) {
470
+ const p = beginPass(ctx, "outline/flood", to.createView());
471
+ p.setPipeline(flood);
472
+ p.setBindGroup(0, bind(`outline/flood${i}`, from, stepBuffers[i].buffer));
473
+ p.draw(3);
474
+ p.end();
475
+ const x = from;
476
+ from = to;
477
+ to = x;
478
+ }
479
+ const c = beginPass(ctx, "outline/composite", target.createView(), true);
480
+ c.setPipeline(composite);
481
+ c.setBindGroup(0, bind("outline/composite", from, stepBuffers[0].buffer));
482
+ c.draw(3);
483
+ c.end();
484
+ }
485
+ };
486
+ }
487
+ function describeOutlines(world) {
488
+ const out = {};
489
+ for (const [name, v] of world.resource(OutlineViews)) {
490
+ out[name] = {
491
+ renderables: v.count,
492
+ draws: v.draws.length,
493
+ styles: v.styles.map((s) => ({
494
+ color: [...s.color],
495
+ width: s.width,
496
+ occluded: OUTLINE_OCCLUSION[s.occluded]
497
+ }))
498
+ };
499
+ }
500
+ return out;
501
+ }
502
+ const outlinePlugin = definePlugin({
503
+ name: "render/outline",
504
+ dependencies: ["render/forward"],
505
+ provides: [OutlineViews],
506
+ build(app) {
507
+ app.insertResource(OutlinePath, { installed: true });
508
+ app.world.initResource(OutlineViews);
509
+ app.addSystems(Last, queueOutlines.inSet(RenderSet.Queue).after(forwardQueue));
510
+ },
511
+ ready(app) {
512
+ registerShaders(app.world.resource(Shaders), OUTLINE_SHADERS);
513
+ app.world.initResource(RenderDescribers).set("outlines", describeOutlines);
514
+ addRenderFeatures(app.world, {
515
+ name: "render/outline",
516
+ description: "Selection outlines (mask and jump flood).",
517
+ nodes: ["post/outline"],
518
+ baseline: { strategy: "The same fragment passes" }
519
+ });
520
+ app.world.resource(Graph).addNode("post/outline", outlineNode(app.world));
521
+ }
522
+ });
523
+ export {
524
+ OUTLINE_SHADERS,
525
+ OutlineViews,
526
+ describeOutlines,
527
+ outlinePlugin,
528
+ queueOutlines
529
+ };
@@ -0,0 +1,15 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ export declare const OUTLINE_OCCLUSION: readonly ['hide', 'show', 'dim'];
3
+ export declare const Outline: import("@aethervtt/shard-core").ComponentDef<{
4
+ readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
5
+ readonly width: import("@aethervtt/shard-core").FieldType<number, "f32">;
6
+ readonly occluded: import("@aethervtt/shard-core").FieldType<"dim" | "hide" | "show", "u8">;
7
+ }>;
8
+ /** Set by `outlinePlugin`: Outlines draw. */
9
+ export declare const OutlinePath: import("@aethervtt/shard-core").ResourceDef<{
10
+ installed: true;
11
+ }>;
12
+ /** Outline styles a view can draw at once (they ride in the visible entries' top 4 bits). */
13
+ export declare const MAX_OUTLINE_STYLES = 16;
14
+ /** Warns once per world about an Outline without the outline pass (render/feature-missing). */
15
+ export declare function observeOutlinesWithoutPass(world: World): void;