@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
@@ -0,0 +1,97 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { GpuContext } from '@aethervtt/shard-gpu';
3
+ import { Texture } from '@aethervtt/shard-texture';
4
+ import type { GpuTexture } from '../gpu-assets.js';
5
+ /** What a baseline texture asks of the asset layer that owns it. */
6
+ export interface TwinHost {
7
+ readonly gpu: GpuContext;
8
+ /** Reload the texture's artifact (its bytes come back, and its version bumps). */
9
+ reload(texture: Texture): void;
10
+ /** The loading fallback's view of this kind, shown while a twin waits. */
11
+ fallback(srgb: boolean, array: boolean): GPUTextureView;
12
+ /** A twin started waiting for its bytes (`ready` false), or was made (`ready` true). */
13
+ twin(texture: Texture, kind: string, ready: boolean): void;
14
+ /** Development builds: the texture's usage doesn't match the slot reading it. */
15
+ mismatch(texture: Texture, kind: string): void;
16
+ }
17
+ /** A twin that waited for its bytes: which texture and view, and how long it took. */
18
+ export interface TwinWait {
19
+ texture: string;
20
+ kind: string;
21
+ since: number;
22
+ ms: number | undefined;
23
+ }
24
+ /** What the asset layer lends its baseline textures. */
25
+ export interface TwinSource {
26
+ readonly gpu: GpuContext;
27
+ readonly world: World | undefined;
28
+ /** The asset guid of an imported texture. */
29
+ guidOf(texture: Texture): string | undefined;
30
+ /** Reloads the texture's artifact, once at a time. */
31
+ reload(texture: Texture): void;
32
+ /** The loading fallback: a solid white texture. */
33
+ white(): GpuTexture;
34
+ }
35
+ /**
36
+ * The twin bookkeeping of one asset layer: reloads, the loading fallback, the waits
37
+ * `render.describe` lists, and the dev-build `render/texture-color-space-mismatch` log.
38
+ */
39
+ export declare class Twins implements TwinHost {
40
+ readonly gpu: GpuContext;
41
+ /** Pending twins, and the last 32 finished. */
42
+ readonly waits: TwinWait[];
43
+ private readonly source;
44
+ private readonly mismatched;
45
+ constructor(source: TwinSource);
46
+ reload(texture: Texture): void;
47
+ fallback(srgb: boolean, array: boolean): GPUTextureView;
48
+ twin(texture: Texture, kind: string, ready: boolean): void;
49
+ mismatch(texture: Texture, kind: string): void;
50
+ /** `render.describe → twins`: pending twins with how long they've waited, and recent ones. */
51
+ describe(): {
52
+ pending: {
53
+ texture: string;
54
+ kind: string;
55
+ waitedMs: number;
56
+ }[];
57
+ recent: {
58
+ texture: string;
59
+ kind: string;
60
+ waitedMs: number;
61
+ }[];
62
+ };
63
+ }
64
+ /**
65
+ * A texture on the baseline tier: the primary copy, and the twins slots asked for. The views are
66
+ * getters, so a twin is made (or requested) only when something binds it.
67
+ */
68
+ export declare class BaselineTexture implements GpuTexture {
69
+ readonly texture: GPUTexture;
70
+ readonly version: number;
71
+ readonly generation: number;
72
+ bytes: number;
73
+ private readonly source;
74
+ private readonly host;
75
+ private readonly primarySrgb;
76
+ private readonly primaryDimension;
77
+ private readonly views;
78
+ private readonly pending;
79
+ /** Twins made, destroyed with the primary. */
80
+ readonly twins: GPUTexture[];
81
+ constructor(host: TwinHost, source: Texture);
82
+ get linear(): GPUTextureView;
83
+ get srgb(): GPUTextureView;
84
+ get arrayLinear(): GPUTextureView | undefined;
85
+ get arraySrgb(): GPUTextureView | undefined;
86
+ /** The primary and every twin. */
87
+ destroy(): void;
88
+ /**
89
+ * Whether a slot reading it this way waits for a twin's bytes (and shows the loading fallback).
90
+ * Asking requests the twin, as binding it would: material preparation asks for deferred draws.
91
+ */
92
+ waiting(wantSrgb: boolean, array: boolean): boolean;
93
+ private keyFor;
94
+ private view;
95
+ }
96
+ /** Solid 1×1 textures for empty slots, as baseline textures that keep their bytes. */
97
+ export declare function solidTexture(host: TwinHost, rgba: readonly number[]): BaselineTexture;
@@ -0,0 +1,203 @@
1
+ import { ShardError } from "@aethervtt/shard-core";
2
+ import { DevMode, LogResource } from "@aethervtt/shard-runtime";
3
+ import { FORMAT_INFO, Texture } from "@aethervtt/shard-texture";
4
+ class Twins {
5
+ gpu;
6
+ /** Pending twins, and the last 32 finished. */
7
+ waits = [];
8
+ source;
9
+ mismatched = /* @__PURE__ */ new Set();
10
+ constructor(source) {
11
+ this.source = source;
12
+ this.gpu = source.gpu;
13
+ }
14
+ reload(texture) {
15
+ this.source.reload(texture);
16
+ }
17
+ fallback(srgb, array) {
18
+ const white = this.source.white();
19
+ return array ? srgb ? white.arraySrgb : white.arrayLinear : srgb ? white.srgb : white.linear;
20
+ }
21
+ twin(texture, kind, ready) {
22
+ const name = this.source.guidOf(texture) ?? texture.format;
23
+ const waits = this.waits;
24
+ const i = waits.findIndex((w) => w.texture === name && w.kind === kind && w.ms === void 0);
25
+ if (!ready) {
26
+ if (i === -1) waits.push({ texture: name, kind, since: performance.now(), ms: void 0 });
27
+ } else if (i !== -1) {
28
+ waits[i].ms = performance.now() - waits[i].since;
29
+ } else if (waits.length < 256) {
30
+ waits.push({ texture: name, kind, since: performance.now(), ms: 0 });
31
+ }
32
+ while (waits.filter((w) => w.ms !== void 0).length > 32) {
33
+ waits.splice(
34
+ waits.findIndex((w) => w.ms !== void 0),
35
+ 1
36
+ );
37
+ }
38
+ }
39
+ mismatch(texture, kind) {
40
+ const world = this.source.world;
41
+ if (!world?.tryResource(DevMode)?.enabled) return;
42
+ const name = this.source.guidOf(texture);
43
+ if (name === void 0 || this.mismatched.has(`${name}|${kind}`)) return;
44
+ this.mismatched.add(`${name}|${kind}`);
45
+ world.tryResource(LogResource)?.error(
46
+ new ShardError(
47
+ "render/texture-color-space-mismatch",
48
+ `A ${texture.usage} texture is read as ${kind.split("/")[0]}: the baseline tier keeps a second copy of it`,
49
+ { path: name, hint: "Set its usage to match the slot in its .meta, and the copy goes." }
50
+ )
51
+ );
52
+ }
53
+ /** `render.describe → twins`: pending twins with how long they've waited, and recent ones. */
54
+ describe() {
55
+ const now = performance.now();
56
+ const pending = [];
57
+ const recent = [];
58
+ for (const w of this.waits) {
59
+ if (w.ms === void 0)
60
+ pending.push({ texture: w.texture, kind: w.kind, waitedMs: now - w.since });
61
+ else recent.push({ texture: w.texture, kind: w.kind, waitedMs: w.ms });
62
+ }
63
+ return { pending, recent };
64
+ }
65
+ }
66
+ const DIMENSION = { plain: "2d", array: "2d-array", cube: "cube" };
67
+ const DIMENSIONS = ["2d", "2d-array", "cube"];
68
+ function keyOf(srgb, dimension) {
69
+ return (srgb ? 1 : 0) + DIMENSIONS.indexOf(dimension) * 2;
70
+ }
71
+ function kindOf(key) {
72
+ return `${key & 1 ? "srgb" : "linear"}/${DIMENSIONS[key >> 1]}`;
73
+ }
74
+ function create(gpu, texture, format, dimension) {
75
+ const info = FORMAT_INFO[texture.format];
76
+ const layers = texture.faces * texture.layers;
77
+ const handle = gpu.device.createTexture({
78
+ label: `texture/${format}${dimension === "2d" ? "" : `/${dimension}`}`,
79
+ size: { width: texture.width, height: texture.height, depthOrArrayLayers: layers },
80
+ format,
81
+ mipLevelCount: texture.mipCount,
82
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
83
+ textureBindingViewDimension: dimension
84
+ });
85
+ for (let level = 0; texture.levels && level < texture.mipCount; level++) {
86
+ const w = Math.max(1, texture.width >> level);
87
+ const h = Math.max(1, texture.height >> level);
88
+ const blocksWide = Math.ceil(w / info.block);
89
+ const blocksHigh = Math.ceil(h / info.block);
90
+ gpu.device.queue.writeTexture(
91
+ { texture: handle, mipLevel: level },
92
+ texture.levels[level],
93
+ { bytesPerRow: blocksWide * info.bytes, rowsPerImage: blocksHigh },
94
+ {
95
+ width: blocksWide * info.block,
96
+ height: blocksHigh * info.block,
97
+ depthOrArrayLayers: layers
98
+ }
99
+ );
100
+ }
101
+ return handle;
102
+ }
103
+ class BaselineTexture {
104
+ texture;
105
+ version;
106
+ generation;
107
+ bytes;
108
+ source;
109
+ host;
110
+ primarySrgb;
111
+ primaryDimension;
112
+ views = /* @__PURE__ */ new Map();
113
+ pending = /* @__PURE__ */ new Set();
114
+ /** Twins made, destroyed with the primary. */
115
+ twins = [];
116
+ constructor(host, source) {
117
+ this.host = host;
118
+ this.source = source;
119
+ this.version = source.version;
120
+ this.generation = host.gpu.generation;
121
+ this.bytes = source.byteSize;
122
+ const info = FORMAT_INFO[source.format];
123
+ this.primarySrgb = source.usage === "color" && info.srgbView !== void 0;
124
+ this.primaryDimension = source.faces === 6 ? DIMENSION.cube : source.layers > 1 ? DIMENSION.array : DIMENSION.plain;
125
+ const format = this.primarySrgb ? info.srgbView : source.format;
126
+ this.texture = create(host.gpu, source, format, this.primaryDimension);
127
+ this.views.set(
128
+ keyOf(this.primarySrgb, this.primaryDimension),
129
+ this.texture.createView({ dimension: this.primaryDimension })
130
+ );
131
+ }
132
+ get linear() {
133
+ return this.view(false, false);
134
+ }
135
+ get srgb() {
136
+ return this.view(true, false);
137
+ }
138
+ get arrayLinear() {
139
+ return this.source.faces === 1 ? this.view(false, true) : void 0;
140
+ }
141
+ get arraySrgb() {
142
+ return this.source.faces === 1 ? this.view(true, true) : void 0;
143
+ }
144
+ /** The primary and every twin. */
145
+ destroy() {
146
+ this.texture.destroy();
147
+ for (const t of this.twins) t.destroy();
148
+ }
149
+ /**
150
+ * Whether a slot reading it this way waits for a twin's bytes (and shows the loading fallback).
151
+ * Asking requests the twin, as binding it would: material preparation asks for deferred draws.
152
+ */
153
+ waiting(wantSrgb, array) {
154
+ this.view(wantSrgb, array);
155
+ return this.pending.has(this.keyFor(wantSrgb, array));
156
+ }
157
+ keyFor(wantSrgb, array) {
158
+ const srgb = wantSrgb && FORMAT_INFO[this.source.format].srgbView !== void 0;
159
+ const dimension = array ? DIMENSION.array : this.primaryDimension === DIMENSION.array ? DIMENSION.array : this.primaryDimension;
160
+ return keyOf(srgb, dimension);
161
+ }
162
+ view(wantSrgb, array) {
163
+ const key = this.keyFor(wantSrgb, array);
164
+ const ready = this.views.get(key);
165
+ if (ready) return ready;
166
+ const srgb = (key & 1) === 1;
167
+ const dimension = DIMENSIONS[key >> 1];
168
+ const info = FORMAT_INFO[this.source.format];
169
+ if (srgb !== this.primarySrgb) this.host.mismatch(this.source, kindOf(key));
170
+ if (!this.source.levels) {
171
+ if (!this.pending.has(key)) {
172
+ this.pending.add(key);
173
+ this.host.twin(this.source, kindOf(key), false);
174
+ this.host.reload(this.source);
175
+ }
176
+ return this.host.fallback(srgb, dimension === DIMENSION.array);
177
+ }
178
+ const format = srgb ? info.srgbView : this.source.format;
179
+ const twin = create(this.host.gpu, this.source, format, dimension);
180
+ this.twins.push(twin);
181
+ this.bytes += this.source.byteSize;
182
+ const view = twin.createView({ dimension });
183
+ this.views.set(key, view);
184
+ this.pending.delete(key);
185
+ this.host.twin(this.source, kindOf(key), true);
186
+ return view;
187
+ }
188
+ }
189
+ function solidTexture(host, rgba) {
190
+ const texture = Texture.create({
191
+ width: 1,
192
+ height: 1,
193
+ mips: [new Uint8Array(rgba)],
194
+ cpu: true,
195
+ usage: "data"
196
+ });
197
+ return new BaselineTexture(host, texture);
198
+ }
199
+ export {
200
+ BaselineTexture,
201
+ Twins,
202
+ solidTexture
203
+ };
package/dist/camera.d.ts CHANGED
@@ -41,6 +41,7 @@ export declare const Camera3d: import("@aethervtt/shard-core").ComponentDef<{
41
41
  readonly clearColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
42
42
  readonly target: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"RenderTarget"> | null, "object">;
43
43
  readonly active: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
44
+ readonly layers: import("@aethervtt/shard-core").FieldType<number, "u16">;
44
45
  }>;
45
46
  export type Camera3dValue = Infer<typeof Camera3d>;
46
47
  export declare const PhysicalCamera: import("@aethervtt/shard-core").ComponentDef<{
@@ -65,6 +66,7 @@ export declare function camera2d(options?: {
65
66
  readonly clearColor: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
66
67
  readonly target: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"RenderTarget"> | null, "object">;
67
68
  readonly active: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
69
+ readonly layers: import("@aethervtt/shard-core").FieldType<number, "u16">;
68
70
  }>, {
69
71
  readonly projection: 'orthographic';
70
72
  readonly orthoHeight: number;
package/dist/camera.js CHANGED
@@ -77,6 +77,10 @@ const Camera3d = defineComponent(
77
77
  active: t.bool({
78
78
  default: true,
79
79
  description: "Renders when true. An inactive camera keeps its settings and transform but draws nothing."
80
+ }),
81
+ layers: t.u16({
82
+ default: 65535,
83
+ description: "Render layers this camera draws (0057): a renderable draws if its RenderLayers mask shares a bit. Default: all."
80
84
  })
81
85
  },
82
86
  {
@@ -37,6 +37,13 @@ struct CullView {
37
37
  lod_camera: u32,
38
38
  args_base: u32,
39
39
  visible_base: u32,
40
+ /** Render layers the view draws (0057). */
41
+ layers: u32,
42
+ /** Its hidden set's first word in lod_state, past the LOD entries (0070), or NO_HIDDEN. */
43
+ @if(HIDDEN) hidden_base: u32,
44
+ @if(!HIDDEN) _pad0: u32,
45
+ _pad1: u32,
46
+ _pad2: u32,
40
47
  }
41
48
 
42
49
  struct Params {
@@ -67,12 +74,15 @@ struct Deform {
67
74
  }
68
75
  @group(0) @binding(8) var<storage, read> deforms: array<Deform>;
69
76
 
77
+ @if(HIDDEN) const NO_HIDDEN: u32 = 0xffffffffu;
78
+
70
79
  const NO_BATCH: u32 = 0xffffffffu;
71
80
  const LOD_BIT: u32 = 0x80000000u;
72
81
  const LOD_UNSET: u32 = 0xffu;
73
82
  const FLAG_VISIBLE: u32 = 1u;
74
83
  const FLAG_CASTER: u32 = 2u;
75
84
  const FLAG_RANGE: u32 = 8u;
85
+ const FLAG_SHADOW_ONLY: u32 = 128u;
76
86
  const FLAG_SKINNED: u32 = 32u;
77
87
  const VIEW_CASTERS: u32 = 1u;
78
88
  const VIEW_UPDATE_LOD: u32 = 2u;
@@ -155,8 +165,17 @@ fn cull(@builtin(global_invocation_id) id: vec3u) {
155
165
  let inst = instances[s];
156
166
  if (inst.batch == NO_BATCH) { return; }
157
167
  var mask = FLAG_VISIBLE;
158
- if ((view.flags & VIEW_CASTERS) != 0u) { mask |= FLAG_CASTER; }
159
- if ((inst.flags & mask) != mask) { return; }
168
+ var flags = inst.flags;
169
+ if ((view.flags & VIEW_CASTERS) != 0u) {
170
+ mask |= FLAG_CASTER;
171
+ // Hidden meshes that still cast (ShadowWhenHidden, 0067).
172
+ if ((flags & FLAG_SHADOW_ONLY) != 0u) { flags |= FLAG_VISIBLE; }
173
+ }
174
+ if ((flags & mask) != mask) { return; }
175
+ // Render layers (0057) ride in flags bits 8\u201323.
176
+ if ((((inst.flags >> 8u) & 0xffffu) & view.layers) == 0u) { return; }
177
+ // Hidden from this view (ViewVisibility, 0070): a bit per slot.
178
+ @if(HIDDEN) if (view.hidden_base != NO_HIDDEN && (lod_state[view.hidden_base + (s >> 5u)] & (1u << (s & 31u))) != 0u) { return; }
160
179
  var b = inst.batch;
161
180
  var lod = false;
162
181
  var lod_set: LodSet;
package/dist/culling.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
2
2
  import type { NodeContext } from './graph.js';
3
3
  import { type CullParams, type DrawList, type InstanceStore } from './instances.js';
4
+ import type { HiddenSets } from './view-visibility-plugin.js';
4
5
  /**
5
6
  * GPU culling: one compute pass per frame culls every instance slot against every draw view
6
7
  * (cameras first, which choose LOD levels, then shadow views, which reuse them), writing indirect
@@ -11,6 +12,8 @@ export declare class GpuCuller {
11
12
  readonly supported: boolean;
12
13
  /** Turn off to force CPU culling (tests, debugging). */
13
14
  enabled: boolean;
15
+ /** Views this frame with a hidden set (0070): the cull links its HIDDEN variant. */
16
+ private hiddenViews;
14
17
  private readonly views;
15
18
  private viewCount;
16
19
  private cameraViews;
@@ -58,8 +61,12 @@ export declare class GpuCuller {
58
61
  * batches into `forwardOnly`.
59
62
  */
60
63
  add(store: InstanceStore, list: DrawList, params: CullParams, lodCamera: number, forwardOnly?: DrawList): number;
61
- /** Uploads the frame's view, batch, and LOD tables, and sizes the buffers. */
62
- prepare(store: InstanceStore): void;
64
+ /**
65
+ * Uploads the frame's view, batch, and LOD tables, and sizes the buffers. Hidden sets (0070)
66
+ * ride in the LOD state buffer after its entries: the cull's storage bindings are at WebGPU's
67
+ * guaranteed 8 per stage.
68
+ */
69
+ prepare(store: InstanceStore, hidden?: HiddenSets): void;
63
70
  private slotCount;
64
71
  private slotCapacity;
65
72
  /** Encodes the cull: reset arguments, cull camera views, then shadow views. */
@@ -75,6 +82,8 @@ export declare const Culler: import("@aethervtt/shard-core").ResourceDef<GpuCull
75
82
  * members of blended batches, so it costs what's transparent, not what exists.
76
83
  */
77
84
  export declare function cullTransparent(store: InstanceStore, list: DrawList, params: CullParams, forward: Float32Array): void;
85
+ /** The CPU ground list (0057) of a view the GPU culls. */
86
+ export declare function cullGround(store: InstanceStore, list: DrawList, params: CullParams, overlay?: DrawList): void;
78
87
  /**
79
88
  * The CPU-side visible set of a view as slots (tests compare it with the GPU's). Pass
80
89
  * `mask = 0xffffffff` to keep the LOD level in the top bits.
package/dist/culling.js CHANGED
@@ -7,8 +7,9 @@ import {
7
7
  SLOT_MASK
8
8
  } from "./instances.js";
9
9
  import { Shaders } from "./plugin.js";
10
+ import { NO_HIDDEN } from "./view-visibility.js";
10
11
  const ARGS_WORDS = 5;
11
- const VIEW_FLOATS = 32;
12
+ const VIEW_FLOATS = 36;
12
13
  const BATCH_WORDS = 8;
13
14
  const LOD_WORDS = 20;
14
15
  const VIEW_CASTERS = 1;
@@ -21,6 +22,8 @@ class GpuCuller {
21
22
  supported;
22
23
  /** Turn off to force CPU culling (tests, debugging). */
23
24
  enabled = true;
25
+ /** Views this frame with a hidden set (0070): the cull links its HIDDEN variant. */
26
+ hiddenViews = 0;
24
27
  views = [];
25
28
  viewCount = 0;
26
29
  cameraViews = 0;
@@ -59,7 +62,7 @@ class GpuCuller {
59
62
  pending = /* @__PURE__ */ new Set();
60
63
  constructor(gpu) {
61
64
  this.gpu = gpu;
62
- this.supported = gpu.features.has("indirect-first-instance");
65
+ this.supported = gpu.tier === "full" && gpu.features.has("indirect-first-instance");
63
66
  const storage = GPUBufferUsage.STORAGE;
64
67
  this.viewBuffer = new GpuBuffer(gpu, { label: "cull/views", usage: storage, size: 4096 });
65
68
  this.batchBuffer = new GpuBuffer(gpu, { label: "cull/batches", usage: storage, size: 2048 });
@@ -82,6 +85,7 @@ class GpuCuller {
82
85
  beginFrame() {
83
86
  this.viewCount = 0;
84
87
  this.cameraViews = 0;
88
+ this.hiddenViews = 0;
85
89
  }
86
90
  /** The LOD state row of a camera (cameras choose; their shadow views reuse the choice). */
87
91
  lodCamera(entity) {
@@ -110,7 +114,9 @@ class GpuCuller {
110
114
  hasEye: false,
111
115
  lodScale: 1,
112
116
  flags: 0,
113
- lodCamera: 0
117
+ lodCamera: 0,
118
+ layers: 4294967295,
119
+ hiddenBase: NO_HIDDEN
114
120
  };
115
121
  this.views.push(view);
116
122
  }
@@ -122,6 +128,9 @@ class GpuCuller {
122
128
  if (params.eye) view.eye.set(params.eye);
123
129
  view.lodScale = params.lodScale;
124
130
  view.lodCamera = lodCamera;
131
+ view.layers = params.layers ?? 4294967295;
132
+ view.hiddenBase = params.hiddenBase ?? NO_HIDDEN;
133
+ if (view.hiddenBase !== NO_HIDDEN) this.hiddenViews++;
125
134
  view.flags = (params.require & InstanceFlags.Caster ? VIEW_CASTERS : 0) | (params.updateLod ? VIEW_UPDATE_LOD : 0) | (params.orthographic ? VIEW_ORTHO : 0) | (params.planes ? 0 : VIEW_NO_PLANES);
126
135
  if (params.updateLod) this.cameraViews = v + 1;
127
136
  list.cullView = v;
@@ -136,7 +145,8 @@ class GpuCuller {
136
145
  let n = 0;
137
146
  let m = 0;
138
147
  for (const batch of store.sorted) {
139
- if (batch.count === 0 || batch.transparent || !store.anyVisible(batch)) continue;
148
+ if (batch.count === 0 || batch.transparent || batch.ground || !store.anyVisible(batch))
149
+ continue;
140
150
  if (!batch.ready) {
141
151
  list.pending += batch.count;
142
152
  continue;
@@ -157,9 +167,22 @@ class GpuCuller {
157
167
  if (forwardOnly) forwardOnly.length = m;
158
168
  return v;
159
169
  }
160
- /** Uploads the frame's view, batch, and LOD tables, and sizes the buffers. */
161
- prepare(store) {
170
+ /**
171
+ * Uploads the frame's view, batch, and LOD tables, and sizes the buffers. Hidden sets (0070)
172
+ * ride in the LOD state buffer after its entries: the cull's storage bindings are at WebGPU's
173
+ * guaranteed 8 per stage.
174
+ */
175
+ prepare(store, hidden) {
162
176
  if (this.viewCount === 0) return;
177
+ const lodCams = Math.max(1, this.lodCameras.size);
178
+ const hiddenWords = hidden && hidden.sets.size > 0 ? hidden.data.length : 0;
179
+ if (this.lodStateCapacity !== store.capacity * lodCams || this.lodState.byteLength < (this.lodStateCapacity + hiddenWords) * 4) {
180
+ this.lodStateCapacity = store.capacity * lodCams;
181
+ this.lodState.ensureCapacity((this.lodStateCapacity + hiddenWords) * 4);
182
+ const fill = new Uint32Array(this.lodStateCapacity).fill(LOD_UNSET);
183
+ this.lodState.write(fill);
184
+ }
185
+ if (hiddenWords > 0) hidden.upload(this.lodState, this.lodStateCapacity * 4);
163
186
  const batches = store.batches;
164
187
  this.batchCount = batches.length;
165
188
  if (this.batchData.length < batches.length * BATCH_WORDS) {
@@ -182,7 +205,7 @@ class GpuCuller {
182
205
  this.batchData[o + 4] = batch.mesh.drawCount;
183
206
  this.batchData[o + 5] = batch.mesh.indexed ? 1 + batch.mesh.baseVertex : 0;
184
207
  this.batchData[o + 6] = batch.region;
185
- this.batchData[o + 7] = (batch.transparent ? BATCH_TRANSPARENT : 0) | (batch.ready ? 0 : BATCH_NOT_READY);
208
+ this.batchData[o + 7] = (batch.transparent || batch.ground ? BATCH_TRANSPARENT : 0) | (batch.ready ? 0 : BATCH_NOT_READY);
186
209
  }
187
210
  this.regionSize = Math.max(1, region);
188
211
  this.batchBuffer.write(this.batchData, 0, 0, Math.max(1, batches.length) * BATCH_WORDS);
@@ -220,6 +243,8 @@ class GpuCuller {
220
243
  this.viewU32[o + 29] = view.lodCamera;
221
244
  this.viewU32[o + 30] = v * batches.length;
222
245
  this.viewU32[o + 31] = v * this.regionSize;
246
+ this.viewU32[o + 32] = view.layers;
247
+ this.viewU32[o + 33] = view.hiddenBase === NO_HIDDEN ? NO_HIDDEN : this.lodStateCapacity + view.hiddenBase;
223
248
  for (const list of [view.list, view.forwardOnly]) {
224
249
  if (!list) continue;
225
250
  for (let d = 0; d < list.length; d++) {
@@ -232,12 +257,6 @@ class GpuCuller {
232
257
  this.viewBuffer.write(this.viewData, 0, 0, this.viewCount * VIEW_FLOATS);
233
258
  this.args.ensureCapacity(Math.max(1, this.viewCount * batches.length) * ARGS_WORDS * 4);
234
259
  this.visible.ensureCapacity(this.viewCount * this.regionSize * 4);
235
- const lodCams = Math.max(1, this.lodCameras.size);
236
- if (this.lodStateCapacity !== store.capacity * lodCams) {
237
- this.lodStateCapacity = store.capacity * lodCams;
238
- const fill = new Uint32Array(this.lodStateCapacity).fill(LOD_UNSET);
239
- this.lodState.write(fill);
240
- }
241
260
  this.frameViews = this.views.slice(0, this.viewCount);
242
261
  this.slotCount = store.high;
243
262
  this.slotCapacity = store.capacity;
@@ -249,7 +268,8 @@ class GpuCuller {
249
268
  if (this.viewCount === 0) return;
250
269
  const gpu = ctx.gpu;
251
270
  const world = ctx.world;
252
- const module = world.resource(Shaders).module(gpu, { root: "shard::cull" });
271
+ const hidden = this.hiddenViews > 0;
272
+ const module = world.resource(Shaders).module(gpu, hidden ? HIDDEN_REQUEST : CULL_REQUEST);
253
273
  if (!module) {
254
274
  gpu.pipelines.skipped++;
255
275
  return;
@@ -257,12 +277,12 @@ class GpuCuller {
257
277
  const layout = cullLayout(gpu);
258
278
  const pipelineLayout = gpu.layouts.pipelineLayout({ label: "cull", bindGroupLayouts: [layout] });
259
279
  const reset = gpu.pipelines.compute({
260
- label: "cull/reset",
280
+ label: hidden ? "cull/reset/hidden" : "cull/reset",
261
281
  layout: pipelineLayout,
262
282
  compute: { module, entryPoint: "reset" }
263
283
  });
264
284
  const cull = gpu.pipelines.compute({
265
- label: "cull/instances",
285
+ label: hidden ? "cull/instances/hidden" : "cull/instances",
266
286
  layout: pipelineLayout,
267
287
  compute: { module, entryPoint: "cull" }
268
288
  });
@@ -309,7 +329,7 @@ class GpuCuller {
309
329
  label: "cull",
310
330
  layout,
311
331
  entries: [
312
- { binding: 0, resource: { buffer: store.instanceBuffer.buffer } },
332
+ { binding: 0, resource: { buffer: store.instanceBuffer.gpuBuffer } },
313
333
  { binding: 1, resource: { buffer: this.batchBuffer.buffer } },
314
334
  { binding: 2, resource: { buffer: this.lodBuffer.buffer } },
315
335
  { binding: 3, resource: { buffer: this.viewBuffer.buffer } },
@@ -317,7 +337,7 @@ class GpuCuller {
317
337
  { binding: 5, resource: { buffer: this.visible.buffer } },
318
338
  { binding: 6, resource: { buffer: this.lodState.buffer } },
319
339
  { binding: 7, resource: { buffer: params.buffer } },
320
- { binding: 8, resource: { buffer: store.deform.recordBuffer.buffer } }
340
+ { binding: 8, resource: { buffer: store.deform.recordBuffer.gpuBuffer } }
321
341
  ]
322
342
  })
323
343
  )
@@ -434,6 +454,8 @@ class GpuCuller {
434
454
  while (this.pending.size > 0) await Promise.all(this.pending);
435
455
  }
436
456
  }
457
+ const CULL_REQUEST = { root: "shard::cull" };
458
+ const HIDDEN_REQUEST = { root: "shard::cull", defines: { HIDDEN: true } };
437
459
  function cullLayout(gpu) {
438
460
  const C = GPUShaderStage.COMPUTE;
439
461
  const read = (binding) => ({
@@ -467,6 +489,9 @@ const Culler = defineResource("render/Culler", {
467
489
  function cullTransparent(store, list, params, forward) {
468
490
  store.cullTransparentMembers(list, params, forward);
469
491
  }
492
+ function cullGround(store, list, params, overlay) {
493
+ store.cullGroundMembers(list, params, overlay);
494
+ }
470
495
  function visibleSlots(store, list, mask = SLOT_MASK) {
471
496
  const out = /* @__PURE__ */ new Set();
472
497
  for (let d = 0; d < list.length; d++) {
@@ -492,6 +517,7 @@ async function readVisibleSlots(gpu, culler, list, mask = SLOT_MASK) {
492
517
  export {
493
518
  Culler,
494
519
  GpuCuller,
520
+ cullGround,
495
521
  cullTransparent,
496
522
  readVisibleSlots,
497
523
  visibleSlots
@@ -0,0 +1,48 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import { GpuBuffer } from '@aethervtt/shard-gpu';
3
+ export declare const CUTAWAY_SHADERS: Record<string, string>;
4
+ /** A camera's reveal points as uploaded, and the bind group over them. */
5
+ interface CutawayCamera {
6
+ data: Float32Array;
7
+ buffer: GpuBuffer | undefined;
8
+ bindGroup: GPUBindGroup | undefined;
9
+ /** The device generation and buffer version the bind group was made for. */
10
+ boundGeneration: number;
11
+ boundVersion: number;
12
+ /** Points in use (at most MAX_REVEAL_POINTS). */
13
+ count: number;
14
+ /** Points the component listed. */
15
+ listed: number;
16
+ frame: number;
17
+ /** Too many points was reported for this camera. */
18
+ warned: boolean;
19
+ /** The device generation its buffer was last written on (a new device loses the contents). */
20
+ written: number;
21
+ /** Per batch index: 1 when a point can cut one of its Cutaway instances this frame. */
22
+ cuts: Uint8Array;
23
+ }
24
+ export interface CutawayState {
25
+ cameras: Map<Entity, CutawayCamera>;
26
+ frame: number;
27
+ layout: {
28
+ layout: GPUBindGroupLayout;
29
+ generation: number;
30
+ } | undefined;
31
+ }
32
+ export declare const CutawayCameras: import("@aethervtt/shard-core").ResourceDef<CutawayState>;
33
+ /**
34
+ * Packs each camera's CutawayView into its uniform, with each point's line of sight from the
35
+ * camera. Only a camera whose values changed uploads (a camera moving changes the lines); a still
36
+ * frame writes nothing and allocates nothing.
37
+ */
38
+ export declare const extractCutaways: import("@aethervtt/shard-core").SystemDef<{
39
+ q: import("@aethervtt/shard-core").Query;
40
+ }>;
41
+ /** render.describe's cutaways section: per camera, its reveal points, radius, margin and edge. */
42
+ export declare function describeCutaways(world: World): Record<string, unknown>;
43
+ /**
44
+ * Cutaways (0070): Cutaway renderables open around a camera's CutawayView points, in every camera
45
+ * pass and no shadow pass. Cameras without reveal points draw the plain variants.
46
+ */
47
+ export declare const cutawayPlugin: import("@aethervtt/shard-runtime").Plugin;
48
+ export {};