@aethervtt/shard-terrain 0.1.0 → 0.2.1

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 (76) hide show
  1. package/dist/biomes.d.ts +1 -0
  2. package/dist/biomes.js +3 -0
  3. package/dist/chunk.d.ts +0 -44
  4. package/dist/chunk.js +2 -127
  5. package/dist/colliders.d.ts +16 -1
  6. package/dist/colliders.js +39 -24
  7. package/dist/components.d.ts +5 -0
  8. package/dist/components.js +7 -2
  9. package/dist/cube-sphere.d.ts +50 -0
  10. package/dist/cube-sphere.js +224 -0
  11. package/dist/grid-mesh.d.ts +49 -0
  12. package/dist/grid-mesh.js +208 -0
  13. package/dist/heightfield/bake.d.ts +120 -0
  14. package/dist/heightfield/bake.js +494 -0
  15. package/dist/heightfield/colliders.d.ts +39 -0
  16. package/dist/heightfield/colliders.js +152 -0
  17. package/dist/heightfield/component.d.ts +14 -0
  18. package/dist/heightfield/component.js +59 -0
  19. package/dist/heightfield/deflate.d.ts +11 -0
  20. package/dist/heightfield/deflate.js +484 -0
  21. package/dist/heightfield/heightmap.d.ts +27 -0
  22. package/dist/heightfield/heightmap.js +160 -0
  23. package/dist/heightfield/kernel.d.ts +277 -0
  24. package/dist/heightfield/kernel.js +1033 -0
  25. package/dist/heightfield/material.d.ts +13 -0
  26. package/dist/heightfield/material.js +394 -0
  27. package/dist/heightfield/methods.d.ts +104 -0
  28. package/dist/heightfield/methods.js +218 -0
  29. package/dist/heightfield/pack.d.ts +59 -0
  30. package/dist/heightfield/pack.js +127 -0
  31. package/dist/heightfield/pages.d.ts +91 -0
  32. package/dist/heightfield/pages.js +244 -0
  33. package/dist/heightfield/project.d.ts +40 -0
  34. package/dist/heightfield/project.js +55 -0
  35. package/dist/heightfield/queries.d.ts +48 -0
  36. package/dist/heightfield/queries.js +150 -0
  37. package/dist/heightfield/render.d.ts +108 -0
  38. package/dist/heightfield/render.js +727 -0
  39. package/dist/heightfield/root-grid.d.ts +38 -0
  40. package/dist/heightfield/root-grid.js +109 -0
  41. package/dist/heightfield/runtime.d.ts +105 -0
  42. package/dist/heightfield/runtime.js +312 -0
  43. package/dist/heightfield/source-asset.d.ts +39 -0
  44. package/dist/heightfield/source-asset.js +372 -0
  45. package/dist/heightfield/source.d.ts +151 -0
  46. package/dist/heightfield/source.js +402 -0
  47. package/dist/heightfield/stack.d.ts +29 -0
  48. package/dist/heightfield/stack.js +158 -0
  49. package/dist/heightfield/system.d.ts +22 -0
  50. package/dist/heightfield/system.js +165 -0
  51. package/dist/heightfield/testing.d.ts +317 -0
  52. package/dist/heightfield/testing.js +441 -0
  53. package/dist/heightfield/worker.js +47 -0
  54. package/dist/heights.d.ts +8 -3
  55. package/dist/heights.js +8 -4
  56. package/dist/index.d.ts +24 -7
  57. package/dist/index.js +158 -8
  58. package/dist/lod.d.ts +27 -0
  59. package/dist/lod.js +23 -1
  60. package/dist/material.js +2 -1
  61. package/dist/methods.js +64 -16
  62. package/dist/overlays.d.ts +2 -0
  63. package/dist/overlays.js +48 -6
  64. package/dist/planet.d.ts +2 -7
  65. package/dist/planet.js +8 -18
  66. package/dist/plugin.d.ts +10 -3
  67. package/dist/plugin.js +48 -8
  68. package/dist/quadtree.d.ts +61 -39
  69. package/dist/quadtree.js +44 -208
  70. package/dist/render.d.ts +7 -1
  71. package/dist/render.js +8 -6
  72. package/dist/test-planet.d.ts +39 -0
  73. package/dist/test-planet.js +40 -4
  74. package/dist/testing.d.ts +2 -0
  75. package/dist/testing.js +42 -2
  76. package/package.json +16 -16
@@ -0,0 +1,1033 @@
1
+ // The heightfield bake's kernel (spec 0071): the layer stack evaluated at samples, quantized
2
+ // heights, paint control, normals, exact geometric errors, and page encoding. Plain JavaScript, so
3
+ // pool workers import it directly (no bundler, no TypeScript loader); the main thread runs the same
4
+ // functions for pages it needs at once. Types are in kernel.d.ts.
5
+ //
6
+ // Every sample sits on the leaf lattice: sample (gi, gj) is at x = gi × spacing, z = gj × spacing,
7
+ // computed that one way everywhere. Noise is sampled against a fixed lattice of origins (one per
8
+ // LATTICE metres), so a sample's value never depends on which job or page computed it: a block
9
+ // bake, a single page baked on demand, and a rebake all write the same bytes.
10
+
11
+ import { deflate, inflate } from './deflate.js'
12
+
13
+ /** Segments per page side: a page has PAGE + 1 samples a side. */
14
+ export const PAGE = 64
15
+ /** Samples per side of a page's vertex grid. */
16
+ export const SIDE = PAGE + 1
17
+ /** A leaf page's height samples per side: its grid plus a one-sample border, for normals. */
18
+ export const LEAF_SIDE = SIDE + 2
19
+ /** Leaf pages per block side: the unit of incremental baking. */
20
+ export const BLOCK = 16
21
+ /** Metres between noise origins: inputs stay within LATTICE / 2 of theirs (sub-mm in f32). */
22
+ export const LATTICE = 256
23
+ /** Ancestor levels a block bakes itself (its leaves' parents up to its own root): log2(BLOCK). */
24
+ export const BLOCK_LEVELS = 4
25
+ /** Bumps whenever any baked byte would change for the same inputs. Part of every block key. */
26
+ export const BAKE_VERSION = 1
27
+
28
+ export const BLEND_ADD = 0
29
+ export const BLEND_MAX = 1
30
+ export const BLEND_MIN = 2
31
+ export const BLEND_REPLACE = 3
32
+ export const MODE_FLATTEN = 0
33
+ export const MODE_RAISE = 1
34
+ export const MODE_CARVE = 2
35
+ export const LAYER_NOISE = 0
36
+ export const LAYER_IMAGE = 1
37
+ export const LAYER_SPLINE = 2
38
+
39
+ export { deflate, inflate }
40
+
41
+ // --- small helpers -----------------------------------------------------------------------------
42
+
43
+ function smoothstep(a, b, x) {
44
+ if (!(b > a)) return x < a ? 0 : 1
45
+ const t = Math.min(1, Math.max(0, (x - a) / (b - a)))
46
+ return t * t * (3 - 2 * t)
47
+ }
48
+
49
+ /** 1 inside [lo, hi], easing to 0 over `blend` centred on each end (hard edges at 0). */
50
+ export function windowWeight(x, lo, hi, blend) {
51
+ if (blend <= 0) return x >= lo && x <= hi ? 1 : 0
52
+ const h = blend / 2
53
+ return smoothstep(lo - h, lo + h, x) * (1 - smoothstep(hi - h, hi + h, x))
54
+ }
55
+
56
+ /** Fades from 1 at distance 0 to 0 at `falloff` (a step at 0 falloff). */
57
+ function fade(distance, falloff) {
58
+ if (distance <= 0) return 1
59
+ if (!(falloff > 0)) return 0
60
+ return 1 - smoothstep(0, falloff, distance)
61
+ }
62
+
63
+ /**
64
+ * A rotated rectangle's local coordinates for (x, z): u and v in [0, 1] inside it (u along the
65
+ * rectangle's width, v along its depth), and how far outside it the point is (m).
66
+ */
67
+ function rectLocal(rect, x, z, out) {
68
+ const dx = x - rect.cx
69
+ const dz = z - rect.cz
70
+ // Rotated about +Y by `rotation`: its width axis is (cos, −sin) in (x, z), its depth axis (sin, cos).
71
+ const lu = dx * rect.cos - dz * rect.sin
72
+ const lv = dx * rect.sin + dz * rect.cos
73
+ out[0] = lu / (rect.hw * 2) + 0.5
74
+ out[1] = lv / (rect.hd * 2) + 0.5
75
+ const ou = Math.max(0, Math.abs(lu) - rect.hw)
76
+ const ov = Math.max(0, Math.abs(lv) - rect.hd)
77
+ out[2] = Math.sqrt(ou * ou + ov * ov)
78
+ return out
79
+ }
80
+
81
+ /** Bilinear sample of a heightmap (values 0–1, top row first) at u, v in [0, 1], edges clamped. */
82
+ export function sampleMap(map, u, v) {
83
+ const w = map.width
84
+ const h = map.height
85
+ const fx = Math.min(w - 1, Math.max(0, u * w - 0.5))
86
+ const fy = Math.min(h - 1, Math.max(0, v * h - 0.5))
87
+ const x0 = Math.floor(fx)
88
+ const y0 = Math.floor(fy)
89
+ const x1 = Math.min(w - 1, x0 + 1)
90
+ const y1 = Math.min(h - 1, y0 + 1)
91
+ const tx = fx - x0
92
+ const ty = fy - y0
93
+ const d = map.data
94
+ const a = d[y0 * w + x0]
95
+ const b = d[y0 * w + x1]
96
+ const c = d[y1 * w + x0]
97
+ const e = d[y1 * w + x1]
98
+ return (a * (1 - tx) + b * tx) * (1 - ty) + (c * (1 - tx) + e * tx) * ty
99
+ }
100
+
101
+ // --- splines -----------------------------------------------------------------------------------
102
+
103
+ /**
104
+ * A spline ready for distance queries: its tessellated points (x, y, z) and a grid of buckets of
105
+ * segment indices over XZ, each bucket listing (in index order) the segments within `reach` of it.
106
+ * Built the same way everywhere, so queries are deterministic.
107
+ */
108
+ export function buildSpline(pts, width, falloff, reach) {
109
+ const count = pts.length / 3
110
+ let minX = Infinity
111
+ let minZ = Infinity
112
+ let maxX = -Infinity
113
+ let maxZ = -Infinity
114
+ for (let i = 0; i < count; i++) {
115
+ minX = Math.min(minX, pts[i * 3])
116
+ maxX = Math.max(maxX, pts[i * 3])
117
+ minZ = Math.min(minZ, pts[i * 3 + 2])
118
+ maxZ = Math.max(maxZ, pts[i * 3 + 2])
119
+ }
120
+ // Buckets about half the reach: each lists the few segments that can be within reach of it.
121
+ const cell = Math.max(4, reach / 2)
122
+ const x0 = minX - reach
123
+ const z0 = minZ - reach
124
+ const cols = Math.max(1, Math.ceil((maxX + reach - x0) / cell))
125
+ const rows = Math.max(1, Math.ceil((maxZ + reach - z0) / cell))
126
+ const lists = Array.from({ length: cols * rows }, () => [])
127
+ for (let s = 0; s + 1 < count; s++) {
128
+ const ax = pts[s * 3]
129
+ const az = pts[s * 3 + 2]
130
+ const bx = pts[s * 3 + 3]
131
+ const bz = pts[s * 3 + 5]
132
+ const c0 = Math.max(0, Math.floor((Math.min(ax, bx) - reach - x0) / cell))
133
+ const c1 = Math.min(cols - 1, Math.floor((Math.max(ax, bx) + reach - x0) / cell))
134
+ const r0 = Math.max(0, Math.floor((Math.min(az, bz) - reach - z0) / cell))
135
+ const r1 = Math.min(rows - 1, Math.floor((Math.max(az, bz) + reach - z0) / cell))
136
+ for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) lists[r * cols + c].push(s)
137
+ }
138
+ const start = new Int32Array(cols * rows + 1)
139
+ for (let b = 0; b < lists.length; b++) start[b + 1] = start[b] + lists[b].length
140
+ const segs = new Int32Array(start[lists.length])
141
+ for (let b = 0; b < lists.length; b++) segs.set(lists[b], start[b])
142
+ return {
143
+ pts: Float64Array.from(pts),
144
+ count,
145
+ width,
146
+ falloff,
147
+ reach,
148
+ cell,
149
+ x0,
150
+ z0,
151
+ cols,
152
+ rows,
153
+ start,
154
+ segs,
155
+ minX: minX - reach,
156
+ minZ: minZ - reach,
157
+ maxX: maxX + reach,
158
+ maxZ: maxZ + reach,
159
+ }
160
+ }
161
+
162
+ /**
163
+ * The nearest point of a spline's centerline to (x, z) within its reach: writes [distance, height
164
+ * there, along (0–1 within the segment), segment] into `out` and returns the distance, or Infinity
165
+ * when nothing is within reach. Ties go to the lower segment.
166
+ */
167
+ export function nearestOnSpline(sp, x, z, out) {
168
+ out[0] = Infinity
169
+ if (x < sp.minX || x > sp.maxX || z < sp.minZ || z > sp.maxZ) return Infinity
170
+ const c = Math.floor((x - sp.x0) / sp.cell)
171
+ const r = Math.floor((z - sp.z0) / sp.cell)
172
+ if (c < 0 || r < 0 || c >= sp.cols || r >= sp.rows) return Infinity
173
+ const b = r * sp.cols + c
174
+ const p = sp.pts
175
+ let best = Infinity
176
+ for (let k = sp.start[b]; k < sp.start[b + 1]; k++) {
177
+ const s = sp.segs[k]
178
+ const ax = p[s * 3]
179
+ const ay = p[s * 3 + 1]
180
+ const az = p[s * 3 + 2]
181
+ const bx = p[s * 3 + 3]
182
+ const by = p[s * 3 + 4]
183
+ const bz = p[s * 3 + 5]
184
+ const ex = bx - ax
185
+ const ez = bz - az
186
+ const len2 = ex * ex + ez * ez
187
+ let t = len2 > 0 ? ((x - ax) * ex + (z - az) * ez) / len2 : 0
188
+ t = Math.min(1, Math.max(0, t))
189
+ const dx = x - (ax + ex * t)
190
+ const dz = z - (az + ez * t)
191
+ const d = Math.sqrt(dx * dx + dz * dz)
192
+ if (d < best) {
193
+ best = d
194
+ out[0] = d
195
+ out[1] = ay + (by - ay) * t
196
+ out[2] = t
197
+ out[3] = s
198
+ }
199
+ }
200
+ if (best <= sp.reach) return best
201
+ out[0] = Infinity
202
+ return Infinity
203
+ }
204
+
205
+ // --- noise on the canonical lattice -----------------------------------------------------------
206
+
207
+ /** Per-thread scratch for noise batches. */
208
+ const scratch = {
209
+ pts: new Float32Array(0),
210
+ vals: new Float32Array(0),
211
+ origin: new Float64Array(4),
212
+ records: new Int32Array(0),
213
+ }
214
+
215
+ function ensure(name, n, Kind) {
216
+ if (scratch[name].length < n) scratch[name] = new Kind(n)
217
+ return scratch[name]
218
+ }
219
+
220
+ /**
221
+ * A noise program at the grid's samples: column a, row b is leaf sample (gi0 + a × step, gj0 +
222
+ * b × step). Columns and rows are cut where the lattice of origins changes, and each piece is
223
+ * sampled from its own origin, so every sample gets the value it would get alone. Writes
224
+ * `value × scale + offset` into `out` (f64, row by row).
225
+ */
226
+ function noiseGrid(noise, program, seed, spacing, gi0, gj0, step, nx, nz, scale, offset, out) {
227
+ const records = ensure('records', program.zeroOrigins.length, Int32Array)
228
+ let a0 = 0
229
+ while (a0 < nx) {
230
+ const cx = Math.floor(((gi0 + a0 * step) * spacing) / LATTICE)
231
+ let a1 = a0 + 1
232
+ while (a1 < nx && Math.floor(((gi0 + a1 * step) * spacing) / LATTICE) === cx) a1++
233
+ let b0 = 0
234
+ while (b0 < nz) {
235
+ const cz = Math.floor(((gj0 + b0 * step) * spacing) / LATTICE)
236
+ let b1 = b0 + 1
237
+ while (b1 < nz && Math.floor(((gj0 + b1 * step) * spacing) / LATTICE) === cz) b1++
238
+ const ox = cx * LATTICE + LATTICE / 2
239
+ const oz = cz * LATTICE + LATTICE / 2
240
+ const w = a1 - a0
241
+ const n = w * (b1 - b0)
242
+ const pts = ensure('pts', n * 3, Float32Array)
243
+ const vals = ensure('vals', n, Float32Array)
244
+ let k = 0
245
+ for (let b = b0; b < b1; b++) {
246
+ const lz = (gj0 + b * step) * spacing - oz
247
+ for (let a = a0; a < a1; a++) {
248
+ pts[k * 3] = (gi0 + a * step) * spacing - ox
249
+ pts[k * 3 + 1] = 0
250
+ pts[k * 3 + 2] = lz
251
+ k++
252
+ }
253
+ }
254
+ const origin = scratch.origin
255
+ origin[0] = ox
256
+ origin[1] = 0
257
+ origin[2] = oz
258
+ origin[3] = 0
259
+ noise.computeOrigins(program.terms, origin, records)
260
+ noise.evalProgram(noise.state, program, seed, records, pts, 3, n, vals)
261
+ k = 0
262
+ for (let b = b0; b < b1; b++) {
263
+ for (let a = a0; a < a1; a++) out[b * nx + a] = vals[k++] * scale + offset
264
+ }
265
+ b0 = b1
266
+ }
267
+ a0 = a1
268
+ }
269
+ }
270
+
271
+ /** A noise program at arbitrary points (x, z pairs), each from its own lattice origin. */
272
+ function noisePoints(noise, program, seed, xz, count, scale, offset, out) {
273
+ const records = ensure('records', program.zeroOrigins.length, Int32Array)
274
+ const pts = ensure('pts', 3, Float32Array)
275
+ const vals = ensure('vals', 1, Float32Array)
276
+ const origin = scratch.origin
277
+ for (let i = 0; i < count; i++) {
278
+ const x = xz[i * 2]
279
+ const z = xz[i * 2 + 1]
280
+ const ox = Math.floor(x / LATTICE) * LATTICE + LATTICE / 2
281
+ const oz = Math.floor(z / LATTICE) * LATTICE + LATTICE / 2
282
+ origin[0] = ox
283
+ origin[1] = 0
284
+ origin[2] = oz
285
+ origin[3] = 0
286
+ pts[0] = x - ox
287
+ pts[1] = 0
288
+ pts[2] = z - oz
289
+ noise.computeOrigins(program.terms, origin, records)
290
+ noise.evalProgram(noise.state, program, seed, records, pts, 3, 1, vals)
291
+ out[i] = vals[0] * scale + offset
292
+ }
293
+ }
294
+
295
+ // --- the height stack --------------------------------------------------------------------------
296
+
297
+ function blend(h, v, m, op) {
298
+ if (m <= 0) return h
299
+ switch (op) {
300
+ case BLEND_ADD:
301
+ return h + m * v
302
+ case BLEND_MAX:
303
+ return h + m * (Math.max(h, v) - h)
304
+ case BLEND_MIN:
305
+ return h + m * (Math.min(h, v) - h)
306
+ default:
307
+ return h + m * (v - h)
308
+ }
309
+ }
310
+
311
+ const local = new Float64Array(3)
312
+ const near = new Float64Array(4)
313
+ let values = new Float64Array(0)
314
+
315
+ /**
316
+ * Applies height layers [0, `upTo`) to a grid of samples (see noiseGrid for the layout): `out`
317
+ * gets metres, f64, row by row. The stack is the compiled source (heightfield/stack.ts).
318
+ */
319
+ export function evalHeights(noise, stack, gi0, gj0, step, nx, nz, upTo, out) {
320
+ const n = nx * nz
321
+ const sp = stack.spacing
322
+ out.fill(0, 0, n)
323
+ if (values.length < n) values = new Float64Array(n)
324
+ const layers = stack.height
325
+ const count = Math.min(upTo, layers.length)
326
+ for (let l = 0; l < count; l++) {
327
+ const layer = layers[l]
328
+ if (layer.kind === LAYER_NOISE) {
329
+ noiseGrid(
330
+ noise,
331
+ layer.program,
332
+ stack.seed,
333
+ sp,
334
+ gi0,
335
+ gj0,
336
+ step,
337
+ nx,
338
+ nz,
339
+ layer.scale,
340
+ layer.offset,
341
+ values,
342
+ )
343
+ for (let b = 0; b < nz; b++) {
344
+ const z = (gj0 + b * step) * sp
345
+ for (let a = 0; a < nx; a++) {
346
+ const k = b * nx + a
347
+ let m = 1
348
+ if (layer.rect) {
349
+ rectLocal(layer.rect, (gi0 + a * step) * sp, z, local)
350
+ m = fade(local[2], layer.falloff)
351
+ }
352
+ out[k] = blend(out[k], values[k], m, layer.blend)
353
+ }
354
+ }
355
+ } else if (layer.kind === LAYER_IMAGE) {
356
+ const lo = layer.lo
357
+ const span = layer.hi - layer.lo
358
+ for (let b = 0; b < nz; b++) {
359
+ const z = (gj0 + b * step) * sp
360
+ for (let a = 0; a < nx; a++) {
361
+ rectLocal(layer.rect, (gi0 + a * step) * sp, z, local)
362
+ const m = fade(local[2], layer.falloff)
363
+ if (m <= 0) continue
364
+ const k = b * nx + a
365
+ const v = lo + span * sampleMap(layer.map, local[0], local[1])
366
+ out[k] = blend(out[k], v, m, layer.blend)
367
+ }
368
+ }
369
+ } else {
370
+ const spline = layer.spline
371
+ const half = spline.width / 2
372
+ for (let b = 0; b < nz; b++) {
373
+ const z = (gj0 + b * step) * sp
374
+ for (let a = 0; a < nx; a++) {
375
+ const d = nearestOnSpline(spline, (gi0 + a * step) * sp, z, near)
376
+ if (d === Infinity) continue
377
+ const m = fade(d - half, layer.falloff)
378
+ if (m <= 0) continue
379
+ const k = b * nx + a
380
+ const h = out[k]
381
+ const target = near[1] + layer.offset
382
+ const v =
383
+ layer.mode === MODE_RAISE
384
+ ? Math.max(h, target)
385
+ : layer.mode === MODE_CARVE
386
+ ? Math.min(h, target)
387
+ : target
388
+ out[k] = h + m * (v - h)
389
+ }
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Heights of layers [0, `upTo`) at arbitrary points (x, z pairs): what a spline's "ground" point
397
+ * takes. The same per-sample rules as evalHeights, so a point on the leaf lattice gets the same
398
+ * value either way.
399
+ */
400
+ export function evalHeightPoints(noise, stack, xz, count, upTo, out) {
401
+ out.fill(0, 0, count)
402
+ const v = new Float64Array(count)
403
+ const layers = stack.height
404
+ for (let l = 0; l < Math.min(upTo, layers.length); l++) {
405
+ const layer = layers[l]
406
+ if (layer.kind === LAYER_NOISE)
407
+ noisePoints(noise, layer.program, stack.seed, xz, count, layer.scale, layer.offset, v)
408
+ for (let i = 0; i < count; i++) {
409
+ const x = xz[i * 2]
410
+ const z = xz[i * 2 + 1]
411
+ if (layer.kind === LAYER_NOISE) {
412
+ let m = 1
413
+ if (layer.rect) {
414
+ rectLocal(layer.rect, x, z, local)
415
+ m = fade(local[2], layer.falloff)
416
+ }
417
+ out[i] = blend(out[i], v[i], m, layer.blend)
418
+ } else if (layer.kind === LAYER_IMAGE) {
419
+ rectLocal(layer.rect, x, z, local)
420
+ const m = fade(local[2], layer.falloff)
421
+ if (m <= 0) continue
422
+ const value = layer.lo + (layer.hi - layer.lo) * sampleMap(layer.map, local[0], local[1])
423
+ out[i] = blend(out[i], value, m, layer.blend)
424
+ } else {
425
+ const d = nearestOnSpline(layer.spline, x, z, near)
426
+ if (d === Infinity) continue
427
+ const m = fade(d - layer.spline.width / 2, layer.falloff)
428
+ if (m <= 0) continue
429
+ const h = out[i]
430
+ const target = near[1] + layer.offset
431
+ const value =
432
+ layer.mode === MODE_RAISE
433
+ ? Math.max(h, target)
434
+ : layer.mode === MODE_CARVE
435
+ ? Math.min(h, target)
436
+ : target
437
+ out[i] = h + m * (value - h)
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ // --- quantizing, normals, slopes ---------------------------------------------------------------
444
+
445
+ /** Metres to the u16 a page stores over heightRange [lo, hi]. */
446
+ export function quantize(h, lo, hi) {
447
+ const q = Math.round(((h - lo) / (hi - lo)) * 65535)
448
+ return q < 0 ? 0 : q > 65535 ? 65535 : q
449
+ }
450
+
451
+ export function dequantize(q, lo, hi) {
452
+ return lo + (q * (hi - lo)) / 65535
453
+ }
454
+
455
+ /** A unit normal component stored in a byte: [-1, 1] → 0–255. */
456
+ export function normalByte(v) {
457
+ const b = Math.round((v + 1) * 127.5)
458
+ return b < 0 ? 0 : b > 255 ? 255 : b
459
+ }
460
+
461
+ export function byteNormal(b) {
462
+ return b / 127.5 - 1
463
+ }
464
+
465
+ /**
466
+ * The normal at sample k of a grid of heights (f64 metres, `nx` wide) by central differences at
467
+ * `step` metres, into out [x, y, z] (unit). The sample needs both neighbors on each axis.
468
+ */
469
+ function centralNormal(h, nx, k, step, out) {
470
+ const dx = (h[k + 1] - h[k - 1]) / (2 * step)
471
+ const dz = (h[k + nx] - h[k - nx]) / (2 * step)
472
+ const l = Math.sqrt(dx * dx + 1 + dz * dz)
473
+ out[0] = -dx / l
474
+ out[1] = 1 / l
475
+ out[2] = -dz / l
476
+ return out
477
+ }
478
+
479
+ const nrm = new Float64Array(3)
480
+
481
+ /**
482
+ * A leaf page's normals as bytes (x then z planes, SIDE² each), from its LEAF_SIDE² quantized
483
+ * heights: what the GPU upload and the CPU queries both use.
484
+ */
485
+ export function leafNormals(heights, lo, hi, spacing, out) {
486
+ const n = LEAF_SIDE
487
+ const h = new Float64Array(n * n)
488
+ for (let i = 0; i < n * n; i++) h[i] = dequantize(heights[i], lo, hi)
489
+ for (let j = 0; j < SIDE; j++) {
490
+ for (let i = 0; i < SIDE; i++) {
491
+ centralNormal(h, n, (j + 1) * n + i + 1, spacing, nrm)
492
+ out[j * SIDE + i] = normalByte(nrm[0])
493
+ out[SIDE * SIDE + j * SIDE + i] = normalByte(nrm[2])
494
+ }
495
+ }
496
+ return out
497
+ }
498
+
499
+ /** Slope in degrees from the heights around sample k (central differences at `step`). */
500
+ function slopeAt(h, nx, k, step) {
501
+ const dx = (h[k + 1] - h[k - 1]) / (2 * step)
502
+ const dz = (h[k + nx] - h[k - nx]) / (2 * step)
503
+ return (Math.atan(Math.sqrt(dx * dx + dz * dz)) * 180) / Math.PI
504
+ }
505
+
506
+ // --- paint -------------------------------------------------------------------------------------
507
+
508
+ /**
509
+ * Control texels for paint samples: paint sample (a, b) is leaf sample (gi0 + a × k, gj0 + b × k),
510
+ * whose height and slope come from `hq` (dequantized leaf heights, `hx` wide, with sample (gi0,
511
+ * gj0) at index `h0`, one sample of margin all round). Writes four bytes per texel: the heaviest
512
+ * layer, the second, the second's share of their weight (0–255), and the hole bit (0).
513
+ */
514
+ export function evalControl(noise, stack, gi0, gj0, k, nx, nz, hq, hx, h0, out) {
515
+ const layers = stack.layerCount
516
+ const w = new Float64Array(layers)
517
+ const paint = stack.paint
518
+ const sp = stack.spacing
519
+ // Noise masks for the whole grid first, per entry.
520
+ const noiseValues = paint.map((entry) => {
521
+ if (!entry.noise) return null
522
+ const v = new Float64Array(nx * nz)
523
+ noiseGrid(noise, entry.noise.program, stack.seed, sp, gi0, gj0, k, nx, nz, 1, 0, v)
524
+ return v
525
+ })
526
+ for (let b = 0; b < nz; b++) {
527
+ for (let a = 0; a < nx; a++) {
528
+ const x = (gi0 + a * k) * sp
529
+ const z = (gj0 + b * k) * sp
530
+ const hk = h0 + b * k * hx + a * k
531
+ const height = hq[hk]
532
+ const slope = slopeAt(hq, hx, hk, sp)
533
+ w.fill(0)
534
+ for (let e = 0; e < paint.length; e++) {
535
+ const entry = paint[e]
536
+ let m = 1
537
+ if (entry.height) m *= windowWeight(height, entry.height[0], entry.height[1], entry.blend)
538
+ if (m > 0 && entry.slope)
539
+ m *= windowWeight(slope, entry.slope[0], entry.slope[1], entry.blend)
540
+ if (m > 0 && entry.noise) {
541
+ const above = entry.noise.above
542
+ m *= smoothstep(above - 0.05, above + 0.05, noiseValues[e][b * nx + a])
543
+ }
544
+ if (m > 0 && entry.mask) {
545
+ rectLocal(entry.mask.rect, x, z, local)
546
+ m *= fade(local[2], entry.blend) * sampleMap(entry.mask.map, local[0], local[1])
547
+ }
548
+ if (m > 0 && entry.spline) {
549
+ const d = nearestOnSpline(entry.spline, x, z, near)
550
+ m *= d === Infinity ? 0 : fade(d - entry.spline.width / 2, entry.blend)
551
+ }
552
+ if (m <= 0) continue
553
+ if (m > 1) m = 1
554
+ // Paints over what's below: every weight scales by (1 − m), this layer gains m.
555
+ for (let l = 0; l < layers; l++) w[l] *= 1 - m
556
+ w[entry.layer] += m
557
+ }
558
+ let a0 = 0
559
+ let a1 = -1
560
+ for (let l = 1; l < layers; l++) if (w[l] > w[a0]) a0 = l
561
+ for (let l = 0; l < layers; l++) {
562
+ if (l === a0) continue
563
+ if (a1 < 0 || w[l] > w[a1]) a1 = l
564
+ }
565
+ const o = (b * nx + a) * 4
566
+ const wa = w[a0]
567
+ const wb = a1 >= 0 ? w[a1] : 0
568
+ out[o] = a0
569
+ out[o + 1] = a1 >= 0 && wb > 0 ? a1 : a0
570
+ out[o + 2] = wa + wb > 0 ? Math.round((wb / (wa + wb)) * 255) : 0
571
+ out[o + 3] = 0
572
+ }
573
+ }
574
+ return out
575
+ }
576
+
577
+ // --- pages -------------------------------------------------------------------------------------
578
+
579
+ /** Raw bytes of a page before deflate: leaves hold heights with a border and control; parents add normals. */
580
+ export function pageBytes(leaf, cells) {
581
+ const c = (cells + 1) * (cells + 1) * 4
582
+ return leaf ? LEAF_SIDE * LEAF_SIDE * 2 + c : SIDE * SIDE * 4 + c
583
+ }
584
+
585
+ /**
586
+ * A page's raw bytes: heights as row deltas split into low and high byte planes (smooth ground
587
+ * deflates well), then parents' normals (x plane, z plane), then control (four planes).
588
+ */
589
+ export function encodePage(page, leaf, cells) {
590
+ const side = leaf ? LEAF_SIDE : SIDE
591
+ const count = side * side
592
+ const out = new Uint8Array(pageBytes(leaf, cells))
593
+ const h = page.heights
594
+ for (let j = 0; j < side; j++) {
595
+ for (let i = 0; i < side; i++) {
596
+ const k = j * side + i
597
+ const prev = i > 0 ? h[k - 1] : j > 0 ? h[k - side] : 0
598
+ const d = (h[k] - prev) & 0xffff
599
+ out[k] = d & 0xff
600
+ out[count + k] = d >> 8
601
+ }
602
+ }
603
+ let o = count * 2
604
+ if (!leaf) {
605
+ out.set(page.normals.subarray(0, SIDE * SIDE * 2), o)
606
+ o += SIDE * SIDE * 2
607
+ }
608
+ const texels = (cells + 1) * (cells + 1)
609
+ const ctl = page.control
610
+ for (let t = 0; t < texels; t++) {
611
+ out[o + t] = ctl[t * 4]
612
+ out[o + texels + t] = ctl[t * 4 + 1]
613
+ out[o + texels * 2 + t] = ctl[t * 4 + 2]
614
+ out[o + texels * 3 + t] = ctl[t * 4 + 3]
615
+ }
616
+ return out
617
+ }
618
+
619
+ /** The inverse of encodePage: { heights, normals (parents; null for leaves), control }. */
620
+ export function decodePage(raw, leaf, cells) {
621
+ const side = leaf ? LEAF_SIDE : SIDE
622
+ const count = side * side
623
+ const heights = new Uint16Array(count)
624
+ for (let j = 0; j < side; j++) {
625
+ for (let i = 0; i < side; i++) {
626
+ const k = j * side + i
627
+ const prev = i > 0 ? heights[k - 1] : j > 0 ? heights[k - side] : 0
628
+ heights[k] = (prev + (raw[k] | (raw[count + k] << 8))) & 0xffff
629
+ }
630
+ }
631
+ let o = count * 2
632
+ let normals = null
633
+ if (!leaf) {
634
+ normals = raw.slice(o, o + SIDE * SIDE * 2)
635
+ o += SIDE * SIDE * 2
636
+ }
637
+ const texels = (cells + 1) * (cells + 1)
638
+ const control = new Uint8Array(texels * 4)
639
+ for (let t = 0; t < texels; t++) {
640
+ control[t * 4] = raw[o + t]
641
+ control[t * 4 + 1] = raw[o + texels + t]
642
+ control[t * 4 + 2] = raw[o + texels * 2 + t]
643
+ control[t * 4 + 3] = raw[o + texels * 3 + t]
644
+ }
645
+ return { heights, normals, control }
646
+ }
647
+
648
+ /** Inflates and decodes a page. */
649
+ export function readPage(packed, leaf, cells) {
650
+ return decodePage(inflate(packed, pageBytes(leaf, cells)), leaf, cells)
651
+ }
652
+
653
+ /**
654
+ * A page as the GPU pool holds it: LEAF_SIDE² RGBA8 texels of (height low byte, height high
655
+ * byte, normal x, normal z), a parent's heights inset by one with its edges repeated into the
656
+ * border. A leaf's normals come from its heights (unless `page.normals` already holds them), on the
657
+ * pool, so both tiers upload the same bytes.
658
+ */
659
+ export function pageTexels(page, leaf, lo, hi, spacing, texels) {
660
+ const n = LEAF_SIDE
661
+ const normals =
662
+ page.normals ?? leafNormals(page.heights, lo, hi, spacing, new Uint8Array(SIDE * SIDE * 2))
663
+ for (let j = 0; j < n; j++) {
664
+ for (let i = 0; i < n; i++) {
665
+ let h
666
+ if (leaf) h = page.heights[j * n + i]
667
+ else {
668
+ const si = Math.min(SIDE - 1, Math.max(0, i - 1))
669
+ const sj = Math.min(SIDE - 1, Math.max(0, j - 1))
670
+ h = page.heights[sj * SIDE + si]
671
+ }
672
+ const o = (j * n + i) * 4
673
+ texels[o] = h & 0xff
674
+ texels[o + 1] = h >> 8
675
+ const vi = i - 1
676
+ const vj = j - 1
677
+ if (vi >= 0 && vj >= 0 && vi < SIDE && vj < SIDE) {
678
+ texels[o + 2] = normals[vj * SIDE + vi]
679
+ texels[o + 3] = normals[SIDE * SIDE + vj * SIDE + vi]
680
+ } else {
681
+ texels[o + 2] = 128
682
+ texels[o + 3] = 128
683
+ }
684
+ }
685
+ }
686
+ return texels
687
+ }
688
+
689
+ // --- errors ------------------------------------------------------------------------------------
690
+
691
+ /**
692
+ * The largest |h − surface| over samples [i0, i1] × [j0, j1] of a grid (`hx` wide, f64 metres),
693
+ * where `surface` is the grid at every `s`-th sample drawn as triangles split along each cell's
694
+ * anti-diagonal (Rapier's split, and the drawn mesh's). The range's ends are multiples of `s`.
695
+ */
696
+ function levelError(h, hx, i0, j0, i1, j1, s) {
697
+ let worst = 0
698
+ for (let cj = j0; cj < j1; cj += s) {
699
+ for (let ci = i0; ci < i1; ci += s) {
700
+ const h00 = h[cj * hx + ci]
701
+ const h10 = h[cj * hx + ci + s]
702
+ const h01 = h[(cj + s) * hx + ci]
703
+ const h11 = h[(cj + s) * hx + ci + s]
704
+ for (let b = 0; b <= s; b++) {
705
+ const v = b / s
706
+ const row = (cj + b) * hx + ci
707
+ for (let a = 0; a <= s; a++) {
708
+ const u = a / s
709
+ const surface =
710
+ u + v <= 1
711
+ ? h00 + u * (h10 - h00) + v * (h01 - h00)
712
+ : h11 + (1 - u) * (h01 - h11) + (1 - v) * (h10 - h11)
713
+ const e = Math.abs(h[row + a] - surface)
714
+ if (e > worst) worst = e
715
+ }
716
+ }
717
+ }
718
+ }
719
+ return worst
720
+ }
721
+
722
+ // --- block bakes -------------------------------------------------------------------------------
723
+
724
+ /**
725
+ * Bakes one block (spec 0071): the stack over its leaf samples (plus a margin for normals), its
726
+ * leaf pages, and its own ancestors up to BLOCK_LEVELS (or the root) with their normals (each
727
+ * level's normal the [1 2 1]² average of its children's), exact geometric errors, and height
728
+ * ranges; plus its share of every higher level's error. `layout` is the terrain's grid
729
+ * (heightfield/source.ts terrainLayout). Returns compressed pages and stats; allocates per call.
730
+ */
731
+ export function bakeBlock(noise, stack, layout, bx, bz) {
732
+ const t0 = performance.now()
733
+ const D = layout.depth
734
+ const sp = layout.spacing
735
+ const lo = stack.lo
736
+ const hi = stack.hi
737
+ const k = layout.paintStep
738
+ const cells = layout.cells
739
+ const block = layout.block
740
+ const lmax = Math.min(layout.blockLevels, D)
741
+ // The block's leaves, clipped to the terrain.
742
+ const lx0 = bx * block
743
+ const lz0 = bz * block
744
+ const nlx = Math.min(block, layout.leavesX - lx0)
745
+ const nlz = Math.min(block, layout.leavesZ - lz0)
746
+ // Leaf samples [s0, s1] per axis, plus a margin: the [1 2 1] chain to level lmax reaches 2^lmax − 1
747
+ // samples out, and leaf normals one more.
748
+ const margin = 2 ** lmax + 1
749
+ const gx0 = lx0 * PAGE - margin
750
+ const gz0 = lz0 * PAGE - margin
751
+ const nx = nlx * PAGE + 1 + 2 * margin
752
+ const nz = nlz * PAGE + 1 + 2 * margin
753
+ const H = new Float64Array(nx * nz)
754
+ evalHeights(noise, stack, gx0, gz0, 1, nx, nz, stack.height.length, H)
755
+ // Quantize; heights clipped by heightRange inside the block are reported once.
756
+ const Q = new Uint16Array(nx * nz)
757
+ const Hq = new Float64Array(nx * nz)
758
+ let clipped = 0
759
+ let lowest = Infinity
760
+ let highest = -Infinity
761
+ for (let j = 0; j < nz; j++) {
762
+ for (let i = 0; i < nx; i++) {
763
+ const idx = j * nx + i
764
+ const h = H[idx]
765
+ const q = quantize(h, lo, hi)
766
+ Q[idx] = q
767
+ Hq[idx] = dequantize(q, lo, hi)
768
+ if (i >= margin && j >= margin && i < nx - margin && j < nz - margin) {
769
+ if (h < lo || h > hi) clipped++
770
+ if (h < lowest) lowest = h
771
+ if (h > highest) highest = h
772
+ }
773
+ }
774
+ }
775
+ // Paint over the block's paint samples (one leaf sample of margin for slopes is inside H's).
776
+ const pnx = (nlx * PAGE) / k + 1
777
+ const pnz = (nlz * PAGE) / k + 1
778
+ const C = new Uint8Array(pnx * pnz * 4)
779
+ evalControl(noise, stack, lx0 * PAGE, lz0 * PAGE, k, pnx, pnz, Hq, nx, margin * nx + margin, C)
780
+ // Leaf normals over the margin, then each level's [1 2 1]² average of the one below.
781
+ const N = [new Float64Array(nx * nz * 3)]
782
+ for (let j = 1; j < nz - 1; j++) {
783
+ for (let i = 1; i < nx - 1; i++) {
784
+ centralNormal(Hq, nx, j * nx + i, sp, nrm)
785
+ const o = (j * nx + i) * 3
786
+ N[0][o] = nrm[0]
787
+ N[0][o + 1] = nrm[1]
788
+ N[0][o + 2] = nrm[2]
789
+ }
790
+ }
791
+ // Level L's texels sit on block-relative samples that are multiples of 2^L, each the weighted
792
+ // sum of level L − 1's at ±2^(L−1). Each level covers what the next one up reads: the block plus
793
+ // 2^lmax − 2^L samples.
794
+ const RX = nlx * PAGE
795
+ const RZ = nlz * PAGE
796
+ for (let L = 1; L <= lmax; L++) {
797
+ const s = 2 ** L
798
+ const r = s / 2
799
+ const ext = 2 ** lmax - s
800
+ const prev = N[L - 1]
801
+ const next = new Float64Array(nx * nz * 3)
802
+ for (let rj = -ext; rj <= RZ + ext; rj += s) {
803
+ const j = margin + rj
804
+ for (let ri = -ext; ri <= RX + ext; ri += s) {
805
+ const i = margin + ri
806
+ let sx = 0
807
+ let sy = 0
808
+ let sz = 0
809
+ for (let b = -1; b <= 1; b++) {
810
+ for (let a = -1; a <= 1; a++) {
811
+ const wgt = (2 - Math.abs(a)) * (2 - Math.abs(b))
812
+ const o = ((j + b * r) * nx + i + a * r) * 3
813
+ sx += prev[o] * wgt
814
+ sy += prev[o + 1] * wgt
815
+ sz += prev[o + 2] * wgt
816
+ }
817
+ }
818
+ const l = Math.sqrt(sx * sx + sy * sy + sz * sz) || 1
819
+ const o = (j * nx + i) * 3
820
+ next[o] = sx / l
821
+ next[o + 1] = sy / l
822
+ next[o + 2] = sz / l
823
+ }
824
+ }
825
+ N.push(next)
826
+ }
827
+ const pages = []
828
+ const pack = (level, X, Z, page, leaf, min, max, error) => {
829
+ const raw = encodePage(page, leaf, cells)
830
+ pages.push({ level, x: X, z: Z, data: deflate(raw), min, max, error })
831
+ }
832
+ // Leaves.
833
+ const leafRange = new Uint16Array(nlx * nlz * 2)
834
+ for (let lz = 0; lz < nlz; lz++) {
835
+ for (let lx = 0; lx < nlx; lx++) {
836
+ const heights = new Uint16Array(LEAF_SIDE * LEAF_SIDE)
837
+ let qmin = 65535
838
+ let qmax = 0
839
+ for (let j = 0; j < LEAF_SIDE; j++) {
840
+ const row = (margin + lz * PAGE - 1 + j) * nx + margin + lx * PAGE - 1
841
+ for (let i = 0; i < LEAF_SIDE; i++) {
842
+ const q = Q[row + i]
843
+ heights[j * LEAF_SIDE + i] = q
844
+ if (i > 0 && j > 0 && i < LEAF_SIDE - 1 && j < LEAF_SIDE - 1) {
845
+ if (q < qmin) qmin = q
846
+ if (q > qmax) qmax = q
847
+ }
848
+ }
849
+ }
850
+ leafRange[(lz * nlx + lx) * 2] = qmin
851
+ leafRange[(lz * nlx + lx) * 2 + 1] = qmax
852
+ const control = new Uint8Array((cells + 1) * (cells + 1) * 4)
853
+ for (let b = 0; b <= cells; b++) {
854
+ const src = ((lz * cells + b) * pnx + lx * cells) * 4
855
+ control.set(C.subarray(src, src + (cells + 1) * 4), b * (cells + 1) * 4)
856
+ }
857
+ pack(0, lx0 + lx, lz0 + lz, { heights, normals: null, control }, true, qmin, qmax, 0)
858
+ }
859
+ }
860
+ // The block's own ancestors.
861
+ let ranges = leafRange
862
+ let rx = nlx
863
+ let rz = nlz
864
+ for (let L = 1; L <= lmax; L++) {
865
+ const s = 2 ** L
866
+ const nnx = Math.ceil(nlx / s)
867
+ const nnz = Math.ceil(nlz / s)
868
+ const next = new Uint16Array(nnx * nnz * 2)
869
+ for (let z = 0; z < nnz; z++) {
870
+ for (let x = 0; x < nnx; x++) {
871
+ // Height range over its children.
872
+ let qmin = 65535
873
+ let qmax = 0
874
+ for (let c = 0; c < 4; c++) {
875
+ const cx = x * 2 + (c & 1)
876
+ const cz = z * 2 + (c >> 1)
877
+ if (cx >= rx || cz >= rz) continue
878
+ qmin = Math.min(qmin, ranges[(cz * rx + cx) * 2])
879
+ qmax = Math.max(qmax, ranges[(cz * rx + cx) * 2 + 1])
880
+ }
881
+ next[(z * nnx + x) * 2] = qmin
882
+ next[(z * nnx + x) * 2 + 1] = qmax
883
+ const heights = new Uint16Array(SIDE * SIDE)
884
+ const normals = new Uint8Array(SIDE * SIDE * 2)
885
+ const n = N[L]
886
+ for (let j = 0; j < SIDE; j++) {
887
+ for (let i = 0; i < SIDE; i++) {
888
+ const gi = margin + (x * PAGE + i) * s
889
+ const gj = margin + (z * PAGE + j) * s
890
+ heights[j * SIDE + i] = Q[gj * nx + gi]
891
+ const o = (gj * nx + gi) * 3
892
+ normals[j * SIDE + i] = normalByte(n[o])
893
+ normals[SIDE * SIDE + j * SIDE + i] = normalByte(n[o + 2])
894
+ }
895
+ }
896
+ const control = new Uint8Array((cells + 1) * (cells + 1) * 4)
897
+ for (let b = 0; b <= cells; b++) {
898
+ for (let a = 0; a <= cells; a++) {
899
+ const src = ((z * cells + b) * s * pnx + (x * cells + a) * s) * 4
900
+ control.set(C.subarray(src, src + 4), (b * (cells + 1) + a) * 4)
901
+ }
902
+ }
903
+ // Exact error: every leaf sample under it against its own surface.
904
+ const i0 = margin + x * PAGE * s
905
+ const j0 = margin + z * PAGE * s
906
+ const error = levelError(Hq, nx, i0, j0, i0 + PAGE * s, j0 + PAGE * s, s)
907
+ pack(
908
+ L,
909
+ (lx0 >> L) + x,
910
+ (lz0 >> L) + z,
911
+ { heights, normals, control },
912
+ false,
913
+ qmin,
914
+ qmax,
915
+ error,
916
+ )
917
+ }
918
+ }
919
+ ranges = next
920
+ rx = nnx
921
+ rz = nnz
922
+ }
923
+ // Higher levels' errors over this block's samples (each block lies inside one node there).
924
+ const above = []
925
+ for (let L = lmax + 1; L <= D; L++) {
926
+ const s = 2 ** L
927
+ above.push(levelError(Hq, nx, margin, margin, margin + RX, margin + RZ, s))
928
+ }
929
+ return {
930
+ pages,
931
+ above,
932
+ range: [ranges[0], ranges[1]],
933
+ clipped,
934
+ lowest: lowest === Infinity ? 0 : lowest,
935
+ highest: highest === -Infinity ? 0 : highest,
936
+ ms: performance.now() - t0,
937
+ }
938
+ }
939
+
940
+ /**
941
+ * One leaf page baked on its own (spec 0071): heights with their border and control, exactly the
942
+ * bytes its block bake writes (every sample is canonical). Colliders and height queries use it
943
+ * when a page they need hasn't arrived by the frame it's due.
944
+ */
945
+ export function bakeLeaf(noise, stack, layout, X, Z) {
946
+ const k = layout.paintStep
947
+ const cells = layout.cells
948
+ const n = LEAF_SIDE
949
+ const H = new Float64Array(n * n)
950
+ evalHeights(noise, stack, X * PAGE - 1, Z * PAGE - 1, 1, n, n, stack.height.length, H)
951
+ const heights = new Uint16Array(n * n)
952
+ const Hq = new Float64Array(n * n)
953
+ let qmin = 65535
954
+ let qmax = 0
955
+ for (let j = 0; j < n; j++) {
956
+ for (let i = 0; i < n; i++) {
957
+ const q = quantize(H[j * n + i], stack.lo, stack.hi)
958
+ heights[j * n + i] = q
959
+ Hq[j * n + i] = dequantize(q, stack.lo, stack.hi)
960
+ if (i > 0 && j > 0 && i < n - 1 && j < n - 1) {
961
+ if (q < qmin) qmin = q
962
+ if (q > qmax) qmax = q
963
+ }
964
+ }
965
+ }
966
+ const control = new Uint8Array((cells + 1) * (cells + 1) * 4)
967
+ evalControl(noise, stack, X * PAGE, Z * PAGE, k, cells + 1, cells + 1, Hq, n, n + 1, control)
968
+ return { page: { heights, normals: null, control }, min: qmin, max: qmax }
969
+ }
970
+
971
+ /**
972
+ * A page above the blocks from its four children (spec 0071): heights and control as their even
973
+ * samples, normals as the [1 2 1]² average of the children's around each texel. `children` is
974
+ * the four decoded pages in order (x, z), (x+1, z), (x, z+1), (x+1, z+1) (null past the terrain's
975
+ * edge), and `normalAt(i, j, out)` writes the children's normal at child-grid texel (i, j), from
976
+ * −1 to 2 × PAGE + 1 (the ring from the neighbors' pages, clamped at the terrain's edge).
977
+ * Returns the decoded page.
978
+ */
979
+ export function parentPage(children, cells, normalAt) {
980
+ const heights = new Uint16Array(SIDE * SIDE)
981
+ const normals = new Uint8Array(SIDE * SIDE * 2)
982
+ const control = new Uint8Array((cells + 1) * (cells + 1) * 4)
983
+ // Child-grid texel (gi, gj), 0 to 2 × PAGE: a shared edge is read from the lower child.
984
+ const v = new Float64Array(3)
985
+ for (let j = 0; j < SIDE; j++) {
986
+ const gj = 2 * j
987
+ const cz = gj > PAGE ? 1 : 0
988
+ for (let i = 0; i < SIDE; i++) {
989
+ const gi = 2 * i
990
+ const cx = gi > PAGE ? 1 : 0
991
+ const c = children[cz * 2 + cx]
992
+ heights[j * SIDE + i] = c ? c.heights[(gj - cz * PAGE) * SIDE + gi - cx * PAGE] : 0
993
+ let sx = 0
994
+ let sy = 0
995
+ let sz = 0
996
+ for (let b = -1; b <= 1; b++) {
997
+ for (let a = -1; a <= 1; a++) {
998
+ const wgt = (2 - Math.abs(a)) * (2 - Math.abs(b))
999
+ normalAt(gi + a, gj + b, v)
1000
+ sx += v[0] * wgt
1001
+ sy += v[1] * wgt
1002
+ sz += v[2] * wgt
1003
+ }
1004
+ }
1005
+ const l = Math.sqrt(sx * sx + sy * sy + sz * sz) || 1
1006
+ normals[j * SIDE + i] = normalByte(sx / l)
1007
+ normals[SIDE * SIDE + j * SIDE + i] = normalByte(sz / l)
1008
+ }
1009
+ }
1010
+ for (let b = 0; b <= cells; b++) {
1011
+ for (let a = 0; a <= cells; a++) {
1012
+ const ca = 2 * a
1013
+ const cb = 2 * b
1014
+ const cx = ca > cells ? 1 : 0
1015
+ const cz = cb > cells ? 1 : 0
1016
+ const c = children[cz * 2 + cx]
1017
+ if (!c) continue
1018
+ const src = ((cb - cz * cells) * (cells + 1) + (ca - cx * cells)) * 4
1019
+ control.set(c.control.subarray(src, src + 4), (b * (cells + 1) + a) * 4)
1020
+ }
1021
+ }
1022
+ return { heights, normals, control }
1023
+ }
1024
+
1025
+ /** A decoded parent page's normal at texel (i, j), unpacked to a unit vector in `out`. */
1026
+ export function pageNormal(page, i, j, out) {
1027
+ const x = byteNormal(page.normals[j * SIDE + i])
1028
+ const z = byteNormal(page.normals[SIDE * SIDE + j * SIDE + i])
1029
+ out[0] = x
1030
+ out[1] = Math.sqrt(Math.max(0, 1 - x * x - z * z))
1031
+ out[2] = z
1032
+ return out
1033
+ }