@aethervtt/shard-scatter 0.0.0-stage → 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.
@@ -0,0 +1,149 @@
1
+ import { t } from "@aethervtt/shard-core";
2
+ import { defineMaterial } from "@aethervtt/shard-render";
3
+ const Vegetation = defineMaterial("scatter/Vegetation", {
4
+ fields: {
5
+ baseTint: t.color({
6
+ default: [0.36, 0.27, 0.19, 1],
7
+ description: "Color at part 0 (bark, stone, a blade\u2019s root), times baseColor."
8
+ }),
9
+ tipTint: t.color({
10
+ default: [0.32, 0.55, 0.18, 1],
11
+ description: "Color at part 1 (leaves, a blade\u2019s tip), times baseColor."
12
+ }),
13
+ shade: t.f32({
14
+ default: 0.25,
15
+ min: 0,
16
+ max: 1,
17
+ description: "How much the per-vertex shade varies brightness (\xB1)."
18
+ }),
19
+ sway: t.f32({
20
+ default: 0,
21
+ min: 0,
22
+ description: "Wind response: metres a tip moves at wind strength 1 (0: still)."
23
+ }),
24
+ windDirection: t.vec3({
25
+ default: [1, 0, 0],
26
+ description: "World-space wind direction (the Wind resource writes it each frame)."
27
+ }),
28
+ windStrength: t.f32({ default: 1, min: 0, description: "Written from the Wind resource." }),
29
+ gustScale: t.f32({
30
+ default: 0.05,
31
+ min: 0,
32
+ unit: "1/m",
33
+ description: "Gust waves per metre (written from the Wind resource)."
34
+ })
35
+ },
36
+ shader: "scatter::vegetation",
37
+ plugin: "scatterPlugin",
38
+ description: "Scatter items\u2019 material: the standard material tinted from base to tip by the generators\u2019 part codes (uv1), with wind sway on the tips. Engine generator items get one by default."
39
+ });
40
+ const SWAY_WGSL = (u) => `
41
+ /** An integer hash to [0, 1) (no trig: this runs for every vertex of every blade). */
42
+ fn scatter_hash(c: vec2i) -> f32 {
43
+ var h = bitcast<u32>(c.x) * 747796405u + bitcast<u32>(c.y) * 2891336453u;
44
+ h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u;
45
+ return f32((h >> 22u) ^ h) * (1.0 / 4294967296.0);
46
+ }
47
+
48
+ /** Smooth gusts in [0, 1]: value noise moving downwind. */
49
+ fn scatter_gust(p: vec2f) -> f32 {
50
+ let i = floor(p);
51
+ let f = p - i;
52
+ let s = f * f * (3.0 - 2.0 * f);
53
+ let c = vec2i(i);
54
+ let a = mix(scatter_hash(c), scatter_hash(c + vec2i(1, 0)), s.x);
55
+ let b = mix(scatter_hash(c + vec2i(0, 1)), scatter_hash(c + vec2i(1, 1)), s.x);
56
+ return mix(a, b, s.y);
57
+ }
58
+
59
+ fn scatter_sway(position: vec3f) -> vec3f {
60
+ let amount = ${u}.sway * ${u}.windStrength;
61
+ if (amount <= 0.0) { return position; }
62
+ let part = vertex_uv1().x;
63
+ // The instance's axes and origin, straight from its transform's rows.
64
+ let inst = mesh_current_instance;
65
+ let ex = vec3f(inst.row0.x, inst.row1.x, inst.row2.x);
66
+ let ey = vec3f(inst.row0.y, inst.row1.y, inst.row2.y);
67
+ let ez = vec3f(inst.row0.z, inst.row1.z, inst.row2.z);
68
+ let origin = vec3f(inst.row0.w, inst.row1.w, inst.row2.w);
69
+ let up = normalize(ey);
70
+ var wind = ${u}.windDirection;
71
+ wind = wind - up * dot(wind, up);
72
+ let len = length(wind);
73
+ if (len < 1e-5) { return position; }
74
+ wind = wind / len;
75
+ // Gusts sweep downwind; a fast flutter rides on them.
76
+ let side = cross(up, wind);
77
+ let along = vec2f(dot(origin, wind), dot(origin, side)) * ${u}.gustScale;
78
+ let gust = scatter_gust(along - vec2f(globals.time * 0.6, 0.0));
79
+ let flutter = sin(globals.time * 3.1 + dot(origin, vec3f(0.37, 0.21, 0.43)) * 2.0) * 0.25;
80
+ let push = amount * part * part * (0.35 + 0.65 * gust + flutter * 0.3);
81
+ let w = wind * push;
82
+ // World direction to object space (rows of a rotation times uniform scale).
83
+ let local = vec3f(dot(ex, w) / dot(ex, ex), dot(ey, w) / dot(ey, ey), dot(ez, w) / dot(ez, ez));
84
+ // Bending down as it leans keeps the blade's length.
85
+ return position + local - vec3f(0.0, 0.5 * dot(local, local) * part, 0.0);
86
+ }
87
+ `;
88
+ const SCATTER_SHADERS = {
89
+ "scatter::vegetation": `
90
+ import shard::pbr::types::{ VertexOutput, PbrInput };
91
+ import shard::pbr::standard::standard_input;
92
+ import shard::globals::globals;
93
+ import shard::mesh::{ vertex_uv1, mesh_current_instance };
94
+ import material::vegetation::Vegetation;
95
+ ${SWAY_WGSL("Vegetation")}
96
+ override fn vertex_position(position: vec3f, normal: vec3f, uv: vec2f) -> vec3f {
97
+ return scatter_sway(position);
98
+ }
99
+
100
+ override fn pbr_input(in: VertexOutput) -> PbrInput {
101
+ var p = standard_input(in);
102
+ let tint = mix(Vegetation.baseTint.rgb, Vegetation.tipTint.rgb, clamp(in.uv1.x, 0.0, 1.0));
103
+ let shade = 1.0 + Vegetation.shade * (in.uv1.y * 2.0 - 1.0);
104
+ p.base_color = p.base_color * tint * shade;
105
+ return p;
106
+ }
107
+ `
108
+ };
109
+ const GENERATOR_LOOKS = {
110
+ "shard/Rock": {
111
+ baseTint: [0.42, 0.4, 0.37, 1],
112
+ tipTint: [0.42, 0.4, 0.37, 1],
113
+ roughness: 0.9,
114
+ sway: 0
115
+ },
116
+ "shard/Tree": {
117
+ baseTint: [0.3, 0.22, 0.15, 1],
118
+ tipTint: [0.22, 0.42, 0.12, 1],
119
+ roughness: 0.8,
120
+ sway: 0.06,
121
+ doubleSided: true
122
+ },
123
+ "shard/Bush": {
124
+ baseTint: [0.28, 0.21, 0.14, 1],
125
+ tipTint: [0.2, 0.38, 0.13, 1],
126
+ roughness: 0.85,
127
+ sway: 0.05,
128
+ doubleSided: true
129
+ },
130
+ "shard/GrassClump": {
131
+ baseTint: [0.12, 0.22, 0.06, 1],
132
+ tipTint: [0.42, 0.58, 0.2, 1],
133
+ roughness: 0.75,
134
+ sway: 0.12,
135
+ doubleSided: true
136
+ },
137
+ "shard/Crystal": {
138
+ baseTint: [0.35, 0.55, 0.75, 1],
139
+ tipTint: [0.7, 0.9, 1, 1],
140
+ roughness: 0.15,
141
+ sway: 0
142
+ }
143
+ };
144
+ export {
145
+ GENERATOR_LOOKS,
146
+ SCATTER_SHADERS,
147
+ SWAY_WGSL,
148
+ Vegetation
149
+ };
@@ -0,0 +1,79 @@
1
+ import { type Entity, type Query, type World } from '@aethervtt/shard-core';
2
+ import { type Mesh } from '@aethervtt/shard-mesh';
3
+ import { type NoiseGraph } from '@aethervtt/shard-noise';
4
+ import type { Workers } from '@aethervtt/shard-platform';
5
+ import { type CompiledRule } from './rules.js';
6
+ import { type ChunkPlacements, type FoliagePatch, type PatchJob, type PlacementJob, type Surface, type SurfaceChunk } from './surface.js';
7
+ /**
8
+ * The mesh's triangles bucketed on an XZ grid, for vertical rays: the highest hit under a point,
9
+ * its height and its face normal. Built once per mesh version, in the surface's metres.
10
+ */
11
+ export declare class HeightIndex {
12
+ readonly positions: Float64Array;
13
+ readonly indices: Uint32Array;
14
+ readonly minX: number;
15
+ readonly minZ: number;
16
+ readonly maxX: number;
17
+ readonly maxZ: number;
18
+ readonly minY: number;
19
+ readonly maxY: number;
20
+ readonly cell: number;
21
+ readonly cols: number;
22
+ readonly rows: number;
23
+ /** Triangles per bucket: bucket b holds tris[start[b] .. start[b + 1]). */
24
+ readonly start: Int32Array;
25
+ readonly tris: Int32Array;
26
+ constructor(mesh: Mesh, scale: number);
27
+ private clampCol;
28
+ private clampRow;
29
+ /**
30
+ * The highest surface under (x, z): writes [height, nx, ny, nz] (the face normal, facing up) into
31
+ * `out` and returns true, or false where the mesh isn't.
32
+ */
33
+ heightAt(x: number, z: number, out: Float64Array): boolean;
34
+ }
35
+ /**
36
+ * A mesh entity's scatter surface (`ScatterSurface`): rules on a square lattice in its local XZ
37
+ * plane (in metres: the entity's uniform scale is applied), heights from vertical rays.
38
+ */
39
+ export declare class MeshSurface implements Surface {
40
+ readonly kind: 'mesh' | 'heightfield';
41
+ readonly entity: Entity;
42
+ rules: CompiledRule[];
43
+ masks: (NoiseGraph | undefined)[];
44
+ version: number;
45
+ index: HeightIndex | undefined;
46
+ /** The entity's uniform scale: local units to metres. */
47
+ scale: number;
48
+ /** Per rule: cells per chunk side, and chunk size (m). */
49
+ cells: number[];
50
+ sizes: number[];
51
+ private readonly cameras;
52
+ private readonly anchors;
53
+ private readonly inverse;
54
+ constructor(world: World, entity: Entity, cameras: Query);
55
+ get ready(): boolean;
56
+ /** Takes the mesh (its version changes rebuild the index) and the entity's scale. */
57
+ setMesh(mesh: Mesh, scale: number): boolean;
58
+ private mesh;
59
+ private meshVersion;
60
+ configure(rules: CompiledRule[], masks: (NoiseGraph | undefined)[], version: number): void;
61
+ viewers(world: World, out: {
62
+ points: Float64Array;
63
+ cameras: number;
64
+ anchors: number;
65
+ }): void;
66
+ chunksNear(rule: CompiledRule, viewers: Float64Array, first: number, count: number, range: number, visit: (chunk: SurfaceChunk, distance: number) => void): void;
67
+ chunk(rule: CompiledRule, x: number, z: number): SurfaceChunk;
68
+ chunkAt(rule: CompiledRule, x: number, _y: number, z: number): SurfaceChunk;
69
+ upAt(_x: number, _y: number, _z: number, out: Float64Array): Float64Array;
70
+ placeChunkRoot(world: World, root: Entity, chunk: SurfaceChunk): void;
71
+ startPlacement(chunk: SurfaceChunk, due: number, _workers: Workers | undefined): PlacementJob;
72
+ finishPlacement(job: PlacementJob): ChunkPlacements;
73
+ startPatch(chunk: SurfaceChunk, _workers: Workers | undefined): PatchJob;
74
+ /** The chunk's ground on a 33 × 33 grid of rays, with the rule's density per vertex. */
75
+ finishPatch(job: PatchJob): FoliagePatch;
76
+ chunkTransform(world: World, chunk: SurfaceChunk, out: Float32Array): void;
77
+ /** A chunk's placements: lattice candidates, rays down, masks, transforms. */
78
+ place(rule: CompiledRule, chunk: SurfaceChunk, k: number): ChunkPlacements;
79
+ }
@@ -0,0 +1,470 @@
1
+ import { affine, ChildOf } from "@aethervtt/shard-core";
2
+ import { DEG, dcos } from "@aethervtt/shard-mesh";
3
+ import { sampleNoise } from "@aethervtt/shard-noise";
4
+ import { TerrainAnchor } from "@aethervtt/shard-terrain";
5
+ import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
6
+ import { writeRotation } from "./planet.js";
7
+ import {
8
+ cellRandom,
9
+ FOLIAGE,
10
+ inRange,
11
+ jitterFor,
12
+ MAX_FOLIAGE_PER_CHUNK,
13
+ pickItem,
14
+ STREAM_ACCEPT,
15
+ STREAM_ITEM,
16
+ STREAM_SCALE,
17
+ STREAM_SHADE,
18
+ STREAM_VARIANT,
19
+ STREAM_X,
20
+ STREAM_Y,
21
+ STREAM_YAW
22
+ } from "./rules.js";
23
+ import {
24
+ P_CELL,
25
+ P_ITEM,
26
+ P_QX,
27
+ P_SCALE,
28
+ P_SHADE,
29
+ P_VARIANT,
30
+ P_X,
31
+ PLACEMENT_STRIDE
32
+ } from "./surface.js";
33
+ import { cameraPositions } from "./viewers.js";
34
+ const TARGET_PROPS = 256;
35
+ const FOLIAGE_CHUNK = 32;
36
+ const PATCH_GRID = 33;
37
+ class HeightIndex {
38
+ positions;
39
+ indices;
40
+ minX;
41
+ minZ;
42
+ maxX;
43
+ maxZ;
44
+ minY;
45
+ maxY;
46
+ cell;
47
+ cols;
48
+ rows;
49
+ /** Triangles per bucket: bucket b holds tris[start[b] .. start[b + 1]). */
50
+ start;
51
+ tris;
52
+ constructor(mesh, scale) {
53
+ const p = mesh.positions;
54
+ const n = p.length / 3;
55
+ this.positions = new Float64Array(n * 3);
56
+ for (let i = 0; i < n * 3; i++) this.positions[i] = p[i] * scale;
57
+ const idx = mesh.indices;
58
+ this.indices = idx ? Uint32Array.from(idx) : Uint32Array.from({ length: n }, (_, i) => i);
59
+ let minX = Infinity;
60
+ let minZ = Infinity;
61
+ let maxX = -Infinity;
62
+ let maxZ = -Infinity;
63
+ let minY = Infinity;
64
+ let maxY = -Infinity;
65
+ for (let i = 0; i < n; i++) {
66
+ const x = this.positions[i * 3];
67
+ const y = this.positions[i * 3 + 1];
68
+ const z = this.positions[i * 3 + 2];
69
+ minX = Math.min(minX, x);
70
+ maxX = Math.max(maxX, x);
71
+ minY = Math.min(minY, y);
72
+ maxY = Math.max(maxY, y);
73
+ minZ = Math.min(minZ, z);
74
+ maxZ = Math.max(maxZ, z);
75
+ }
76
+ this.minX = minX;
77
+ this.minZ = minZ;
78
+ this.maxX = maxX;
79
+ this.maxZ = maxZ;
80
+ this.minY = minY;
81
+ this.maxY = maxY;
82
+ const tris = this.indices.length / 3;
83
+ const area = Math.max(1e-6, (maxX - minX) * (maxZ - minZ));
84
+ this.cell = Math.max(1e-3, Math.sqrt(area * 4 / Math.max(1, tris)));
85
+ this.cols = Math.max(1, Math.ceil((maxX - minX) / this.cell));
86
+ this.rows = Math.max(1, Math.ceil((maxZ - minZ) / this.cell));
87
+ const buckets = this.cols * this.rows;
88
+ const counts = new Int32Array(buckets + 1);
89
+ const range = (t, visit) => {
90
+ const pp = this.positions;
91
+ const a = this.indices[t * 3] * 3;
92
+ const b = this.indices[t * 3 + 1] * 3;
93
+ const c = this.indices[t * 3 + 2] * 3;
94
+ const x0 = Math.min(pp[a], pp[b], pp[c]);
95
+ const x1 = Math.max(pp[a], pp[b], pp[c]);
96
+ const z0 = Math.min(pp[a + 2], pp[b + 2], pp[c + 2]);
97
+ const z1 = Math.max(pp[a + 2], pp[b + 2], pp[c + 2]);
98
+ const i0 = this.clampCol(Math.floor((x0 - minX) / this.cell));
99
+ const i1 = this.clampCol(Math.floor((x1 - minX) / this.cell));
100
+ const j0 = this.clampRow(Math.floor((z0 - minZ) / this.cell));
101
+ const j1 = this.clampRow(Math.floor((z1 - minZ) / this.cell));
102
+ for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) visit(i + j * this.cols);
103
+ };
104
+ for (let t = 0; t < tris; t++) range(t, (b) => counts[b + 1]++);
105
+ for (let b = 0; b < buckets; b++) counts[b + 1] += counts[b];
106
+ this.start = counts.slice();
107
+ const fill = counts.slice();
108
+ this.tris = new Int32Array(counts[buckets]);
109
+ for (let t = 0; t < tris; t++) range(t, (b) => this.tris[fill[b]++] = t);
110
+ }
111
+ clampCol(i) {
112
+ return Math.max(0, Math.min(this.cols - 1, i));
113
+ }
114
+ clampRow(j) {
115
+ return Math.max(0, Math.min(this.rows - 1, j));
116
+ }
117
+ /**
118
+ * The highest surface under (x, z): writes [height, nx, ny, nz] (the face normal, facing up) into
119
+ * `out` and returns true, or false where the mesh isn't.
120
+ */
121
+ heightAt(x, z, out) {
122
+ if (x < this.minX || x > this.maxX || z < this.minZ || z > this.maxZ) return false;
123
+ const i = this.clampCol(Math.floor((x - this.minX) / this.cell));
124
+ const j = this.clampRow(Math.floor((z - this.minZ) / this.cell));
125
+ const b = i + j * this.cols;
126
+ const p = this.positions;
127
+ let found = false;
128
+ let best = -Infinity;
129
+ for (let s = this.start[b]; s < this.start[b + 1]; s++) {
130
+ const t = this.tris[s];
131
+ const a = this.indices[t * 3] * 3;
132
+ const bb = this.indices[t * 3 + 1] * 3;
133
+ const c = this.indices[t * 3 + 2] * 3;
134
+ const ax = p[a];
135
+ const az = p[a + 2];
136
+ const bx = p[bb];
137
+ const bz = p[bb + 2];
138
+ const cx = p[c];
139
+ const cz = p[c + 2];
140
+ const d = (bz - cz) * (ax - cx) + (cx - bx) * (az - cz);
141
+ if (Math.abs(d) < 1e-12) continue;
142
+ const l1 = ((bz - cz) * (x - cx) + (cx - bx) * (z - cz)) / d;
143
+ const l2 = ((cz - az) * (x - cx) + (ax - cx) * (z - cz)) / d;
144
+ const l3 = 1 - l1 - l2;
145
+ const eps = -1e-9;
146
+ if (l1 < eps || l2 < eps || l3 < eps) continue;
147
+ const y = l1 * p[a + 1] + l2 * p[bb + 1] + l3 * p[c + 1];
148
+ if (y <= best) continue;
149
+ best = y;
150
+ found = true;
151
+ const ux = bx - ax;
152
+ const uy = p[bb + 1] - p[a + 1];
153
+ const uz = bz - az;
154
+ const vx = cx - ax;
155
+ const vy = p[c + 1] - p[a + 1];
156
+ const vz = cz - az;
157
+ let nx = uy * vz - uz * vy;
158
+ let ny = uz * vx - ux * vz;
159
+ let nz = ux * vy - uy * vx;
160
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
161
+ if (ny < 0) {
162
+ nx = -nx;
163
+ ny = -ny;
164
+ nz = -nz;
165
+ }
166
+ out[0] = y;
167
+ out[1] = nx / len;
168
+ out[2] = ny / len;
169
+ out[3] = nz / len;
170
+ }
171
+ return found;
172
+ }
173
+ }
174
+ const hit = new Float64Array(4);
175
+ const one = new Float32Array(1);
176
+ const point = new Float32Array(3);
177
+ class MeshSurface {
178
+ kind = "mesh";
179
+ entity;
180
+ rules = [];
181
+ masks = [];
182
+ version = 0;
183
+ index;
184
+ /** The entity's uniform scale: local units to metres. */
185
+ scale = 1;
186
+ /** Per rule: cells per chunk side, and chunk size (m). */
187
+ cells = [];
188
+ sizes = [];
189
+ cameras;
190
+ anchors;
191
+ inverse = new Float32Array(12);
192
+ constructor(world, entity, cameras) {
193
+ this.entity = entity;
194
+ this.cameras = cameras;
195
+ this.anchors = world.query({ with: [TerrainAnchor, GlobalTransform] });
196
+ }
197
+ get ready() {
198
+ return this.index !== void 0;
199
+ }
200
+ /** Takes the mesh (its version changes rebuild the index) and the entity's scale. */
201
+ setMesh(mesh, scale) {
202
+ if (this.index && this.meshVersion === mesh.version && this.scale === scale && this.mesh === mesh)
203
+ return false;
204
+ this.mesh = mesh;
205
+ this.meshVersion = mesh.version;
206
+ this.scale = scale;
207
+ this.index = new HeightIndex(mesh, scale);
208
+ return true;
209
+ }
210
+ mesh;
211
+ meshVersion = -1;
212
+ configure(rules, masks, version) {
213
+ this.rules = rules;
214
+ this.masks = masks;
215
+ this.version = version;
216
+ this.cells = rules.map((r) => {
217
+ if (!Number.isFinite(r.cell)) return 1;
218
+ if (r.kind === FOLIAGE) {
219
+ let k = Math.max(1, Math.round(FOLIAGE_CHUNK / r.cell));
220
+ while (k > 1 && r.density * (k * r.cell) ** 2 > MAX_FOLIAGE_PER_CHUNK) k--;
221
+ return k;
222
+ }
223
+ const size = r.density > 0 ? Math.sqrt(TARGET_PROPS / r.density) : 64;
224
+ return Math.max(1, Math.round(size / r.cell));
225
+ });
226
+ this.sizes = rules.map((r, i) => Number.isFinite(r.cell) ? r.cell * this.cells[i] : 64);
227
+ }
228
+ viewers(world, out) {
229
+ const m = world.get(this.entity, GlobalTransform).matrix;
230
+ if (!affine.invert(this.inverse, m)) return;
231
+ const inv = this.inverse;
232
+ const s = this.scale;
233
+ const put = (x, y, z, i) => {
234
+ if (out.points.length < (i + 1) * 3) {
235
+ const grown = new Float64Array((i + 1) * 6);
236
+ grown.set(out.points);
237
+ out.points = grown;
238
+ }
239
+ out.points[i * 3] = (inv[0] * x + inv[1] * y + inv[2] * z + inv[3]) * s;
240
+ out.points[i * 3 + 1] = (inv[4] * x + inv[5] * y + inv[6] * z + inv[7]) * s;
241
+ out.points[i * 3 + 2] = (inv[8] * x + inv[9] * y + inv[10] * z + inv[11]) * s;
242
+ };
243
+ const n = cameraPositions(world, this.cameras, put);
244
+ out.cameras = n;
245
+ let a = 0;
246
+ for (const table of this.anchors.tables) {
247
+ const enabled = table.column(TerrainAnchor, "enabled");
248
+ const g = table.column(GlobalTransform, "matrix");
249
+ for (let row = 0; row < table.count; row++) {
250
+ if (!enabled[row]) continue;
251
+ put(g[row * 12 + 3], g[row * 12 + 7], g[row * 12 + 11], n + a);
252
+ a++;
253
+ }
254
+ }
255
+ out.anchors = a;
256
+ }
257
+ chunksNear(rule, viewers, first, count, range, visit) {
258
+ const index = this.index;
259
+ if (!index) return;
260
+ const size = this.sizes[rule.index];
261
+ const x0 = Math.floor(index.minX / size);
262
+ const x1 = Math.floor(index.maxX / size);
263
+ const z0 = Math.floor(index.minZ / size);
264
+ const z1 = Math.floor(index.maxZ / size);
265
+ for (let v = first; v < first + count; v++) {
266
+ const vx = viewers[v * 3];
267
+ const vy = viewers[v * 3 + 1];
268
+ const vz = viewers[v * 3 + 2];
269
+ const ground = index.heightAt(vx, vz, hit) ? hit[0] : index.maxY;
270
+ const altitude = Math.max(0, vy - ground);
271
+ if (altitude > range) continue;
272
+ const reach = Math.ceil(range / size);
273
+ const cx = Math.floor(vx / size);
274
+ const cz = Math.floor(vz / size);
275
+ for (let z = Math.max(z0, cz - reach); z <= Math.min(z1, cz + reach); z++) {
276
+ for (let x = Math.max(x0, cx - reach); x <= Math.min(x1, cx + reach); x++) {
277
+ const dx = Math.max(x * size - vx, 0, vx - (x + 1) * size);
278
+ const dz = Math.max(z * size - vz, 0, vz - (z + 1) * size);
279
+ const distance = Math.sqrt(dx * dx + dz * dz + altitude * altitude);
280
+ if (distance > range) continue;
281
+ visit(this.chunk(rule, x, z), distance);
282
+ }
283
+ }
284
+ }
285
+ }
286
+ chunk(rule, x, z) {
287
+ const size = this.sizes[rule.index];
288
+ const center = new Float64Array(3);
289
+ center[0] = (x + 0.5) * size;
290
+ center[2] = (z + 0.5) * size;
291
+ return {
292
+ key: `${x}/${z}`,
293
+ rule: rule.index,
294
+ face: 0,
295
+ depth: 0,
296
+ x,
297
+ y: z,
298
+ center,
299
+ extent: size * 0.71
300
+ };
301
+ }
302
+ chunkAt(rule, x, _y, z) {
303
+ const size = this.sizes[rule.index];
304
+ return this.chunk(rule, Math.floor(x / size), Math.floor(z / size));
305
+ }
306
+ upAt(_x, _y, _z, out) {
307
+ out[0] = 0;
308
+ out[1] = 1;
309
+ out[2] = 0;
310
+ return out;
311
+ }
312
+ placeChunkRoot(world, root, chunk) {
313
+ const s = this.scale;
314
+ world.add(root, ChildOf, { parent: this.entity });
315
+ world.set(root, Transform, {
316
+ translation: [chunk.center[0] / s, chunk.center[1] / s, chunk.center[2] / s],
317
+ scale: [1 / s, 1 / s, 1 / s]
318
+ });
319
+ }
320
+ startPlacement(chunk, due, _workers) {
321
+ const state = { k: this.cells[chunk.rule], result: void 0 };
322
+ state.result = this.place(this.rules[chunk.rule], chunk, state.k);
323
+ return { chunk, due, ready: true, cancelled: false, state };
324
+ }
325
+ finishPlacement(job) {
326
+ return job.state.result;
327
+ }
328
+ startPatch(chunk, _workers) {
329
+ return { chunk, ready: true, cancelled: false, state: void 0 };
330
+ }
331
+ /** The chunk's ground on a 33 × 33 grid of rays, with the rule's density per vertex. */
332
+ finishPatch(job) {
333
+ const { chunk } = job;
334
+ const rule = this.rules[chunk.rule];
335
+ const index = this.index;
336
+ const size = this.sizes[rule.index];
337
+ const n = PATCH_GRID;
338
+ const count = n * n;
339
+ const positions = new Float32Array(count * 3);
340
+ const normals = new Float32Array(count * 3);
341
+ const density = new Float32Array(count);
342
+ const cosLo = dcos(Math.max(0, rule.slopeLo) * DEG);
343
+ const cosHi = dcos(Math.min(90, rule.slopeHi) * DEG);
344
+ const slopeMask = rule.slopeLo < rule.slopeHi;
345
+ const mask = this.masks[rule.index];
346
+ const x0 = chunk.x * size;
347
+ const z0 = chunk.y * size;
348
+ for (let j = 0; j < n; j++) {
349
+ for (let i = 0; i < n; i++) {
350
+ const k2 = i + j * n;
351
+ const x = x0 + i / (n - 1) * size;
352
+ const z = z0 + j / (n - 1) * size;
353
+ positions[k2 * 3] = x - chunk.center[0];
354
+ positions[k2 * 3 + 2] = z - chunk.center[2];
355
+ normals[k2 * 3 + 1] = 1;
356
+ if (!index.heightAt(x, z, hit)) continue;
357
+ const y = hit[0];
358
+ positions[k2 * 3 + 1] = y - chunk.center[1];
359
+ normals[k2 * 3] = hit[1];
360
+ normals[k2 * 3 + 1] = hit[2];
361
+ normals[k2 * 3 + 2] = hit[3];
362
+ let w = inRange(y, rule.heightLo, rule.heightHi) ? 1 : 0;
363
+ if (w > 0 && slopeMask && (hit[2] > cosLo + 1e-12 || hit[2] < cosHi - 1e-12)) w = 0;
364
+ if (w > 0 && mask) {
365
+ point[0] = x;
366
+ point[1] = y;
367
+ point[2] = z;
368
+ sampleNoise(mask, rule.seed, point, one);
369
+ if (!(one[0] > rule.noiseAbove)) w = 0;
370
+ }
371
+ density[k2] = w;
372
+ }
373
+ }
374
+ const k = this.cells[rule.index];
375
+ const cell = size / k;
376
+ return {
377
+ grid: n,
378
+ positions,
379
+ normals,
380
+ density,
381
+ cell0: [chunk.x * k, chunk.y * k],
382
+ domain: 0,
383
+ accept: Math.min(1, rule.density * cell * cell),
384
+ jitter: jitterFor(cell, rule.spacing),
385
+ radial: void 0
386
+ };
387
+ }
388
+ chunkTransform(world, chunk, out) {
389
+ const g = world.get(this.entity, GlobalTransform).matrix;
390
+ const s = this.scale;
391
+ const x = chunk.center[0] / s;
392
+ const y = chunk.center[1] / s;
393
+ const z = chunk.center[2] / s;
394
+ for (let r = 0; r < 3; r++) {
395
+ out[r * 4] = g[r * 4] / s;
396
+ out[r * 4 + 1] = g[r * 4 + 1] / s;
397
+ out[r * 4 + 2] = g[r * 4 + 2] / s;
398
+ out[r * 4 + 3] = g[r * 4] * x + g[r * 4 + 1] * y + g[r * 4 + 2] * z + g[r * 4 + 3];
399
+ }
400
+ }
401
+ /** A chunk's placements: lattice candidates, rays down, masks, transforms. */
402
+ place(rule, chunk, k) {
403
+ const index = this.index;
404
+ const cell = this.sizes[rule.index] / k;
405
+ const accept = Math.min(1, rule.density * cell * cell);
406
+ const jitter = jitterFor(cell, rule.spacing);
407
+ const lo = (1 - jitter) / 2;
408
+ const out = new Float32Array(k * k * PLACEMENT_STRIDE);
409
+ const cosLo = dcos(Math.max(0, rule.slopeLo) * DEG);
410
+ const cosHi = dcos(Math.min(90, rule.slopeHi) * DEG);
411
+ const slopeMask = rule.slopeLo < rule.slopeHi;
412
+ const mask = this.masks[rule.index];
413
+ let n = 0;
414
+ for (let j = 0; j < k; j++) {
415
+ for (let i = 0; i < k; i++) {
416
+ const gi = chunk.x * k + i;
417
+ const gj = chunk.y * k + j;
418
+ if (cellRandom(rule.seed, gi, gj, 0, STREAM_ACCEPT) >= accept) continue;
419
+ const x = (gi + lo + jitter * cellRandom(rule.seed, gi, gj, 0, STREAM_X)) * cell;
420
+ const z = (gj + lo + jitter * cellRandom(rule.seed, gi, gj, 0, STREAM_Y)) * cell;
421
+ if (!index.heightAt(x, z, hit)) continue;
422
+ const y = hit[0];
423
+ if (!inRange(y, rule.heightLo, rule.heightHi)) continue;
424
+ const cos = hit[2];
425
+ if (slopeMask && (cos > cosLo + 1e-12 || cos < cosHi - 1e-12)) continue;
426
+ if (mask) {
427
+ point[0] = x;
428
+ point[1] = y;
429
+ point[2] = z;
430
+ sampleNoise(mask, rule.seed, point, one);
431
+ if (!(one[0] > rule.noiseAbove)) continue;
432
+ }
433
+ const o = n * PLACEMENT_STRIDE;
434
+ out[o + P_ITEM] = pickItem(rule, cellRandom(rule.seed, gi, gj, 0, STREAM_ITEM));
435
+ const item = rule.items[out[o + P_ITEM]];
436
+ out[o + P_VARIANT] = Math.min(
437
+ item.variants - 1,
438
+ Math.floor(cellRandom(rule.seed, gi, gj, 0, STREAM_VARIANT) * item.variants)
439
+ );
440
+ out[o + P_X] = x - chunk.center[0];
441
+ out[o + P_X + 1] = y - chunk.center[1];
442
+ out[o + P_X + 2] = z - chunk.center[2];
443
+ let ux = hit[1] * rule.align;
444
+ let uy = 1 + (hit[2] - 1) * rule.align;
445
+ let uz = hit[3] * rule.align;
446
+ const ul = Math.sqrt(ux * ux + uy * uy + uz * uz) || 1;
447
+ ux /= ul;
448
+ uy /= ul;
449
+ uz /= ul;
450
+ writeRotation(
451
+ ux,
452
+ uy,
453
+ uz,
454
+ cellRandom(rule.seed, gi, gj, 0, STREAM_YAW) * Math.PI * 2,
455
+ out,
456
+ o + P_QX
457
+ );
458
+ out[o + P_SCALE] = rule.scaleMin + (rule.scaleMax - rule.scaleMin) * cellRandom(rule.seed, gi, gj, 0, STREAM_SCALE);
459
+ out[o + P_CELL] = i + j * k;
460
+ out[o + P_SHADE] = cellRandom(rule.seed, gi, gj, 0, STREAM_SHADE);
461
+ n++;
462
+ }
463
+ }
464
+ return { chunk, count: n, data: out, candidates: k * k };
465
+ }
466
+ }
467
+ export {
468
+ HeightIndex,
469
+ MeshSurface
470
+ };