@aethervtt/shard-terrain 0.0.1 → 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.
- package/dist/biomes.d.ts +1 -0
- package/dist/biomes.js +3 -0
- package/dist/chunk.d.ts +0 -44
- package/dist/chunk.js +2 -127
- package/dist/colliders.d.ts +16 -1
- package/dist/colliders.js +39 -24
- package/dist/components.d.ts +5 -0
- package/dist/components.js +7 -2
- package/dist/cube-sphere.d.ts +50 -0
- package/dist/cube-sphere.js +224 -0
- package/dist/grid-mesh.d.ts +49 -0
- package/dist/grid-mesh.js +208 -0
- package/dist/heightfield/bake.d.ts +120 -0
- package/dist/heightfield/bake.js +494 -0
- package/dist/heightfield/colliders.d.ts +39 -0
- package/dist/heightfield/colliders.js +152 -0
- package/dist/heightfield/component.d.ts +14 -0
- package/dist/heightfield/component.js +59 -0
- package/dist/heightfield/deflate.d.ts +11 -0
- package/dist/heightfield/deflate.js +484 -0
- package/dist/heightfield/heightmap.d.ts +27 -0
- package/dist/heightfield/heightmap.js +160 -0
- package/dist/heightfield/kernel.d.ts +277 -0
- package/dist/heightfield/kernel.js +1033 -0
- package/dist/heightfield/material.d.ts +13 -0
- package/dist/heightfield/material.js +394 -0
- package/dist/heightfield/methods.d.ts +104 -0
- package/dist/heightfield/methods.js +218 -0
- package/dist/heightfield/pack.d.ts +59 -0
- package/dist/heightfield/pack.js +127 -0
- package/dist/heightfield/pages.d.ts +91 -0
- package/dist/heightfield/pages.js +244 -0
- package/dist/heightfield/project.d.ts +40 -0
- package/dist/heightfield/project.js +55 -0
- package/dist/heightfield/queries.d.ts +48 -0
- package/dist/heightfield/queries.js +150 -0
- package/dist/heightfield/render.d.ts +108 -0
- package/dist/heightfield/render.js +727 -0
- package/dist/heightfield/root-grid.d.ts +38 -0
- package/dist/heightfield/root-grid.js +109 -0
- package/dist/heightfield/runtime.d.ts +105 -0
- package/dist/heightfield/runtime.js +312 -0
- package/dist/heightfield/source-asset.d.ts +39 -0
- package/dist/heightfield/source-asset.js +372 -0
- package/dist/heightfield/source.d.ts +151 -0
- package/dist/heightfield/source.js +402 -0
- package/dist/heightfield/stack.d.ts +29 -0
- package/dist/heightfield/stack.js +158 -0
- package/dist/heightfield/system.d.ts +22 -0
- package/dist/heightfield/system.js +165 -0
- package/dist/heightfield/testing.d.ts +317 -0
- package/dist/heightfield/testing.js +441 -0
- package/dist/heightfield/worker.js +47 -0
- package/dist/heights.d.ts +8 -3
- package/dist/heights.js +8 -4
- package/dist/index.d.ts +24 -7
- package/dist/index.js +158 -8
- package/dist/lod.d.ts +27 -0
- package/dist/lod.js +23 -1
- package/dist/material.js +2 -1
- package/dist/methods.js +64 -16
- package/dist/overlays.d.ts +2 -0
- package/dist/overlays.js +48 -6
- package/dist/planet.d.ts +2 -7
- package/dist/planet.js +8 -18
- package/dist/plugin.d.ts +10 -3
- package/dist/plugin.js +48 -8
- package/dist/quadtree.d.ts +61 -39
- package/dist/quadtree.js +44 -208
- package/dist/render.d.ts +7 -1
- package/dist/render.js +18 -8
- package/dist/test-planet.d.ts +39 -0
- package/dist/test-planet.js +41 -5
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +43 -3
- 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
|
+
}
|