@aethervtt/shard-render 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/atmosphere-nodes.js +100 -12
  4. package/dist/atmosphere-shaders.js +17 -5
  5. package/dist/baseline/exposure.d.ts +1 -1
  6. package/dist/baseline/exposure.js +18 -5
  7. package/dist/crowd.d.ts +21 -0
  8. package/dist/crowd.js +121 -0
  9. package/dist/decals-plugin.d.ts +5 -0
  10. package/dist/decals-plugin.js +54 -0
  11. package/dist/decals.d.ts +69 -0
  12. package/dist/decals.js +326 -0
  13. package/dist/deferred.js +4 -1
  14. package/dist/display-nodes.js +24 -11
  15. package/dist/foliage-budget.d.ts +64 -0
  16. package/dist/foliage-budget.js +108 -0
  17. package/dist/foliage-path.d.ts +19 -0
  18. package/dist/foliage-path.js +7 -0
  19. package/dist/foliage-shaders.d.ts +11 -0
  20. package/dist/foliage-shaders.js +504 -0
  21. package/dist/foliage.d.ts +194 -0
  22. package/dist/foliage.js +1027 -0
  23. package/dist/forward.js +26 -2
  24. package/dist/graph.d.ts +34 -0
  25. package/dist/graph.js +139 -14
  26. package/dist/index.d.ts +11 -1
  27. package/dist/index.js +57 -0
  28. package/dist/overlay-registry.d.ts +3 -1
  29. package/dist/overlay-registry.js +3 -1
  30. package/dist/overlays.js +5 -2
  31. package/dist/perf-overlay.d.ts +9 -0
  32. package/dist/perf-overlay.js +125 -0
  33. package/dist/plugin.d.ts +11 -1
  34. package/dist/plugin.js +33 -2
  35. package/dist/post-common.d.ts +20 -2
  36. package/dist/post-common.js +79 -2
  37. package/dist/post-nodes.d.ts +5 -0
  38. package/dist/post-nodes.js +136 -104
  39. package/dist/post-scene.d.ts +130 -0
  40. package/dist/post-scene.js +164 -0
  41. package/dist/post-shaders.js +41 -63
  42. package/dist/post.js +11 -1
  43. package/dist/ribbons-plugin.d.ts +5 -0
  44. package/dist/ribbons-plugin.js +59 -0
  45. package/dist/ribbons.d.ts +44 -0
  46. package/dist/ribbons.js +288 -0
  47. package/dist/scenario.d.ts +70 -0
  48. package/dist/scenario.js +125 -0
  49. package/dist/shaders.js +58 -5
  50. package/dist/shadows.d.ts +2 -1
  51. package/dist/shadows.js +3 -1
  52. package/dist/standard.js +6 -0
  53. package/dist/testing.d.ts +1 -0
  54. package/dist/testing.js +6 -0
  55. package/dist/timer.d.ts +16 -1
  56. package/dist/timer.js +66 -19
  57. package/dist/vfx-draw.d.ts +37 -0
  58. package/dist/vfx-draw.js +291 -0
  59. package/dist/vfx-state.d.ts +42 -0
  60. package/dist/vfx-state.js +81 -0
  61. package/dist/vfx.d.ts +69 -0
  62. package/dist/vfx.js +103 -0
  63. package/package.json +19 -11
@@ -0,0 +1,108 @@
1
+ import {
2
+ defineResource,
3
+ spanCount,
4
+ spanCovers,
5
+ spanId,
6
+ spanName
7
+ } from "@aethervtt/shard-core";
8
+ const FOLIAGE_SPAN_KEY = "gpu:foliage";
9
+ const FoliageBudget = defineResource("render/FoliageBudget", {
10
+ description: "GPU foliage's time budget (0045, 0075): ms is the target (0: the scenario's gpu:foliage slice); detail is how much of full density and range it draws to stay inside it.",
11
+ hostWritable: true,
12
+ init: () => ({ ms: 0, minDetail: 0.1, target: 0, measuredMs: 0, detail: 1 })
13
+ });
14
+ const SMOOTHING = 0.15;
15
+ const STEP_DOWN = 1.01;
16
+ const STEP_UP = 1.005;
17
+ const RECOVER = 0.85;
18
+ class FoliageController {
19
+ detail = 1;
20
+ /** Smoothed GPU time (ms); 0 before the first sample. */
21
+ smoothed = 0;
22
+ primed = false;
23
+ /** One frame's foliage GPU time against `target` (0: none, back to full detail at once). */
24
+ sample(ms, target, minDetail) {
25
+ this.smoothed = this.primed ? this.smoothed + SMOOTHING * (ms - this.smoothed) : ms;
26
+ this.primed = true;
27
+ if (!(target > 0)) {
28
+ this.detail = 1;
29
+ return;
30
+ }
31
+ const floor = Math.min(1, Math.max(1e-3, minDetail));
32
+ if (this.smoothed > target) this.detail = Math.max(floor, this.detail / STEP_DOWN);
33
+ else if (this.smoothed < target * RECOVER) this.detail = Math.min(1, this.detail * STEP_UP);
34
+ if (this.detail < floor) this.detail = floor;
35
+ }
36
+ /** No target: full detail, and the smoothing starts over. */
37
+ reset() {
38
+ this.detail = 1;
39
+ this.primed = false;
40
+ this.smoothed = 0;
41
+ }
42
+ /** The share of instances kept by thinning (the cull's density factor). */
43
+ get density() {
44
+ return Math.sqrt(this.detail);
45
+ }
46
+ /** The share of each layer's range drawn. */
47
+ get rangeScale() {
48
+ return Math.sqrt(Math.sqrt(this.detail));
49
+ }
50
+ }
51
+ class CoveredGpuTime {
52
+ key;
53
+ /** The latest frame's covered time (ms), after `read` returned true. */
54
+ ms = 0;
55
+ ids = new Int32Array(8);
56
+ seen = new Float64Array(8);
57
+ count = 0;
58
+ scanned = 0;
59
+ frameId = -1;
60
+ frameRuns = 0;
61
+ constructor(key) {
62
+ this.key = key;
63
+ }
64
+ /** True when a new GPU frame's timings landed since the last call; `ms` is then its covered time. */
65
+ read(profiler) {
66
+ if (spanCount() !== this.scanned) this.scan(profiler);
67
+ if (this.frameId < 0) return false;
68
+ const runs = profiler.runsOf(this.frameId);
69
+ if (runs === this.frameRuns) return false;
70
+ this.frameRuns = runs;
71
+ let ms = 0;
72
+ for (let i = 0; i < this.count; i++) {
73
+ const id = this.ids[i];
74
+ const r = profiler.runsOf(id);
75
+ if (r === this.seen[i]) continue;
76
+ this.seen[i] = r;
77
+ ms += profiler.lastOf(id);
78
+ }
79
+ this.ms = ms;
80
+ return true;
81
+ }
82
+ /** New span names since the last scan: the ones the key covers join the list. */
83
+ scan(profiler) {
84
+ const n = spanCount();
85
+ for (let id = this.scanned; id < n; id++) {
86
+ if (!spanCovers(this.key, spanName(id))) continue;
87
+ if (this.count === this.ids.length) {
88
+ const ids = new Int32Array(this.count * 2);
89
+ ids.set(this.ids);
90
+ this.ids = ids;
91
+ const seen = new Float64Array(this.count * 2);
92
+ seen.set(this.seen);
93
+ this.seen = seen;
94
+ }
95
+ this.ids[this.count] = id;
96
+ this.seen[this.count] = profiler.runsOf(id);
97
+ this.count++;
98
+ }
99
+ this.scanned = n;
100
+ if (this.frameId < 0) this.frameId = spanId("gpu:frame");
101
+ }
102
+ }
103
+ export {
104
+ CoveredGpuTime,
105
+ FOLIAGE_SPAN_KEY,
106
+ FoliageBudget,
107
+ FoliageController
108
+ };
@@ -0,0 +1,19 @@
1
+ import type { NodeContext } from './graph.js';
2
+ import type { CameraData } from './view.js';
3
+ /**
4
+ * What the forward passes ask of GPU foliage (0045, `foliagePlugin`): draw a camera's visible
5
+ * foliage into its opaque or G-buffer pass, and its shadow casters into a cascade. The core only
6
+ * holds this hook; the instances, placement, culling and pipelines live in the plugin.
7
+ */
8
+ export interface FoliageSupport {
9
+ /** Draws into the open pass; `pass` is PASS_OPAQUE or PASS_GBUFFER. */
10
+ draw(ctx: NodeContext, cam: CameraData, pass: number, renderPass: GPURenderPassEncoder): void;
11
+ /**
12
+ * Draws shadow casters into a cascade layer's open pass (its view at `offset`). Foliage casts
13
+ * into the nearest cascade only.
14
+ */
15
+ drawShadow(ctx: NodeContext, cam: CameraData, renderPass: GPURenderPassEncoder, offset: number, cascade: number): void;
16
+ /** Whether a camera's nearest cascade must redraw this frame (foliage sways: cached ones go stale). */
17
+ animatedShadows(cam: CameraData): boolean;
18
+ }
19
+ export declare const FoliagePath: import("@aethervtt/shard-core").ResourceDef<FoliageSupport>;
@@ -0,0 +1,7 @@
1
+ import { defineResource } from "@aethervtt/shard-core";
2
+ const FoliagePath = defineResource("render/FoliagePath", {
3
+ description: "Present when GPU foliage (foliagePlugin) is installed."
4
+ });
5
+ export {
6
+ FoliagePath
7
+ };
@@ -0,0 +1,11 @@
1
+ /** Bytes per placement dispatch's parameters (a dynamic uniform offset). */
2
+ export declare const PLACE_PARAMS_BYTES = 256;
3
+ /** Floats per patch vertex: position (3), normal (3), density, pad. */
4
+ export declare const PATCH_FLOATS = 8;
5
+ /** Floats per chunk record: chunk → world rows (12), bounds min (3) + active, size (3) + pad. */
6
+ export declare const CHUNK_FLOATS = 20;
7
+ /** Floats in a view's cull parameters. */
8
+ export declare const CULL_PARAMS_FLOATS = 44;
9
+ /** Meshes a layer may draw (weights are 8 vec4s). */
10
+ export declare const MAX_FOLIAGE_MESHES = 32;
11
+ export declare const FOLIAGE_SHADERS: Record<string, string>;
@@ -0,0 +1,504 @@
1
+ const PLACE_PARAMS_BYTES = 256;
2
+ const PATCH_FLOATS = 8;
3
+ const CHUNK_FLOATS = 20;
4
+ const CULL_PARAMS_FLOATS = 44;
5
+ const MAX_FOLIAGE_MESHES = 32;
6
+ const COMMON = `
7
+ const PLACED: u32 = 1u;
8
+
9
+ struct FoliageChunk {
10
+ row0: vec4f,
11
+ row1: vec4f,
12
+ row2: vec4f,
13
+ /** Bounds of its positions (xyz) and whether the slot holds a chunk (w > 0.5). */
14
+ bmin: vec4f,
15
+ bsize: vec4f,
16
+ }
17
+
18
+ struct FoliageInstance {
19
+ position: vec3f,
20
+ mesh: u32,
21
+ up: vec3f,
22
+ yaw: f32,
23
+ scale: f32,
24
+ shade: f32,
25
+ rank: f32,
26
+ placed: bool,
27
+ }
28
+
29
+ fn oct_wrap(v: vec2f) -> vec2f {
30
+ return (1.0 - abs(v.yx)) * select(vec2f(-1.0), vec2f(1.0), v.xy >= vec2f(0.0));
31
+ }
32
+
33
+ fn oct_encode(n: vec3f) -> vec2f {
34
+ var p = n.xy / (abs(n.x) + abs(n.y) + abs(n.z));
35
+ if (n.z < 0.0) { p = oct_wrap(p); }
36
+ return p * 0.5 + 0.5;
37
+ }
38
+
39
+ fn oct_decode(e: vec2f) -> vec3f {
40
+ let f = e * 2.0 - 1.0;
41
+ var n = vec3f(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));
42
+ let t = clamp(-n.z, 0.0, 1.0);
43
+ n = vec3f(n.x + select(t, -t, n.x >= 0.0), n.y + select(t, -t, n.y >= 0.0), n.z);
44
+ return normalize(n);
45
+ }
46
+
47
+ fn decode_instance(raw: vec4u, chunk: FoliageChunk, scale_min: f32, scale_max: f32) -> FoliageInstance {
48
+ var out: FoliageInstance;
49
+ let q = vec3f(f32(raw.x & 0xffffu), f32(raw.x >> 16u), f32(raw.y & 0xffffu)) / 65535.0;
50
+ out.position = chunk.bmin.xyz + q * chunk.bsize.xyz;
51
+ out.mesh = (raw.y >> 16u) & 0xffu;
52
+ out.placed = ((raw.y >> 24u) & PLACED) != 0u;
53
+ out.up = oct_decode(vec2f(f32(raw.z & 0xffu), f32((raw.z >> 8u) & 0xffu)) / 255.0);
54
+ out.yaw = f32((raw.z >> 16u) & 0xffu) / 256.0 * 6.2831853;
55
+ out.scale = mix(scale_min, scale_max, f32(raw.z >> 24u) / 255.0);
56
+ out.shade = f32(raw.w & 0xffu) / 255.0;
57
+ out.rank = f32((raw.w >> 8u) & 0xffu) / 255.0;
58
+ return out;
59
+ }
60
+
61
+ /** Rotates v by unit quaternion q. */
62
+ fn quat_rotate(q: vec4f, v: vec3f) -> vec3f {
63
+ let t = 2.0 * cross(q.xyz, v);
64
+ return v + q.w * t + cross(q.xyz, t);
65
+ }
66
+
67
+ /** World rows of an instance: the chunk's frame times its local transform (up, yaw, scale). */
68
+ fn instance_rows(chunk: FoliageChunk, inst: FoliageInstance, scale: vec3f) -> array<vec4f, 3> {
69
+ // Shortest arc from +Y to up, then yaw about +Y (the CPU's writeRotation).
70
+ let q = normalize(vec4f(inst.up.z, 0.0, -inst.up.x, 1.0 + inst.up.y));
71
+ let c = cos(inst.yaw);
72
+ let s = sin(inst.yaw);
73
+ let ax = quat_rotate(q, vec3f(c, 0.0, -s)) * scale.x;
74
+ let ay = inst.up * scale.y;
75
+ let az = quat_rotate(q, vec3f(s, 0.0, c)) * scale.z;
76
+ let p = inst.position;
77
+ return array<vec4f, 3>(
78
+ compose_row(chunk.row0, ax, ay, az, p),
79
+ compose_row(chunk.row1, ax, ay, az, p),
80
+ compose_row(chunk.row2, ax, ay, az, p),
81
+ );
82
+ }
83
+
84
+ /** One row of (chunk frame) \xD7 (local basis ax, ay, az and translation p). */
85
+ fn compose_row(m: vec4f, ax: vec3f, ay: vec3f, az: vec3f, p: vec3f) -> vec4f {
86
+ return vec4f(dot(m.xyz, vec3f(ax.x, ay.x, az.x)), dot(m.xyz, vec3f(ax.y, ay.y, az.y)), dot(m.xyz, vec3f(ax.z, ay.z, az.z)), dot(m.xyz, p) + m.w);
87
+ }
88
+
89
+ /** Of the instances in range, the share kept at a distance (fraction of the range): far ones thin out. */
90
+ fn keep_share(f: f32, start: f32) -> f32 {
91
+ if (f <= start) { return 1.0; }
92
+ return mix(1.0, 0.3, clamp((f - start) / max(1e-4, 1.0 - start), 0.0, 1.0));
93
+ }
94
+ `;
95
+ const FOLIAGE_SHADERS = {
96
+ "shard::foliage::common": COMMON,
97
+ "shard::foliage::place": `
98
+ import shard::foliage::common::{ PLACED, oct_encode };
99
+
100
+ struct PlaceParams {
101
+ cells: u32,
102
+ grid: u32,
103
+ slot: u32,
104
+ per_chunk: u32,
105
+ gi0: i32,
106
+ gj0: i32,
107
+ domain: u32,
108
+ seed: u32,
109
+ accept: f32,
110
+ jitter: f32,
111
+ scale_min: f32,
112
+ scale_max: f32,
113
+ align: f32,
114
+ mesh_count: u32,
115
+ avoid_count: u32,
116
+ radial: u32,
117
+ patch_base: u32,
118
+ avoid_base: u32,
119
+ _p0: u32,
120
+ _p1: u32,
121
+ /** The chunk's origin in its surface's frame (radial up on planets). */
122
+ origin: vec4f,
123
+ bmin: vec4f,
124
+ bsize: vec4f,
125
+ /** Cumulative pick weights per mesh, ending at 1. */
126
+ weights: array<vec4f, 8>,
127
+ }
128
+
129
+ @group(0) @binding(0) var<uniform> params: PlaceParams;
130
+ @group(0) @binding(1) var<storage, read> ground: array<f32>;
131
+ @group(0) @binding(2) var<storage, read> avoid: array<vec4f>;
132
+ @group(0) @binding(3) var<storage, read_write> instances: array<vec4u>;
133
+
134
+ fn mix32(x: u32) -> u32 {
135
+ var h = x;
136
+ h ^= h >> 16u;
137
+ h *= 0x7feb352du;
138
+ h ^= h >> 15u;
139
+ h *= 0x846ca68bu;
140
+ return h ^ (h >> 16u);
141
+ }
142
+
143
+ /** The CPU's cellRandom (rules.ts): hash32 of the rule seed, the global cell, its domain, a stream. */
144
+ fn cell_random(gi: i32, gj: i32, stream: u32) -> f32 {
145
+ let h = mix32(params.seed ^ (bitcast<u32>(gi) * 501125321u) ^ (bitcast<u32>(gj) * 1136930381u) ^ (params.domain * 1720413743u) ^ (stream * 1066037191u));
146
+ return f32(h >> 8u) * (1.0 / 16777216.0);
147
+ }
148
+
149
+ struct PatchSample {
150
+ position: vec3f,
151
+ normal: vec3f,
152
+ density: f32,
153
+ }
154
+
155
+ fn vertex_at(i: u32, j: u32) -> PatchSample {
156
+ let o = params.patch_base + (i + j * params.grid) * 8u;
157
+ var s: PatchSample;
158
+ s.position = vec3f(ground[o], ground[o + 1u], ground[o + 2u]);
159
+ s.normal = vec3f(ground[o + 3u], ground[o + 4u], ground[o + 5u]);
160
+ s.density = ground[o + 6u];
161
+ return s;
162
+ }
163
+
164
+ /** The patch at (s, t) in [0, 1]\xB2: its grid's triangles, split along (i, j)\u2013(i+1, j+1) like the ground. */
165
+ fn patch_at(s: f32, t: f32) -> PatchSample {
166
+ let n = f32(params.grid - 1u);
167
+ let fx = clamp(s * n, 0.0, n);
168
+ let fy = clamp(t * n, 0.0, n);
169
+ let i = min(u32(fx), params.grid - 2u);
170
+ let j = min(u32(fy), params.grid - 2u);
171
+ let a = fx - f32(i);
172
+ let b = fy - f32(j);
173
+ let p00 = vertex_at(i, j);
174
+ let p11 = vertex_at(i + 1u, j + 1u);
175
+ var out: PatchSample;
176
+ if (a >= b) {
177
+ let p10 = vertex_at(i + 1u, j);
178
+ out.position = p00.position + a * (p10.position - p00.position) + b * (p11.position - p10.position);
179
+ out.normal = p00.normal + a * (p10.normal - p00.normal) + b * (p11.normal - p10.normal);
180
+ out.density = p00.density + a * (p10.density - p00.density) + b * (p11.density - p10.density);
181
+ } else {
182
+ let p01 = vertex_at(i, j + 1u);
183
+ out.position = p00.position + b * (p01.position - p00.position) + a * (p11.position - p01.position);
184
+ out.normal = p00.normal + b * (p01.normal - p00.normal) + a * (p11.normal - p01.normal);
185
+ out.density = p00.density + b * (p01.density - p00.density) + a * (p11.density - p01.density);
186
+ }
187
+ out.normal = normalize(out.normal);
188
+ return out;
189
+ }
190
+
191
+ fn weight(m: u32) -> f32 {
192
+ return params.weights[m / 4u][m % 4u];
193
+ }
194
+
195
+ @compute @workgroup_size(64)
196
+ fn place(@builtin(global_invocation_id) id: vec3u) {
197
+ let c = id.x;
198
+ if (c >= params.cells * params.cells) { return; }
199
+ let out = params.slot * params.per_chunk + c;
200
+ let i = c % params.cells;
201
+ let j = c / params.cells;
202
+ let gi = params.gi0 + i32(i);
203
+ let gj = params.gj0 + i32(j);
204
+ instances[out] = vec4u(0u);
205
+ let accept = cell_random(gi, gj, 0u);
206
+ if (accept >= params.accept) { return; }
207
+ let lo = (1.0 - params.jitter) * 0.5;
208
+ let s = (f32(i) + lo + params.jitter * cell_random(gi, gj, 1u)) / f32(params.cells);
209
+ let t = (f32(j) + lo + params.jitter * cell_random(gi, gj, 2u)) / f32(params.cells);
210
+ let p = patch_at(s, t);
211
+ if (accept >= params.accept * p.density) { return; }
212
+ for (var k = 0u; k < params.avoid_count; k++) {
213
+ let a = avoid[params.avoid_base + k];
214
+ let d = p.position - a.xyz;
215
+ if (dot(d, d) < a.w * a.w) { return; }
216
+ }
217
+ var radial = vec3f(0.0, 1.0, 0.0);
218
+ if (params.radial == 1u) { radial = normalize(params.origin.xyz + p.position); }
219
+ let up = normalize(mix(radial, p.normal, params.align));
220
+ let pick = cell_random(gi, gj, 3u);
221
+ var mesh = params.mesh_count - 1u;
222
+ for (var m = 0u; m < params.mesh_count; m++) {
223
+ if (pick < weight(m)) { mesh = m; break; }
224
+ }
225
+ let q = clamp((p.position - params.bmin.xyz) / max(params.bsize.xyz, vec3f(1e-6)), vec3f(0.0), vec3f(1.0));
226
+ let qi = vec3u(round(q * 65535.0));
227
+ let o = vec2u(round(oct_encode(up) * 255.0));
228
+ let yaw = u32(cell_random(gi, gj, 5u) * 256.0) & 0xffu;
229
+ let scale = u32(round(cell_random(gi, gj, 6u) * 255.0));
230
+ let shade = u32(round(cell_random(gi, gj, 7u) * 255.0));
231
+ let rank = u32(round(cell_random(gi, gj, 8u) * 255.0));
232
+ instances[out] = vec4u(
233
+ qi.x | (qi.y << 16u),
234
+ qi.z | (mesh << 16u) | (PLACED << 24u),
235
+ o.x | (o.y << 8u) | (yaw << 16u) | (scale << 24u),
236
+ shade | (rank << 8u),
237
+ );
238
+ }
239
+ `,
240
+ "shard::foliage::cull": `
241
+ import shard::foliage::common::{ FoliageChunk, decode_instance, keep_share };
242
+
243
+ struct CullParams {
244
+ planes: array<vec4f, 6>,
245
+ /** Camera position (xyz, origin-relative world) and the layer's range (w). */
246
+ camera: vec4f,
247
+ /**
248
+ * Shadow range, where thinning starts (fraction of range), the meshes' largest radius, and the
249
+ * share of instances kept everywhere (density, 0075's budget: 1 at full detail).
250
+ */
251
+ ranges: vec4f,
252
+ /** Where each LOD level after the first begins, as fractions of the range (up to 4). */
253
+ lods: vec4f,
254
+ per_chunk: u32,
255
+ chunk_count: u32,
256
+ /** Meshes placement picks from, and the drawables (each mesh's levels) they draw as. */
257
+ mesh_count: u32,
258
+ drawables: u32,
259
+ scale_min: f32,
260
+ scale_max: f32,
261
+ _p0: u32,
262
+ _p1: u32,
263
+ }
264
+
265
+ /** Per drawable (a mesh's level of detail): index count, first index, base vertex. */
266
+ struct MeshInfo { count: u32, first: u32, base: u32, _p: u32 }
267
+
268
+ @group(0) @binding(0) var<uniform> params: CullParams;
269
+ @group(0) @binding(1) var<storage, read> instances: array<vec4u>;
270
+ @group(0) @binding(2) var<storage, read> chunks: array<FoliageChunk>;
271
+ @group(0) @binding(3) var<storage, read_write> visible: array<u32>;
272
+ @group(0) @binding(4) var<storage, read_write> args: array<atomic<u32>>;
273
+ @group(0) @binding(5) var<storage, read> meshes: array<MeshInfo>;
274
+ /** Per instance: its drawable in the camera's list and in the shadow list (+1; 0: neither). */
275
+ @group(0) @binding(6) var<storage, read_write> classes: array<u32>;
276
+ @group(0) @binding(7) var<storage, read_write> counts: array<atomic<u32>>;
277
+ /** Per mesh placement picks: its first drawable and how many levels it has. */
278
+ @group(0) @binding(8) var<storage, read> levels: array<vec2u>;
279
+
280
+ /** One args record per (list, drawable): the camera's list (0) and its shadow casters (1). */
281
+ @compute @workgroup_size(64)
282
+ fn reset(@builtin(global_invocation_id) id: vec3u) {
283
+ let k = id.x;
284
+ if (k >= params.drawables * 2u) { return; }
285
+ let mesh = meshes[k % params.drawables];
286
+ atomicStore(&args[k * 5u], mesh.count);
287
+ atomicStore(&args[k * 5u + 1u], 0u);
288
+ atomicStore(&args[k * 5u + 2u], mesh.first);
289
+ atomicStore(&args[k * 5u + 3u], mesh.base);
290
+ atomicStore(&args[k * 5u + 4u], 0u);
291
+ atomicStore(&counts[k], 0u);
292
+ }
293
+
294
+ /** Pass 1: which list and drawable each instance lands in, counted per drawable. */
295
+ @compute @workgroup_size(64)
296
+ fn classify(@builtin(global_invocation_id) id: vec3u, @builtin(num_workgroups) groups: vec3u) {
297
+ let n = id.x + id.y * groups.x * 64u;
298
+ if (n >= params.chunk_count * params.per_chunk) { return; }
299
+ classes[n] = 0u;
300
+ let chunk = chunks[n / params.per_chunk];
301
+ if (chunk.bmin.w < 0.5) { return; }
302
+ let inst = decode_instance(instances[n], chunk, params.scale_min, params.scale_max);
303
+ if (!inst.placed) { return; }
304
+ let world = vec3f(dot(chunk.row0.xyz, inst.position) + chunk.row0.w, dot(chunk.row1.xyz, inst.position) + chunk.row1.w, dot(chunk.row2.xyz, inst.position) + chunk.row2.w);
305
+ let d = distance(world, params.camera.xyz);
306
+ let range = params.camera.w;
307
+ // Its level by distance, then the drawable for that level.
308
+ let lod = levels[inst.mesh];
309
+ let f = d / max(range, 1e-3);
310
+ var level = 0u;
311
+ for (var l = 0u; l + 1u < lod.y && l < 4u; l++) {
312
+ if (f >= params.lods[l]) { level = l + 1u; }
313
+ }
314
+ let drawable = lod.x + level;
315
+ var kind = 0u;
316
+ if (d < params.ranges.x && inst.rank <= params.ranges.w) {
317
+ kind = (drawable + 1u) << 16u;
318
+ atomicAdd(&counts[params.drawables + drawable], 1u);
319
+ }
320
+ if (d < range && inst.rank <= keep_share(f, params.ranges.y) * params.ranges.w) {
321
+ let r = params.ranges.z * inst.scale * 2.0;
322
+ var inside = true;
323
+ for (var p = 0; p < 6; p++) {
324
+ let plane = params.planes[p];
325
+ if (dot(plane.xyz, world) + plane.w < -r) { inside = false; }
326
+ }
327
+ if (inside) {
328
+ kind = kind | (drawable + 1u);
329
+ atomicAdd(&counts[drawable], 1u);
330
+ }
331
+ }
332
+ classes[n] = kind;
333
+ }
334
+
335
+ /** Pass 2: each drawable's first instance in the visible list (an exclusive prefix sum). */
336
+ @compute @workgroup_size(1)
337
+ fn scan() {
338
+ var start = 0u;
339
+ for (var k = 0u; k < params.drawables * 2u; k++) {
340
+ atomicStore(&args[k * 5u + 4u], start);
341
+ start += atomicLoad(&counts[k]);
342
+ }
343
+ }
344
+
345
+ /** Pass 3: instances into their drawables' ranges; the instance counts end as the draws' counts. */
346
+ @compute @workgroup_size(64)
347
+ fn scatter(@builtin(global_invocation_id) id: vec3u, @builtin(num_workgroups) groups: vec3u) {
348
+ let n = id.x + id.y * groups.x * 64u;
349
+ if (n >= params.chunk_count * params.per_chunk) { return; }
350
+ let kind = classes[n];
351
+ if (kind == 0u) { return; }
352
+ let own = kind & 0xffffu;
353
+ if (own != 0u) {
354
+ let k = own - 1u;
355
+ let slot = atomicAdd(&args[k * 5u + 1u], 1u);
356
+ visible[atomicLoad(&args[k * 5u + 4u]) + slot] = n;
357
+ }
358
+ let shadow = kind >> 16u;
359
+ if (shadow != 0u) {
360
+ let k = params.drawables + shadow - 1u;
361
+ let slot = atomicAdd(&args[k * 5u + 1u], 1u);
362
+ visible[atomicLoad(&args[k * 5u + 4u]) + slot] = n;
363
+ }
364
+ }
365
+ `,
366
+ "shard::foliage::vertex": `
367
+ import shard::pbr::types::VertexOutput;
368
+ import shard::mesh::{ Instance, mesh_vertex };
369
+ import shard::foliage::common::{ FoliageChunk, decode_instance, instance_rows, keep_share };
370
+
371
+ /** What a draw needs per camera: where it is (thinning and fade), the layer's range and scale. */
372
+ struct FoliageDraw {
373
+ camera: vec4f,
374
+ /** Where thinning starts (fraction of range), scale min, scale max, instances per chunk. */
375
+ params: vec4f,
376
+ }
377
+
378
+ @data @group(2) @binding(0) var<storage, read> foliage_instances: array<vec4u>;
379
+ @data @group(2) @binding(1) var<storage, read> foliage_visible: array<u32>;
380
+ @data @group(2) @binding(2) var<storage, read> foliage_chunks: array<FoliageChunk>;
381
+ @group(2) @binding(3) var<uniform> foliage_draw: FoliageDraw;
382
+
383
+ /**
384
+ * mesh_vertex for a foliage instance: decoded, thinned blades widened, the far edge of the range
385
+ * shrunk to nothing, then the material's vertex hooks as for any mesh. InstanceData reads
386
+ * (fade, shade).
387
+ */
388
+ fn foliage_vertex(instance_index: u32, position: vec3f, normal: vec3f, uv: vec2f, uv1: vec2f, tangent: vec4f) -> VertexOutput {
389
+ let n = foliage_visible[instance_index];
390
+ let per_chunk = u32(foliage_draw.params.w);
391
+ let chunk = foliage_chunks[n / per_chunk];
392
+ let inst = decode_instance(foliage_instances[n], chunk, foliage_draw.params.y, foliage_draw.params.z);
393
+ let world = vec3f(dot(chunk.row0.xyz, inst.position) + chunk.row0.w, dot(chunk.row1.xyz, inst.position) + chunk.row1.w, dot(chunk.row2.xyz, inst.position) + chunk.row2.w);
394
+ let f = distance(world, foliage_draw.camera.xyz) / max(foliage_draw.camera.w, 1e-3);
395
+ let keep = keep_share(f, foliage_draw.params.x);
396
+ let fade = 1.0 - smoothstep(0.88, 1.0, f);
397
+ let wide = inverseSqrt(keep);
398
+ let s = inst.scale * fade;
399
+ var rows = instance_rows(chunk, inst, vec3f(s * wide, s, s * wide));
400
+ var record: Instance;
401
+ record.row0 = rows[0];
402
+ record.row1 = rows[1];
403
+ record.row2 = rows[2];
404
+ record.batch = 0u;
405
+ record.flags = 7u;
406
+ record.range = pack2x16float(vec2f(fade, inst.shade));
407
+ record.entity = 0u;
408
+ return mesh_vertex(record, position, normal, uv, uv1, tangent);
409
+ }
410
+ `,
411
+ "shard::foliage::forward": `
412
+ import shard::view::view;
413
+ import shard::pbr::types::VertexOutput;
414
+ import shard::foliage::vertex::foliage_vertex;
415
+ import shard::pbr::material::{ pbr_input, fragment_output };
416
+ import shard::pbr::standard::material;
417
+ import shard::pbr::lighting::apply_lighting;
418
+
419
+ @vertex fn vs(
420
+ @builtin(instance_index) instance_index: u32,
421
+ @location(0) position: vec3f,
422
+ @location(1) normal: vec3f,
423
+ @location(2) uv: vec2f,
424
+ @location(3) uv1: vec2f,
425
+ @location(4) tangent: vec4f,
426
+ ) -> VertexOutput {
427
+ var out = foliage_vertex(instance_index, position, normal, uv, uv1, tangent);
428
+ out.clip = view.viewProj * vec4f(out.world_position, 1.0);
429
+ return out;
430
+ }
431
+
432
+ @fragment fn fs(in: VertexOutput, @builtin(front_facing) front: bool) -> @location(0) vec4f {
433
+ var p = pbr_input(in);
434
+ if (material.alphaMode == 1u && p.alpha < material.alphaCutoff) { discard; }
435
+ // Two-sided: the back of a blade faces the viewer.
436
+ if (!front) { p.normal = -p.normal; }
437
+ let color = (apply_lighting(p, in.world_position, in.clip, in.flags) + p.emissive) * view.exposure;
438
+ return vec4f(fragment_output(vec4f(color, p.alpha)).rgb, 1.0);
439
+ }
440
+ `,
441
+ "shard::foliage::gbuffer": `
442
+ import shard::view::view;
443
+ import shard::pbr::types::VertexOutput;
444
+ import shard::foliage::vertex::foliage_vertex;
445
+ import shard::pbr::material::pbr_input;
446
+ import shard::pbr::standard::material;
447
+ import shard::pbr::gbuffer::{ GBufferOutput, pack_gbuffer };
448
+
449
+ @vertex fn vs(
450
+ @builtin(instance_index) instance_index: u32,
451
+ @location(0) position: vec3f,
452
+ @location(1) normal: vec3f,
453
+ @location(2) uv: vec2f,
454
+ @location(3) uv1: vec2f,
455
+ @location(4) tangent: vec4f,
456
+ ) -> VertexOutput {
457
+ var out = foliage_vertex(instance_index, position, normal, uv, uv1, tangent);
458
+ out.clip = view.viewProj * vec4f(out.world_position, 1.0);
459
+ return out;
460
+ }
461
+
462
+ @fragment fn fs(in: VertexOutput, @builtin(front_facing) front: bool) -> GBufferOutput {
463
+ var p = pbr_input(in);
464
+ @if(MASK) if (p.alpha < material.alphaCutoff) { discard; }
465
+ if (!front) { p.normal = -p.normal; }
466
+ return pack_gbuffer(p, true, view.exposure);
467
+ }
468
+ `,
469
+ "shard::foliage::shadow": `
470
+ import shard::pbr::types::VertexOutput;
471
+ import shard::foliage::vertex::foliage_vertex;
472
+ import shard::pbr::material::pbr_input;
473
+ import shard::pbr::standard::material;
474
+
475
+ struct ShadowView { view_proj: mat4x4f }
476
+ @group(0) @binding(0) var<uniform> shadow_view: ShadowView;
477
+
478
+ @vertex fn vs(
479
+ @builtin(instance_index) instance_index: u32,
480
+ @location(0) position: vec3f,
481
+ @location(1) normal: vec3f,
482
+ @location(2) uv: vec2f,
483
+ @location(3) uv1: vec2f,
484
+ @location(4) tangent: vec4f,
485
+ ) -> VertexOutput {
486
+ var out = foliage_vertex(instance_index, position, normal, uv, uv1, tangent);
487
+ out.clip = shadow_view.view_proj * vec4f(out.world_position, 1.0);
488
+ return out;
489
+ }
490
+
491
+ @fragment fn fs_mask(in: VertexOutput) {
492
+ let p = pbr_input(in);
493
+ if (p.alpha < material.alphaCutoff) { discard; }
494
+ }
495
+ `
496
+ };
497
+ export {
498
+ CHUNK_FLOATS,
499
+ CULL_PARAMS_FLOATS,
500
+ FOLIAGE_SHADERS,
501
+ MAX_FOLIAGE_MESHES,
502
+ PATCH_FLOATS,
503
+ PLACE_PARAMS_BYTES
504
+ };