@aethervtt/shard-terrain 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,13 @@
1
+ /** Texels a side of the chunk table (one RGBA8 texel per pool slot, rows of this many). */
2
+ export declare const CHUNK_TABLE_WIDTH = 1024;
3
+ /**
4
+ * Heightfield terrain's surface (spec 0071). One shared grid mesh draws every chunk: its vertex
5
+ * stage reads heights from the chunk's page in the pool (vertex fetch), geomorphs odd vertices
6
+ * toward their parent level from the page's own samples, and drops skirts; its surface stage reads
7
+ * the page's normals and control texels per fragment and blends the two heaviest material layers
8
+ * from the albedo and ORM arrays (planar on XZ, triplanar on steep `triplanar` layers). Shading
9
+ * normals are the pages' (averaged from their subtree, so far ground keeps its relief); layer
10
+ * normal maps would take a 17th sampled texture on the baseline tier.
11
+ */
12
+ export declare const TerrainSurfaceMaterial: import("@aethervtt/shard-render").MaterialType;
13
+ export declare const HEIGHTFIELD_SHADERS: Record<string, string>;
@@ -0,0 +1,394 @@
1
+ import { t } from "@aethervtt/shard-core";
2
+ import { defineMaterial } from "@aethervtt/shard-render";
3
+ import { MORPH_WGSL } from "../lod.js";
4
+ import { LEAF_SIDE, PAGE } from "./kernel.js";
5
+ const CHUNK_TABLE_WIDTH = 1024;
6
+ const hidden = (description) => t.vec4({ hidden: true, description });
7
+ const TerrainSurfaceMaterial = defineMaterial("terrain/TerrainSurface", {
8
+ shader: "terrain::heightfield",
9
+ arrays: ["pages", "control", "albedoArray", "ormArray"],
10
+ standardTextures: false,
11
+ fields: {
12
+ pages: t.handle("Texture", {
13
+ hidden: true,
14
+ description: "Set by the terrain: the page pool, RGBA8 texels of (height low, height high, normal x, normal z)."
15
+ }),
16
+ control: t.handle("Texture", {
17
+ hidden: true,
18
+ description: "Set by the terrain: control texels per page (two layers and their blend)."
19
+ }),
20
+ chunks: t.handle("Texture", {
21
+ hidden: true,
22
+ description: "Set by the terrain: per pool slot, edge locks, drawn quadrants, fade and depth."
23
+ }),
24
+ layerTable: t.handle("Texture", {
25
+ hidden: true,
26
+ description: "Set by the terrain: per material layer, its array layers, scale, tint, triplanar."
27
+ }),
28
+ albedoArray: t.handle("Texture", { description: "The source\u2019s albedo texture array." }),
29
+ ormArray: t.handle("Texture", {
30
+ description: "The source\u2019s occlusion/roughness/metallic array."
31
+ }),
32
+ center: hidden("Set by the terrain: its origin in the origin frame (xyz)."),
33
+ rot0: hidden("Set by the terrain: origin \u2192 terrain rotation rows (xyz), texture origin (w)."),
34
+ rot1: hidden("Set by the terrain."),
35
+ rot2: hidden("Set by the terrain."),
36
+ camera: hidden(
37
+ "Set by the terrain: the selecting camera (xyz), pixels per radian / errorPixels (w)."
38
+ ),
39
+ pool: hidden(
40
+ "Set by the terrain: pages per atlas row, per layer, atlas side, control cell side."
41
+ ),
42
+ range: hidden(
43
+ "Set by the terrain: lowest and highest height, paint cells per page, layer count."
44
+ ),
45
+ errors0: hidden("Set by the terrain: morph error per depth (0\u20133)."),
46
+ errors1: hidden("Set by the terrain: morph error per depth (4\u20137)."),
47
+ errors2: hidden("Set by the terrain: morph error per depth (8\u201311)."),
48
+ skirts0: hidden("Set by the terrain: skirt depth per depth (0\u20133)."),
49
+ skirts1: hidden("Set by the terrain: skirt depth per depth (4\u20137)."),
50
+ skirts2: hidden("Set by the terrain: skirt depth per depth (8\u201311)."),
51
+ debug: hidden(
52
+ "Set by the terrain: debug mode (0 off, 1 layers, 2 seams, 3 levels, 4 normals, 5 pages), has albedo, has ORM, texture period."
53
+ )
54
+ },
55
+ description: "Heightfield terrain: vertex fetch from streamed pages, geomorphing between levels, and two blended material layers from the paint control pages."
56
+ });
57
+ const S = TerrainSurfaceMaterial.varName;
58
+ const HEIGHTFIELD_SHADERS = {
59
+ "terrain::heightfield": `
60
+ import shard::pbr::types::{ VertexOutput, PbrInput };
61
+ import shard::view::view;
62
+ import shard::mesh::{ vertex_world, vertex_instance_data };
63
+ import material::terrain_surface::{ ${S}, ${S}_pages, ${S}_pages_sampler, ${S}_control, ${S}_chunks, ${S}_layerTable, ${S}_albedoArray, ${S}_albedoArray_sampler, ${S}_ormArray, ${S}_ormArray_sampler };
64
+
65
+ /** Page grid: PAGE segments a side; each page takes CELL texels a side in the pool (a border). */
66
+ const PAGE: i32 = ${PAGE};
67
+ const CELL: i32 = ${LEAF_SIDE};
68
+
69
+ struct Chunk {
70
+ locks: u32,
71
+ mask: u32,
72
+ fade: f32,
73
+ depth: u32,
74
+ /** A leaf near an anchor: drawn exactly as its collider, no distance morph. */
75
+ anchored: bool,
76
+ layer: i32,
77
+ corner: vec2i,
78
+ }
79
+
80
+ /** Where a pool slot's page is: its atlas layer and corner texel. */
81
+ fn terrain_cell(slot: u32) -> vec3i {
82
+ let per_row = u32(${S}.pool.x + 0.5);
83
+ let per_layer = u32(${S}.pool.y + 0.5);
84
+ let cell = slot % per_layer;
85
+ return vec3i(i32(cell % per_row) * CELL, i32(cell / per_row) * CELL, i32(slot / per_layer));
86
+ }
87
+
88
+ /** The drawn chunk: InstanceData.y is its pool slot; the chunk table has the rest. */
89
+ fn terrain_chunk() -> Chunk {
90
+ let slot = u32(vertex_instance_data().y + 0.5);
91
+ let t = textureLoad(${S}_chunks, vec2i(i32(slot % ${CHUNK_TABLE_WIDTH}u), i32(slot / ${CHUNK_TABLE_WIDTH}u)), 0);
92
+ var c: Chunk;
93
+ c.locks = u32(t.r * 255.0 + 0.5);
94
+ c.mask = u32(t.g * 255.0 + 0.5);
95
+ c.fade = t.b;
96
+ let d = u32(t.a * 255.0 + 0.5);
97
+ c.depth = d & 127u;
98
+ c.anchored = d >= 128u;
99
+ let cell = terrain_cell(slot);
100
+ c.corner = cell.xy;
101
+ c.layer = cell.z;
102
+ return c;
103
+ }
104
+
105
+ /** The page's height (m) at vertex (i, j), from its 16-bit sample. */
106
+ fn page_height(c: Chunk, i: i32, j: i32) -> f32 {
107
+ let t = textureLoad(${S}_pages, c.corner + vec2i(i + 1, j + 1), c.layer, 0);
108
+ let q = round(t.r * 255.0) + round(t.g * 255.0) * 256.0;
109
+ return ${S}.range.x + q * ((${S}.range.y - ${S}.range.x) / 65535.0);
110
+ }
111
+
112
+ /** Depth \`depth\`'s entry of a 12-entry table packed in three vec4s (selects, no array: see paint_offer). */
113
+ fn per_depth(a: vec4f, b: vec4f, c: vec4f, depth: u32) -> f32 {
114
+ let d = min(depth, 11u);
115
+ let v = select(select(c, b, d < 8u), a, d < 4u);
116
+ let i = d % 4u;
117
+ return select(select(select(v.w, v.z, i == 2u), v.y, i == 1u), v.x, i == 0u);
118
+ }
119
+
120
+ /**
121
+ * A vertex's lock code (grid-mesh.ts lockCode): its edge on the border ring (0 bottom, 1 right,
122
+ * 2 top, 3 left, each edge owning its first corner), the center lines (4-7), the center (8), else -1.
123
+ */
124
+ fn terrain_lock(i: i32, j: i32) -> i32 {
125
+ let s = PAGE;
126
+ let h = PAGE / 2;
127
+ if (j == 0 && i < s) { return 0; }
128
+ if (i == s && j < s) { return 1; }
129
+ if (j == s && i > 0) { return 2; }
130
+ if (i == 0 && j > 0) { return 3; }
131
+ if (i == h && j == h) { return 8; }
132
+ if (i == h) { return select(5, 4, j < h); }
133
+ if (j == h) { return select(7, 6, i < h); }
134
+ return -1;
135
+ }
136
+
137
+ /**
138
+ * How far toward its parent level a vertex has morphed (0043's rules): by distance within the band
139
+ * where the parent splits (not at all on a leaf near an anchor: what's drawn there is the collider);
140
+ * overridden on edges by the chunk's locks (1 this level, 2 the parent's) and on center lines where
141
+ * a partial parent meets a child; never less than the chunk's fade.
142
+ */
143
+ fn terrain_morph(c: Chunk, i: i32, j: i32, local: vec3f) -> f32 {
144
+ var t = 0.0;
145
+ let split = select(per_depth(${S}.errors0, ${S}.errors1, ${S}.errors2, c.depth) * ${S}.camera.w, 0.0, c.depth == 0u || c.anchored);
146
+ if (split > 0.0) {
147
+ let d = distance(vertex_world(local), ${S}.camera.xyz);
148
+ t = ${MORPH_WGSL};
149
+ }
150
+ let code = terrain_lock(i, j);
151
+ if (code >= 0 && code < 4) {
152
+ let bits = (c.locks >> (u32(code) * 2u)) & 3u;
153
+ if (bits == 1u) { return 0.0; } else if (bits == 2u) { return 1.0; }
154
+ return t;
155
+ } else if (code >= 4 && c.mask != 15u) {
156
+ var a = 0u;
157
+ var b = 1u;
158
+ if (code == 5) { a = 2u; b = 3u; } else if (code == 6) { a = 0u; b = 2u; } else if (code == 7) { a = 1u; b = 3u; }
159
+ let differ = ((c.mask >> a) & 1u) != ((c.mask >> b) & 1u);
160
+ if (differ || (code == 8 && c.mask != 0u)) { return 0.0; }
161
+ }
162
+ return max(t, c.fade);
163
+ }
164
+
165
+ /**
166
+ * Object space is the page's grid (x, z from -32 to 32, scaled to metres by the chunk's transform)
167
+ * and height in metres. Odd vertices morph to the parent level's surface, interpolated from their
168
+ * even neighbors on the same page (odd-odd ones along the anti-diagonal, as the parent's quad is
169
+ * split); skirt vertices (position.y 1) hang below.
170
+ */
171
+ override fn vertex_position(position: vec3f, normal: vec3f, uv: vec2f) -> vec3f {
172
+ let c = terrain_chunk();
173
+ let i = i32(position.x + 0.5);
174
+ let j = i32(position.z + 0.5);
175
+ let h = page_height(c, i, j);
176
+ let oi = (i & 1) == 1;
177
+ let oj = (j & 1) == 1;
178
+ var parent = h;
179
+ if (oi && oj) {
180
+ parent = 0.5 * (page_height(c, i + 1, j - 1) + page_height(c, i - 1, j + 1));
181
+ } else if (oi) {
182
+ parent = 0.5 * (page_height(c, i - 1, j) + page_height(c, i + 1, j));
183
+ } else if (oj) {
184
+ parent = 0.5 * (page_height(c, i, j - 1) + page_height(c, i, j + 1));
185
+ }
186
+ let local = vec3f(f32(i - PAGE / 2), h, f32(j - PAGE / 2));
187
+ var y = mix(h, parent, terrain_morph(c, i, j, local));
188
+ if (position.y > 0.5) { y -= per_depth(${S}.skirts0, ${S}.skirts1, ${S}.skirts2, c.depth); }
189
+ return vec3f(local.x, y, local.z);
190
+ }
191
+
192
+ /** To the surface stage: the vertex across its page (xy, 0\u20131), the pool slot (z), its depth (w). */
193
+ override fn vertex_extra(position: vec3f, normal: vec3f, uv: vec2f) -> vec4f {
194
+ let c = terrain_chunk();
195
+ let slot = u32(vertex_instance_data().y + 0.5);
196
+ return vec4f(position.x / f32(PAGE), position.z / f32(PAGE), f32(slot), f32(c.depth));
197
+ }
198
+
199
+ fn layer_info(l: u32) -> vec4f {
200
+ return textureLoad(${S}_layerTable, vec2i(0, i32(l)), 0);
201
+ }
202
+
203
+ fn layer_tint(l: u32) -> vec4f {
204
+ return textureLoad(${S}_layerTable, vec2i(1, i32(l)), 0);
205
+ }
206
+
207
+ /** The two heaviest layers so far (l0 first), and their summed weights (-1 before any). */
208
+ struct Top2 {
209
+ l0: u32,
210
+ w0: f32,
211
+ l1: u32,
212
+ w1: f32,
213
+ }
214
+
215
+ /**
216
+ * Offers layer \`id\` (its entry's weight \`own\`, all its entries' \`total\`) to the top two, in the
217
+ * order the control texels list them: a layer already first, or with no weight here, is skipped,
218
+ * and ties keep the layer listed first. Plain values only: arrays indexed at run time spill out of
219
+ * registers on some GPUs (Apple's), which cost more than the rest of the terrain's shading.
220
+ */
221
+ fn paint_offer(s: Top2, id: u32, own: f32, total: f32) -> Top2 {
222
+ if (own <= 0.0 || (s.w0 >= 0.0 && id == s.l0)) { return s; }
223
+ var r = s;
224
+ if (total > s.w0) {
225
+ r.l1 = s.l0;
226
+ r.w1 = s.w0;
227
+ r.l0 = id;
228
+ r.w0 = total;
229
+ } else if (total > s.w1) {
230
+ r.l1 = id;
231
+ r.w1 = total;
232
+ }
233
+ return r;
234
+ }
235
+
236
+ /** A layer's summed weight over the four texels' two entries each. */
237
+ fn paint_total(id: u32, la: vec4u, lb: vec4u, wa: vec4f, wb: vec4f) -> f32 {
238
+ let x = vec4u(id);
239
+ return dot(select(vec4f(0.0), wa, la == x), vec4f(1.0)) + dot(select(vec4f(0.0), wb, lb == x), vec4f(1.0));
240
+ }
241
+
242
+ struct Layered {
243
+ albedo: vec3f,
244
+ normal: vec3f,
245
+ orm: vec3f,
246
+ }
247
+
248
+ /** One material layer at terrain-space point q: planar on XZ, or triplanar where steep. */
249
+ fn sample_layer(l: u32, q: vec3f, gx: vec3f, gy: vec3f, n: vec3f) -> Layered {
250
+ var out: Layered;
251
+ let info = layer_info(l);
252
+ let tint = layer_tint(l);
253
+ out.normal = n;
254
+ out.orm = vec3f(1.0, 0.9, 0.0);
255
+ if (${S}.debug.y < 0.5) {
256
+ out.albedo = tint.rgb;
257
+ return out;
258
+ }
259
+ let s = 1.0 / max(info.w, 1e-3);
260
+ let ia = i32(info.x + 0.5);
261
+ let io = i32(info.z + 0.5);
262
+ var w = vec3f(0.0, 1.0, 0.0);
263
+ if (tint.w > 0.5 && n.y < 0.7071) {
264
+ w = pow(abs(n), vec3f(4.0));
265
+ w /= max(1e-6, w.x + w.y + w.z);
266
+ }
267
+ var albedo = vec3f(0.0);
268
+ var orm = vec3f(0.0);
269
+ // Top-down always (weight w.y); the sides only on steep triplanar layers.
270
+ let uy = q.xz * s;
271
+ albedo += textureSampleGrad(${S}_albedoArray, ${S}_albedoArray_sampler, uy, ia, gx.xz * s, gy.xz * s).rgb * w.y;
272
+ orm += textureSampleGrad(${S}_ormArray, ${S}_ormArray_sampler, uy, io, gx.xz * s, gy.xz * s).rgb * w.y;
273
+ if (w.x > 0.001) {
274
+ let ux = q.zy * s;
275
+ albedo += textureSampleGrad(${S}_albedoArray, ${S}_albedoArray_sampler, ux, ia, gx.zy * s, gy.zy * s).rgb * w.x;
276
+ orm += textureSampleGrad(${S}_ormArray, ${S}_ormArray_sampler, ux, io, gx.zy * s, gy.zy * s).rgb * w.x;
277
+ }
278
+ if (w.z > 0.001) {
279
+ let uz = q.xy * s;
280
+ albedo += textureSampleGrad(${S}_albedoArray, ${S}_albedoArray_sampler, uz, ia, gx.xy * s, gy.xy * s).rgb * w.z;
281
+ orm += textureSampleGrad(${S}_ormArray, ${S}_ormArray_sampler, uz, io, gx.xy * s, gy.xy * s).rgb * w.z;
282
+ }
283
+ out.albedo = albedo * tint.rgb;
284
+ out.orm = orm;
285
+ return out;
286
+ }
287
+
288
+ override fn pbr_input(in: VertexOutput) -> PbrInput {
289
+ var p: PbrInput;
290
+ p.base_color = vec3f(1.0);
291
+ p.alpha = 1.0;
292
+ p.metallic = 0.0;
293
+ p.roughness = 0.9;
294
+ p.emissive = vec3f(0.0);
295
+ p.occlusion = 1.0;
296
+ let slot = u32(round(in.extra.z));
297
+ let depth = u32(round(in.extra.w));
298
+ let cell = terrain_cell(slot);
299
+ let uv = clamp(in.extra.xy, vec2f(0.0), vec2f(1.0));
300
+ // Normals: the page's, filtered across its texels (vertex i is texel i + 1).
301
+ let texel = vec2f(cell.xy) + 1.5 + uv * f32(PAGE);
302
+ let nt = textureSampleLevel(${S}_pages, ${S}_pages_sampler, texel / ${S}.pool.z, cell.z, 0.0);
303
+ let nx = nt.b * 2.0 - 1.0;
304
+ let nz = nt.a * 2.0 - 1.0;
305
+ let nl = normalize(vec3f(nx, sqrt(max(0.0, 1.0 - nx * nx - nz * nz)), nz));
306
+ // Terrain space to world: the rotation rows transposed.
307
+ let n = normalize(${S}.rot0.xyz * nl.x + ${S}.rot1.xyz * nl.y + ${S}.rot2.xyz * nl.z);
308
+ p.normal = n;
309
+ // Terrain-space position (offset so it stays small; repeats line up across the offset).
310
+ let q = vec3f(dot(${S}.rot0.xyz, in.world_position), dot(${S}.rot1.xyz, in.world_position), dot(${S}.rot2.xyz, in.world_position)) +
311
+ vec3f(${S}.rot0.w, ${S}.rot1.w, ${S}.rot2.w);
312
+ let gx = dpdx(q);
313
+ let gy = dpdy(q);
314
+ // Paint: the four control texels around the fragment, bilinear, their layers summed.
315
+ let cells = ${S}.range.z;
316
+ let side = ${S}.pool.w;
317
+ let ccorner = vec2i(cell.xy / CELL) * i32(side + 0.5);
318
+ let pc = uv * cells;
319
+ let c0 = clamp(vec2i(floor(pc)), vec2i(0), vec2i(i32(cells) - 1));
320
+ let f = pc - vec2f(c0);
321
+ // Texels (0,0), (1,0), (0,1), (1,1) in x, y, z, w; each lists two layers and the second's share.
322
+ let base = ccorner + c0;
323
+ let t0 = textureLoad(${S}_control, base, cell.z, 0);
324
+ let t1 = textureLoad(${S}_control, base + vec2i(1, 0), cell.z, 0);
325
+ let t2 = textureLoad(${S}_control, base + vec2i(0, 1), cell.z, 0);
326
+ let t3 = textureLoad(${S}_control, base + vec2i(1, 1), cell.z, 0);
327
+ let bw = vec4f((1.0 - f.x) * (1.0 - f.y), f.x * (1.0 - f.y), (1.0 - f.x) * f.y, f.x * f.y);
328
+ let sh = vec4f(t0.b, t1.b, t2.b, t3.b);
329
+ let la = vec4u(vec4f(t0.r, t1.r, t2.r, t3.r) * 255.0 + 0.5);
330
+ let lb = vec4u(vec4f(t0.g, t1.g, t2.g, t3.g) * 255.0 + 0.5);
331
+ let wa = bw * (1.0 - sh);
332
+ let wb = bw * sh;
333
+ var top = Top2(0u, -1.0, 0u, -1.0);
334
+ top = paint_offer(top, la.x, wa.x, paint_total(la.x, la, lb, wa, wb));
335
+ top = paint_offer(top, lb.x, wb.x, paint_total(lb.x, la, lb, wa, wb));
336
+ top = paint_offer(top, la.y, wa.y, paint_total(la.y, la, lb, wa, wb));
337
+ top = paint_offer(top, lb.y, wb.y, paint_total(lb.y, la, lb, wa, wb));
338
+ top = paint_offer(top, la.z, wa.z, paint_total(la.z, la, lb, wa, wb));
339
+ top = paint_offer(top, lb.z, wb.z, paint_total(lb.z, la, lb, wa, wb));
340
+ top = paint_offer(top, la.w, wa.w, paint_total(la.w, la, lb, wa, wb));
341
+ top = paint_offer(top, lb.w, wb.w, paint_total(lb.w, la, lb, wa, wb));
342
+ let l0 = top.l0;
343
+ let w0 = top.w0;
344
+ let w1 = top.w1;
345
+ let l1 = select(l0, top.l1, w1 > 0.0);
346
+ let share = select(0.0, w1 / max(1e-6, w0 + w1), w1 > 0.0);
347
+ let debug = u32(${S}.debug.x + 0.5);
348
+ if (debug == 1u) {
349
+ p.base_color = mix(layer_tint(l0).rgb, layer_tint(l1).rgb, share);
350
+ return p;
351
+ }
352
+ if (debug == 2u) {
353
+ p.base_color = vec3f(1.0);
354
+ p.emissive = vec3f(20000.0);
355
+ return p;
356
+ }
357
+ if (debug == 3u) {
358
+ let hue = fract(f32(depth) * 0.37);
359
+ p.base_color = 0.5 + 0.5 * cos(6.2832 * (vec3f(hue) + vec3f(0.0, 0.33, 0.67)));
360
+ p.emissive = p.base_color * 3000.0;
361
+ return p;
362
+ }
363
+ if (debug == 4u) {
364
+ p.base_color = nl * 0.5 + 0.5;
365
+ p.emissive = p.base_color * 3000.0;
366
+ return p;
367
+ }
368
+ if (debug == 5u) {
369
+ let hue = fract(f32(slot) * 0.618);
370
+ p.base_color = 0.5 + 0.5 * cos(6.2832 * (vec3f(hue) + vec3f(0.0, 0.33, 0.67)));
371
+ return p;
372
+ }
373
+ let a = sample_layer(l0, q, gx, gy, nl);
374
+ var c = a;
375
+ if (share > 0.004 && l1 != l0) {
376
+ let b = sample_layer(l1, q, gx, gy, nl);
377
+ c.albedo = mix(a.albedo, b.albedo, share);
378
+ c.orm = mix(a.orm, b.orm, share);
379
+ }
380
+ p.base_color = c.albedo;
381
+ if (${S}.debug.z > 0.5) {
382
+ p.occlusion = c.orm.r;
383
+ p.roughness = clamp(c.orm.g, 0.045, 1.0);
384
+ p.metallic = c.orm.b;
385
+ }
386
+ return p;
387
+ }
388
+ `
389
+ };
390
+ export {
391
+ CHUNK_TABLE_WIDTH,
392
+ HEIGHTFIELD_SHADERS,
393
+ TerrainSurfaceMaterial
394
+ };
@@ -0,0 +1,104 @@
1
+ import { type World } from '@aethervtt/shard-core';
2
+ import type { HeightfieldRuntime } from './runtime.js';
3
+ /** A heightfield from an entity id or scene path; the only one when omitted. */
4
+ export declare function resolveHeightfield(world: World, ref: unknown): HeightfieldRuntime;
5
+ /** `terrain.describe` for one heightfield (spec 0071). */
6
+ export declare function describeHeightfield(world: World, rt: HeightfieldRuntime): {
7
+ entity: number;
8
+ path: string | null;
9
+ source: string | null;
10
+ ready: boolean;
11
+ streaming: boolean;
12
+ waitingFor: string | null;
13
+ problem: {
14
+ code: string;
15
+ message: string;
16
+ hint?: string;
17
+ path?: string;
18
+ details?: unknown[];
19
+ } | null;
20
+ size: number[] | null;
21
+ spacing: number | null;
22
+ heightRange: [number, number] | null;
23
+ roots: {
24
+ x: number;
25
+ z: number;
26
+ size: number;
27
+ } | null;
28
+ depths: number;
29
+ version: number;
30
+ bake: {
31
+ state: import("./runtime.js").BakeState;
32
+ blocks: number;
33
+ current: number;
34
+ stale: number;
35
+ baking: number;
36
+ lastBake: {
37
+ rebaked: number;
38
+ pages: number;
39
+ ms: number;
40
+ msPerBlock: number;
41
+ outOfRange: import("./bake.js").OutOfRange[];
42
+ } | null;
43
+ };
44
+ detail: {
45
+ errorPixels: number | null;
46
+ vertexPixels: number | null;
47
+ lodBias: number;
48
+ errors: number[];
49
+ };
50
+ selected: {
51
+ chunks: number;
52
+ byDepth: Record<number, number>;
53
+ partial: number;
54
+ requested: number;
55
+ waiting: number;
56
+ };
57
+ pages: {
58
+ residentDepth: number;
59
+ resident: number;
60
+ cached: number;
61
+ readsInFlight: number;
62
+ reads: number;
63
+ bytesRead: number;
64
+ bakedInline: number;
65
+ };
66
+ pool: {
67
+ slots: number;
68
+ used: number;
69
+ uploaded: number;
70
+ uploadedLastFrame: number;
71
+ waitingToUpload: number;
72
+ evicted: number;
73
+ } | null;
74
+ colliders: {
75
+ tiles: number;
76
+ active: number;
77
+ pending: number;
78
+ fromPages: number;
79
+ bakedInline: number;
80
+ anchors: number;
81
+ };
82
+ };
83
+ /** `terrain.sample` on a heightfield: loads the leaf pages under the points first. */
84
+ export declare function sampleHeightfield(world: World, rt: HeightfieldRuntime, points: number[][]): Promise<{
85
+ x: number;
86
+ z: number;
87
+ height: number;
88
+ normal: number[];
89
+ slope: number;
90
+ layers: [string, string];
91
+ share: number;
92
+ depth: number;
93
+ }[]>;
94
+ /**
95
+ * A top-down image of a heightfield (`terrain.map`), `width` pixels across, +Z down the image:
96
+ * `height` dark lowlands to white peaks, `slope` flat black to cliffs white, `layers` the heaviest
97
+ * paint layer's tint, `bake` blocks by state (green current, amber stale, cyan dirtied by the last
98
+ * bake), from the finest pages held (the coarse levels always are).
99
+ */
100
+ export declare function heightfieldMap(rt: HeightfieldRuntime, width: number, mode: string): {
101
+ width: number;
102
+ height: number;
103
+ data: Uint8Array<ArrayBuffer>;
104
+ };