@aethervtt/shard-terrain 0.0.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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/biomes.d.ts +65 -0
- package/dist/biomes.js +163 -0
- package/dist/chunk.d.ts +99 -0
- package/dist/chunk.js +304 -0
- package/dist/colliders.d.ts +86 -0
- package/dist/colliders.js +356 -0
- package/dist/components.d.ts +49 -0
- package/dist/components.js +137 -0
- package/dist/cube.d.ts +51 -0
- package/dist/cube.js +87 -0
- package/dist/frame.d.ts +21 -0
- package/dist/frame.js +63 -0
- package/dist/heights.d.ts +51 -0
- package/dist/heights.js +137 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +193 -0
- package/dist/kernel.d.ts +47 -0
- package/dist/kernel.js +243 -0
- package/dist/lod.d.ts +22 -0
- package/dist/lod.js +44 -0
- package/dist/material.d.ts +14 -0
- package/dist/material.js +426 -0
- package/dist/methods.d.ts +12 -0
- package/dist/methods.js +259 -0
- package/dist/nav.d.ts +14 -0
- package/dist/nav.js +114 -0
- package/dist/overlays.d.ts +6 -0
- package/dist/overlays.js +67 -0
- package/dist/planet.d.ts +106 -0
- package/dist/planet.js +313 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +98 -0
- package/dist/points.d.ts +62 -0
- package/dist/points.js +207 -0
- package/dist/quadtree.d.ts +153 -0
- package/dist/quadtree.js +587 -0
- package/dist/render.d.ts +179 -0
- package/dist/render.js +1222 -0
- package/dist/test-planet.d.ts +59 -0
- package/dist/test-planet.js +189 -0
- package/dist/testing.d.ts +42 -0
- package/dist/testing.js +96 -0
- package/dist/view.d.ts +12 -0
- package/dist/view.js +61 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/biomes.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Most biomes a BiomeSet may list. */
|
|
2
|
+
export declare const MAX_BIOMES = 32;
|
|
3
|
+
/** Most texture layers per biome. */
|
|
4
|
+
export declare const MAX_LAYERS = 4;
|
|
5
|
+
/** Biomes a fragment blends (the heaviest ones). */
|
|
6
|
+
export declare const BLENDED_BIOMES = 4;
|
|
7
|
+
export declare const Biome: import("@aethervtt/shard-assets").DataType<{
|
|
8
|
+
readonly layers: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").InferFields<{
|
|
9
|
+
layer: import("@aethervtt/shard-core").FieldType<number, "u16">;
|
|
10
|
+
scale: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
11
|
+
}>[], "object">;
|
|
12
|
+
readonly temperature: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
13
|
+
readonly moisture: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
14
|
+
readonly height: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
15
|
+
readonly slope: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
16
|
+
readonly blend: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
17
|
+
readonly tint: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
18
|
+
}, "terrain/Biome">;
|
|
19
|
+
export declare const BiomeSet: import("@aethervtt/shard-assets").DataType<{
|
|
20
|
+
readonly biomes: import("@aethervtt/shard-core").FieldType<(import("@aethervtt/shard-core").AssetRef<"terrain/Biome"> | null)[], "object">;
|
|
21
|
+
readonly albedo: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
|
|
22
|
+
readonly normal: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
|
|
23
|
+
readonly orm: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").AssetRef<"Texture"> | null, "object">;
|
|
24
|
+
readonly latitudeBias: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
25
|
+
readonly snowLine: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
26
|
+
}, "terrain/BiomeSet">;
|
|
27
|
+
export type BiomeValue = ReturnType<typeof Biome.defaults>;
|
|
28
|
+
export type BiomeSetValue = ReturnType<typeof BiomeSet.defaults>;
|
|
29
|
+
/** A biome set resolved for sampling: the loaded biomes in order, flattened into numbers. */
|
|
30
|
+
export interface BiomeTable {
|
|
31
|
+
count: number;
|
|
32
|
+
/** Per biome, 12 floats: t lo, t hi, m lo, m hi, h lo, h hi, s lo, s hi, blend, layers, 0, 0. */
|
|
33
|
+
ranges: Float32Array;
|
|
34
|
+
/** Per biome, rgba tint. */
|
|
35
|
+
tints: Float32Array;
|
|
36
|
+
/** Per biome, 4 layer indices and 4 scales. */
|
|
37
|
+
layers: Float32Array;
|
|
38
|
+
latitudeBias: number;
|
|
39
|
+
snowLine: number;
|
|
40
|
+
}
|
|
41
|
+
/** The table for a set whose biomes are loaded (undefined entries fall back to defaults). */
|
|
42
|
+
export declare function biomeTable(set: BiomeSetValue | undefined, biomes: readonly (BiomeValue | undefined)[]): BiomeTable;
|
|
43
|
+
/** 1 inside [lo, hi] with soft edges `blend × (hi − lo)` wide; 1 everywhere when lo ≥ hi. */
|
|
44
|
+
export declare function biomeWindow(x: number, lo: number, hi: number, blend: number): number;
|
|
45
|
+
/** What biome selection reads at a point. */
|
|
46
|
+
export interface BiomeInputs {
|
|
47
|
+
temperature: number;
|
|
48
|
+
moisture: number;
|
|
49
|
+
/** Metres above radius. */
|
|
50
|
+
height: number;
|
|
51
|
+
/** Degrees from flat. */
|
|
52
|
+
slope: number;
|
|
53
|
+
/** Sine of the latitude (−1 south pole, 1 north pole). */
|
|
54
|
+
latitude: number;
|
|
55
|
+
}
|
|
56
|
+
/** Temperature after latitude and altitude cool it: what the temperature windows test. */
|
|
57
|
+
export declare function effectiveTemperature(table: BiomeTable, p: BiomeInputs): number;
|
|
58
|
+
/**
|
|
59
|
+
* Every biome's weight at a point (summing to 1): the product of its four windows, normalized.
|
|
60
|
+
* Where no window fits, the first biome gets everything. The terrain shader computes the same
|
|
61
|
+
* thing per fragment and blends the four heaviest.
|
|
62
|
+
*/
|
|
63
|
+
export declare function biomeWeights(table: BiomeTable, p: BiomeInputs, out: Float32Array): Float32Array;
|
|
64
|
+
/** The index of the heaviest weight. */
|
|
65
|
+
export declare function dominantBiome(weights: ArrayLike<number>, count: number): number;
|
package/dist/biomes.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { defineDataType } from "@aethervtt/shard-assets";
|
|
2
|
+
import { ShardError, t } from "@aethervtt/shard-core";
|
|
3
|
+
const MAX_BIOMES = 32;
|
|
4
|
+
const MAX_LAYERS = 4;
|
|
5
|
+
const BLENDED_BIOMES = 4;
|
|
6
|
+
const range = (what, unit) => t.vec2({
|
|
7
|
+
description: `[min, max] ${what}${unit}. Equal ends (the default): anywhere.`
|
|
8
|
+
});
|
|
9
|
+
const Biome = defineDataType(
|
|
10
|
+
"terrain/Biome",
|
|
11
|
+
{
|
|
12
|
+
layers: t.list(
|
|
13
|
+
t.struct({
|
|
14
|
+
layer: t.u16({
|
|
15
|
+
description: "Layer of the BiomeSet\u2019s albedo, normal, and ORM texture arrays (the same index in each)."
|
|
16
|
+
}),
|
|
17
|
+
scale: t.f32({
|
|
18
|
+
default: 4,
|
|
19
|
+
min: 0.01,
|
|
20
|
+
unit: "m",
|
|
21
|
+
description: "World size of one texture repeat."
|
|
22
|
+
})
|
|
23
|
+
}),
|
|
24
|
+
{
|
|
25
|
+
description: "Up to 4 texture layers, from flat ground (first) to the steepest slopes (last), blended by slope."
|
|
26
|
+
}
|
|
27
|
+
),
|
|
28
|
+
temperature: range("temperature", " (\u22121 cold to 1 hot, after latitude and altitude)"),
|
|
29
|
+
moisture: range("moisture", " (\u22121 dry to 1 wet)"),
|
|
30
|
+
height: range("height", " in metres above the planet radius"),
|
|
31
|
+
slope: range("slope", " in degrees (0 flat, 90 a cliff)"),
|
|
32
|
+
blend: t.f32({
|
|
33
|
+
default: 0.1,
|
|
34
|
+
min: 0,
|
|
35
|
+
max: 1,
|
|
36
|
+
description: "Softness of every range\u2019s edges, as a fraction of the range\u2019s width."
|
|
37
|
+
}),
|
|
38
|
+
tint: t.color({
|
|
39
|
+
default: [1, 1, 1, 1],
|
|
40
|
+
description: "Multiplies the albedo (the whole color when the set has no textures)."
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
extension: "biome",
|
|
45
|
+
description: "Where a surface type grows (temperature, moisture, height, slope windows) and how it looks (texture layers, tint)."
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
const BiomeSet = defineDataType(
|
|
49
|
+
"terrain/BiomeSet",
|
|
50
|
+
{
|
|
51
|
+
biomes: t.list(t.handle("terrain/Biome"), {
|
|
52
|
+
description: `The planet\u2019s biomes (*.biome.json), at most ${MAX_BIOMES}. The first is used where none fits.`
|
|
53
|
+
}),
|
|
54
|
+
albedo: t.handle("Texture", {
|
|
55
|
+
description: "Albedo texture array (*.texarray.json), a layer per surface texture."
|
|
56
|
+
}),
|
|
57
|
+
normal: t.handle("Texture", { description: "Normal map texture array, same layers." }),
|
|
58
|
+
orm: t.handle("Texture", {
|
|
59
|
+
description: "Occlusion / roughness / metallic texture array, same layers."
|
|
60
|
+
}),
|
|
61
|
+
latitudeBias: t.f32({
|
|
62
|
+
default: 0.6,
|
|
63
|
+
description: "Temperature drop from the equator to the poles."
|
|
64
|
+
}),
|
|
65
|
+
snowLine: t.f32({
|
|
66
|
+
min: 0,
|
|
67
|
+
unit: "m",
|
|
68
|
+
description: "Height at which temperature has dropped by 1 (0: altitude doesn\u2019t cool)."
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
extension: "biomes",
|
|
73
|
+
description: "A planet\u2019s biomes and the texture arrays their layers index into."
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
function biomeTable(set, biomes) {
|
|
77
|
+
if (biomes.length > MAX_BIOMES) {
|
|
78
|
+
throw new ShardError(
|
|
79
|
+
"terrain/too-many-biomes",
|
|
80
|
+
`A BiomeSet lists ${biomes.length} biomes; the most is ${MAX_BIOMES}`,
|
|
81
|
+
{ hint: "Merge similar biomes, or split the planet\u2019s surface into fewer, broader ones." }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const count = Math.max(1, biomes.length);
|
|
85
|
+
const ranges = new Float32Array(count * 12);
|
|
86
|
+
const tints = new Float32Array(count * 4);
|
|
87
|
+
const layers = new Float32Array(count * 8);
|
|
88
|
+
for (let b = 0; b < count; b++) {
|
|
89
|
+
const v = biomes[b];
|
|
90
|
+
const o = b * 12;
|
|
91
|
+
if (v) {
|
|
92
|
+
ranges.set([...v.temperature, ...v.moisture, ...v.height, ...v.slope], o);
|
|
93
|
+
ranges[o + 8] = v.blend;
|
|
94
|
+
ranges[o + 9] = Math.min(MAX_LAYERS, v.layers.length);
|
|
95
|
+
tints.set(v.tint, b * 4);
|
|
96
|
+
for (let l = 0; l < MAX_LAYERS; l++) {
|
|
97
|
+
const layer = v.layers[l];
|
|
98
|
+
layers[b * 8 + l] = layer ? layer.layer : 0;
|
|
99
|
+
layers[b * 8 + 4 + l] = layer ? layer.scale : 4;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
ranges[o + 8] = 0.1;
|
|
103
|
+
tints.set([0.5, 0.5, 0.5, 1], b * 4);
|
|
104
|
+
for (let l = 0; l < MAX_LAYERS; l++) layers[b * 8 + 4 + l] = 4;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
count,
|
|
109
|
+
ranges,
|
|
110
|
+
tints,
|
|
111
|
+
layers,
|
|
112
|
+
latitudeBias: set?.latitudeBias ?? 0.6,
|
|
113
|
+
snowLine: set?.snowLine ?? 0
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const smoothstep = (a, b, x) => {
|
|
117
|
+
const t2 = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
118
|
+
return t2 * t2 * (3 - 2 * t2);
|
|
119
|
+
};
|
|
120
|
+
function biomeWindow(x, lo, hi, blend) {
|
|
121
|
+
if (!(lo < hi)) return 1;
|
|
122
|
+
const soft = Math.max(1e-6, blend * (hi - lo));
|
|
123
|
+
return smoothstep(lo - soft, lo + soft, x) * (1 - smoothstep(hi - soft, hi + soft, x));
|
|
124
|
+
}
|
|
125
|
+
function effectiveTemperature(table, p) {
|
|
126
|
+
return p.temperature - table.latitudeBias * Math.abs(p.latitude) - (table.snowLine > 0 ? Math.max(0, p.height) / table.snowLine : 0);
|
|
127
|
+
}
|
|
128
|
+
function biomeWeights(table, p, out) {
|
|
129
|
+
const t2 = effectiveTemperature(table, p);
|
|
130
|
+
let sum = 0;
|
|
131
|
+
for (let b = 0; b < table.count; b++) {
|
|
132
|
+
const r = table.ranges;
|
|
133
|
+
const o = b * 12;
|
|
134
|
+
const blend = r[o + 8];
|
|
135
|
+
const w = biomeWindow(t2, r[o], r[o + 1], blend) * biomeWindow(p.moisture, r[o + 2], r[o + 3], blend) * biomeWindow(p.height, r[o + 4], r[o + 5], blend) * biomeWindow(p.slope, r[o + 6], r[o + 7], blend);
|
|
136
|
+
out[b] = w;
|
|
137
|
+
sum += w;
|
|
138
|
+
}
|
|
139
|
+
if (sum < 1e-6) {
|
|
140
|
+
out.fill(0, 0, table.count);
|
|
141
|
+
out[0] = 1;
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
for (let b = 0; b < table.count; b++) out[b] = out[b] / sum;
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function dominantBiome(weights, count) {
|
|
148
|
+
let best = 0;
|
|
149
|
+
for (let b = 1; b < count; b++) if (weights[b] > weights[best]) best = b;
|
|
150
|
+
return best;
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
BLENDED_BIOMES,
|
|
154
|
+
Biome,
|
|
155
|
+
BiomeSet,
|
|
156
|
+
MAX_BIOMES,
|
|
157
|
+
MAX_LAYERS,
|
|
158
|
+
biomeTable,
|
|
159
|
+
biomeWeights,
|
|
160
|
+
biomeWindow,
|
|
161
|
+
dominantBiome,
|
|
162
|
+
effectiveTemperature
|
|
163
|
+
};
|
package/dist/chunk.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { MeshData } from '@aethervtt/shard-mesh';
|
|
2
|
+
import type { NoiseGraph } from '@aethervtt/shard-noise';
|
|
3
|
+
import { type ChunkPoints } from './points.js';
|
|
4
|
+
/**
|
|
5
|
+
* How a chunk's vertices are laid out, per resolution `n` (vertices per edge, 2^k + 1). The border
|
|
6
|
+
* ring comes first (so edge locks rewrite one contiguous block), then the interior row by row, then
|
|
7
|
+
* one skirt vertex under each ring vertex. Every chunk of a resolution shares one index buffer.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChunkLayout {
|
|
10
|
+
resolution: number;
|
|
11
|
+
/** Ring vertices: 4 (n − 1), counter-clockwise from (0, 0) seen from outside. */
|
|
12
|
+
ring: number;
|
|
13
|
+
/** Surface vertices: n². */
|
|
14
|
+
surface: number;
|
|
15
|
+
/** Surface plus skirt: n² + 4 (n − 1). */
|
|
16
|
+
vertexCount: number;
|
|
17
|
+
/** Vertex index of grid point (i, j), at `i + j * n`. */
|
|
18
|
+
index: Uint16Array;
|
|
19
|
+
/** Grid point of each ring vertex: i, j pairs. */
|
|
20
|
+
ringPoints: Uint16Array;
|
|
21
|
+
/** Triangles: the surface, then the skirts (`chunkIndices` with every quadrant, no stitching). */
|
|
22
|
+
indices: Uint16Array;
|
|
23
|
+
/** Triangles without skirts (the surface only): colliders, and planets with skirts off. */
|
|
24
|
+
surfaceIndices: Uint16Array;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A chunk's triangles (cached per combination, so chunks share the array and its GPU index buffer).
|
|
28
|
+
* `mask` picks quadrants (bit q: quadrant q, in child order (x, y), (x+1, y), (x, y+1),
|
|
29
|
+
* (x+1, y+1)): a split waiting for children draws its own quadrants where they're missing and the
|
|
30
|
+
* children elsewhere. `stitch` bit e marks edge e (0 bottom, 1 right, 2 top, 3 left) as meeting a
|
|
31
|
+
* coarser neighbor: that edge's triangles use only its even vertices, the neighbor's, so there's no
|
|
32
|
+
* T-junction (a vertex on the neighbor's edge only up to rounding shows pixel-sized holes).
|
|
33
|
+
*
|
|
34
|
+
* Quads fill the interior; each edge's strip (between the border and the first inner row) is
|
|
35
|
+
* zipped per half, so no triangle crosses a quadrant boundary. Skirts follow the drawn border.
|
|
36
|
+
*/
|
|
37
|
+
export declare function chunkIndices(n: number, mask: number, stitch: number, skirts: boolean): Uint16Array;
|
|
38
|
+
/** Throws `terrain/bad-resolution` unless `n` is 2^k + 1 (5 to 129). */
|
|
39
|
+
export declare function checkResolution(n: number): void;
|
|
40
|
+
export declare function chunkLayout(n: number): ChunkLayout;
|
|
41
|
+
/**
|
|
42
|
+
* A vertex's lock code (uv1.x): its edge for ring vertices (0 bottom, 1 right, 2 top, 3 left, where
|
|
43
|
+
* the instance's lock bits apply), the quadrant boundary for vertices on the center lines (4 and 5
|
|
44
|
+
* the vertical line's lower and upper half, 6 and 7 the horizontal line's left and right half, 8
|
|
45
|
+
* the center; locked to this level where a partial draw meets a child), else −1.
|
|
46
|
+
*/
|
|
47
|
+
export declare function lockCode(i: number, j: number, n: number, vi: number, ring: number): number;
|
|
48
|
+
/** What a chunk is made from: its node, the planet's numbers, and its graphs. */
|
|
49
|
+
export interface ChunkSpec {
|
|
50
|
+
face: number;
|
|
51
|
+
depth: number;
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
radius: number;
|
|
55
|
+
shape: ArrayLike<number>;
|
|
56
|
+
heightScale: number;
|
|
57
|
+
seed: number;
|
|
58
|
+
resolution: number;
|
|
59
|
+
/** Heights; undefined for a flat surface (the ocean). */
|
|
60
|
+
height: NoiseGraph | undefined;
|
|
61
|
+
/** Climate graph with `temperature` and `moisture` nodes, or undefined (both 0). */
|
|
62
|
+
climate: NoiseGraph | undefined;
|
|
63
|
+
/** The depth's geometric error (m), for the vertex stage's morph band. */
|
|
64
|
+
morphError: number;
|
|
65
|
+
/** How far skirts hang below the border (m). */
|
|
66
|
+
skirtDepth: number;
|
|
67
|
+
/** Offset every height by this (m): the ocean surface is at seaLevel. */
|
|
68
|
+
heightOffset?: number;
|
|
69
|
+
/** Noise origin spacing (m): only error sampling, which needs no seams, changes it. */
|
|
70
|
+
snap?: number;
|
|
71
|
+
}
|
|
72
|
+
/** A generated chunk: vertex data relative to its center, plus what selection needs to know. */
|
|
73
|
+
export interface ChunkMesh {
|
|
74
|
+
/** The chunk's reference point in the planet frame (f64): center direction × radius. */
|
|
75
|
+
center: Float64Array;
|
|
76
|
+
data: MeshData;
|
|
77
|
+
/** Surface heights (m above radius), row by row. */
|
|
78
|
+
heights: Float32Array;
|
|
79
|
+
minHeight: number;
|
|
80
|
+
maxHeight: number;
|
|
81
|
+
/** Largest distance between a vertex and where its parent level puts it (m). */
|
|
82
|
+
error: number;
|
|
83
|
+
}
|
|
84
|
+
/** A node's reference point in the planet frame: its center direction × radius (× shape). */
|
|
85
|
+
export declare function chunkCenter(face: number, depth: number, x: number, y: number, radius: number, shape: ArrayLike<number>, out: Float64Array): Float64Array;
|
|
86
|
+
/**
|
|
87
|
+
* Builds a chunk on the CPU: samples heights and climate over the node plus a one-vertex border
|
|
88
|
+
* (normals at the edges match the neighbors'), then positions relative to the center in f64,
|
|
89
|
+
* central-difference normals, morph deltas to the parent level, and skirts. The same numbers the
|
|
90
|
+
* GPU kernel writes, from the canonical CPU noise (0041): colliders and headless runs use these.
|
|
91
|
+
* Every point samples relative to its own lattice origin (`SNAP`), so a vertex another chunk shares
|
|
92
|
+
* gets the same height there.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildChunk(spec: ChunkSpec): ChunkMesh;
|
|
95
|
+
/**
|
|
96
|
+
* The mesh from a chunk's sample points and their values (graph units; heights before
|
|
97
|
+
* `heightScale`): what `buildChunk` does after sampling, and what pool jobs finish with.
|
|
98
|
+
*/
|
|
99
|
+
export declare function assembleChunk(spec: ChunkSpec, pts: ChunkPoints, values: Float32Array, temps: Float32Array, moist: Float32Array): ChunkMesh;
|
package/dist/chunk.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { faceToDirection, nodeExtent } from "./cube.js";
|
|
3
|
+
import {
|
|
4
|
+
createChunkPoints,
|
|
5
|
+
prepareChunkPoints,
|
|
6
|
+
sampleChunkPoints
|
|
7
|
+
} from "./points.js";
|
|
8
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
9
|
+
const sets = /* @__PURE__ */ new Map();
|
|
10
|
+
function chunkIndices(n, mask, stitch, skirts) {
|
|
11
|
+
const key = `${n}:${mask}:${stitch}:${skirts}`;
|
|
12
|
+
let out = sets.get(key);
|
|
13
|
+
if (out) return out;
|
|
14
|
+
const layout = chunkLayout(n);
|
|
15
|
+
const index = layout.index;
|
|
16
|
+
const side = n - 1;
|
|
17
|
+
const h = side / 2;
|
|
18
|
+
const parts = [];
|
|
19
|
+
const quadrant = (i, j) => (i >= h ? 1 : 0) + (j >= h ? 2 : 0);
|
|
20
|
+
for (let j = 1; j < side - 1; j++) {
|
|
21
|
+
for (let i = 1; i < side - 1; i++) {
|
|
22
|
+
if (!(mask & 1 << quadrant(i, j))) continue;
|
|
23
|
+
const a = index[i + j * n];
|
|
24
|
+
const b = index[i + 1 + j * n];
|
|
25
|
+
const c = index[i + 1 + (j + 1) * n];
|
|
26
|
+
const d = index[i + (j + 1) * n];
|
|
27
|
+
parts.push(a, b, c, a, c, d);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const at = (e, s, depth) => {
|
|
31
|
+
const i = e === 0 ? s : e === 1 ? side - depth : e === 2 ? side - s : depth;
|
|
32
|
+
const j = e === 0 ? depth : e === 1 ? s : e === 2 ? side - depth : side - s;
|
|
33
|
+
return index[i + j * n];
|
|
34
|
+
};
|
|
35
|
+
const skirt = [];
|
|
36
|
+
const outer = [];
|
|
37
|
+
const inner = [];
|
|
38
|
+
for (let e = 0; e < 4; e++) {
|
|
39
|
+
const step = stitch & 1 << e ? 2 : 1;
|
|
40
|
+
for (let half = 0; half < 2; half++) {
|
|
41
|
+
const s0 = half * h;
|
|
42
|
+
const s1 = s0 + h;
|
|
43
|
+
const m = s0 + h / 2;
|
|
44
|
+
const q = e === 0 ? quadrant(m, 0) : e === 1 ? quadrant(side, m) : e === 2 ? quadrant(side - m, side) : quadrant(0, side - m);
|
|
45
|
+
if (!(mask & 1 << q)) continue;
|
|
46
|
+
outer.length = 0;
|
|
47
|
+
inner.length = 0;
|
|
48
|
+
for (let s = s0; s <= s1; s += step) outer.push(s);
|
|
49
|
+
for (let s = Math.max(1, s0); s <= Math.min(side - 1, s1); s++) inner.push(s);
|
|
50
|
+
let a = 0;
|
|
51
|
+
let b = 0;
|
|
52
|
+
while (a < outer.length - 1 || b < inner.length - 1) {
|
|
53
|
+
const advanceOuter = b === inner.length - 1 || a < outer.length - 1 && outer[a + 1] <= inner[b + 1];
|
|
54
|
+
if (advanceOuter) {
|
|
55
|
+
parts.push(at(e, outer[a], 0), at(e, outer[a + 1], 0), at(e, inner[b], 1));
|
|
56
|
+
a++;
|
|
57
|
+
} else {
|
|
58
|
+
parts.push(at(e, outer[a], 0), at(e, inner[b + 1], 1), at(e, inner[b], 1));
|
|
59
|
+
b++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!skirts) continue;
|
|
63
|
+
for (let k = 0; k + 1 < outer.length; k++) {
|
|
64
|
+
const va = at(e, outer[k], 0);
|
|
65
|
+
const vb = at(e, outer[k + 1], 0);
|
|
66
|
+
const sa = layout.surface + va;
|
|
67
|
+
const sb = layout.surface + vb;
|
|
68
|
+
skirt.push(va, sa, vb, vb, sa, sb, va, vb, sa, vb, sb, sa);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const v of skirt) parts.push(v);
|
|
73
|
+
out = Uint16Array.from(parts);
|
|
74
|
+
sets.set(key, out);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function checkResolution(n) {
|
|
78
|
+
const k = Math.log2(n - 1);
|
|
79
|
+
if (!Number.isInteger(k) || k < 2 || k > 7) {
|
|
80
|
+
throw new ShardError("terrain/bad-resolution", `Planet resolution ${n} isn't 2^n + 1`, {
|
|
81
|
+
hint: "Use 17, 33 (the default), 65, or 129 vertices per chunk edge."
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function chunkLayout(n) {
|
|
86
|
+
let layout = layouts.get(n);
|
|
87
|
+
if (layout) return layout;
|
|
88
|
+
checkResolution(n);
|
|
89
|
+
const ring = 4 * (n - 1);
|
|
90
|
+
const surface = n * n;
|
|
91
|
+
const index = new Uint16Array(surface);
|
|
92
|
+
const ringPoints = new Uint16Array(ring * 2);
|
|
93
|
+
let r = 0;
|
|
94
|
+
const put = (i, j) => {
|
|
95
|
+
index[i + j * n] = r;
|
|
96
|
+
ringPoints[r * 2] = i;
|
|
97
|
+
ringPoints[r * 2 + 1] = j;
|
|
98
|
+
r++;
|
|
99
|
+
};
|
|
100
|
+
for (let i = 0; i < n - 1; i++) put(i, 0);
|
|
101
|
+
for (let j = 0; j < n - 1; j++) put(n - 1, j);
|
|
102
|
+
for (let i = n - 1; i > 0; i--) put(i, n - 1);
|
|
103
|
+
for (let j = n - 1; j > 0; j--) put(0, j);
|
|
104
|
+
let v = ring;
|
|
105
|
+
for (let j = 1; j < n - 1; j++) for (let i = 1; i < n - 1; i++) index[i + j * n] = v++;
|
|
106
|
+
layout = {
|
|
107
|
+
resolution: n,
|
|
108
|
+
ring,
|
|
109
|
+
surface,
|
|
110
|
+
vertexCount: surface + ring,
|
|
111
|
+
index,
|
|
112
|
+
ringPoints,
|
|
113
|
+
indices: new Uint16Array(0),
|
|
114
|
+
surfaceIndices: new Uint16Array(0)
|
|
115
|
+
};
|
|
116
|
+
layouts.set(n, layout);
|
|
117
|
+
layout.indices = chunkIndices(n, 15, 0, true);
|
|
118
|
+
layout.surfaceIndices = chunkIndices(n, 15, 0, false);
|
|
119
|
+
return layout;
|
|
120
|
+
}
|
|
121
|
+
function lockCode(i, j, n, vi, ring) {
|
|
122
|
+
if (vi < ring) return Math.floor(vi / (n - 1));
|
|
123
|
+
const h = (n - 1) / 2;
|
|
124
|
+
if (i === h && j === h) return 8;
|
|
125
|
+
if (i === h) return j < h ? 4 : 5;
|
|
126
|
+
if (j === h) return i < h ? 6 : 7;
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
let values = new Float32Array(0);
|
|
130
|
+
let temps = new Float32Array(0);
|
|
131
|
+
let moist = new Float32Array(0);
|
|
132
|
+
let grid = new Float64Array(0);
|
|
133
|
+
const points = createChunkPoints();
|
|
134
|
+
const dir = new Float64Array(3);
|
|
135
|
+
function chunkCenter(face, depth, x, y, radius, shape, out) {
|
|
136
|
+
const ext = nodeExtent(depth);
|
|
137
|
+
faceToDirection(face, -1 + (x + 0.5) * ext, -1 + (y + 0.5) * ext, dir);
|
|
138
|
+
out[0] = dir[0] * radius * shape[0];
|
|
139
|
+
out[1] = dir[1] * radius * shape[1];
|
|
140
|
+
out[2] = dir[2] * radius * shape[2];
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function buildChunk(spec) {
|
|
144
|
+
const pts = prepareChunkPoints(
|
|
145
|
+
spec.face,
|
|
146
|
+
spec.depth,
|
|
147
|
+
spec.x,
|
|
148
|
+
spec.y,
|
|
149
|
+
spec.resolution,
|
|
150
|
+
spec.radius,
|
|
151
|
+
points,
|
|
152
|
+
spec.snap
|
|
153
|
+
);
|
|
154
|
+
const count = pts.count;
|
|
155
|
+
if (values.length < count) {
|
|
156
|
+
values = new Float32Array(count);
|
|
157
|
+
temps = new Float32Array(count);
|
|
158
|
+
moist = new Float32Array(count);
|
|
159
|
+
}
|
|
160
|
+
if (spec.height) sampleChunkPoints(spec.height, spec.seed, pts, values);
|
|
161
|
+
else values.fill(0, 0, count);
|
|
162
|
+
if (spec.climate) {
|
|
163
|
+
sampleChunkPoints(spec.climate, spec.seed, pts, temps, "temperature");
|
|
164
|
+
sampleChunkPoints(spec.climate, spec.seed, pts, moist, "moisture");
|
|
165
|
+
} else {
|
|
166
|
+
temps.fill(0, 0, count);
|
|
167
|
+
moist.fill(0, 0, count);
|
|
168
|
+
}
|
|
169
|
+
return assembleChunk(spec, pts, values, temps, moist);
|
|
170
|
+
}
|
|
171
|
+
function assembleChunk(spec, pts, values2, temps2, moist2) {
|
|
172
|
+
const n = spec.resolution;
|
|
173
|
+
const layout = chunkLayout(n);
|
|
174
|
+
const R = spec.radius;
|
|
175
|
+
const b = pts.side;
|
|
176
|
+
const count = pts.count;
|
|
177
|
+
if (grid.length < count * 3) grid = new Float64Array(count * 3);
|
|
178
|
+
const sx = spec.shape[0];
|
|
179
|
+
const sy = spec.shape[1];
|
|
180
|
+
const sz = spec.shape[2];
|
|
181
|
+
const center = chunkCenter(
|
|
182
|
+
spec.face,
|
|
183
|
+
spec.depth,
|
|
184
|
+
spec.x,
|
|
185
|
+
spec.y,
|
|
186
|
+
R,
|
|
187
|
+
spec.shape,
|
|
188
|
+
new Float64Array(3)
|
|
189
|
+
);
|
|
190
|
+
const cx = center[0];
|
|
191
|
+
const cy = center[1];
|
|
192
|
+
const cz = center[2];
|
|
193
|
+
const scale = spec.heightScale;
|
|
194
|
+
const offset = spec.heightOffset ?? 0;
|
|
195
|
+
const d = pts.dirs;
|
|
196
|
+
for (let k = 0; k < count; k++) {
|
|
197
|
+
const r = R + values2[k] * scale + offset;
|
|
198
|
+
grid[k * 3] = d[k * 3] * sx * r - cx;
|
|
199
|
+
grid[k * 3 + 1] = d[k * 3 + 1] * sy * r - cy;
|
|
200
|
+
grid[k * 3 + 2] = d[k * 3 + 2] * sz * r - cz;
|
|
201
|
+
}
|
|
202
|
+
const nv = layout.vertexCount;
|
|
203
|
+
const positions = new Float32Array(nv * 3);
|
|
204
|
+
const normals = new Float32Array(nv * 3);
|
|
205
|
+
const uvs = new Float32Array(nv * 2);
|
|
206
|
+
const uvs1 = new Float32Array(nv * 2);
|
|
207
|
+
const tangents = new Float32Array(nv * 4);
|
|
208
|
+
const heights = new Float32Array(n * n);
|
|
209
|
+
let minH = Infinity;
|
|
210
|
+
let maxH = -Infinity;
|
|
211
|
+
let error = 0;
|
|
212
|
+
const index = layout.index;
|
|
213
|
+
for (let j = 0; j < n; j++) {
|
|
214
|
+
for (let i = 0; i < n; i++) {
|
|
215
|
+
const k = i + 1 + (j + 1) * b;
|
|
216
|
+
const vi = index[i + j * n];
|
|
217
|
+
const p = k * 3;
|
|
218
|
+
positions[vi * 3] = grid[p];
|
|
219
|
+
positions[vi * 3 + 1] = grid[p + 1];
|
|
220
|
+
positions[vi * 3 + 2] = grid[p + 2];
|
|
221
|
+
const ax = grid[p + 3] - grid[p - 3];
|
|
222
|
+
const ay = grid[p + 4] - grid[p - 2];
|
|
223
|
+
const az = grid[p + 5] - grid[p - 1];
|
|
224
|
+
const up = p + b * 3;
|
|
225
|
+
const dn = p - b * 3;
|
|
226
|
+
const bx = grid[up] - grid[dn];
|
|
227
|
+
const by = grid[up + 1] - grid[dn + 1];
|
|
228
|
+
const bz = grid[up + 2] - grid[dn + 2];
|
|
229
|
+
let nx = ay * bz - az * by;
|
|
230
|
+
let ny = az * bx - ax * bz;
|
|
231
|
+
let nz = ax * by - ay * bx;
|
|
232
|
+
const len = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
233
|
+
nx /= len;
|
|
234
|
+
ny /= len;
|
|
235
|
+
nz /= len;
|
|
236
|
+
normals[vi * 3] = nx;
|
|
237
|
+
normals[vi * 3 + 1] = ny;
|
|
238
|
+
normals[vi * 3 + 2] = nz;
|
|
239
|
+
const h = values2[k] * scale + offset;
|
|
240
|
+
heights[i + j * n] = h;
|
|
241
|
+
if (h < minH) minH = h;
|
|
242
|
+
if (h > maxH) maxH = h;
|
|
243
|
+
uvs[vi * 2] = temps2[k];
|
|
244
|
+
uvs[vi * 2 + 1] = moist2[k];
|
|
245
|
+
uvs1[vi * 2] = i + j * n;
|
|
246
|
+
uvs1[vi * 2 + 1] = spec.morphError;
|
|
247
|
+
let dx = 0;
|
|
248
|
+
let dy = 0;
|
|
249
|
+
let dz = 0;
|
|
250
|
+
const oddI = (i & 1) === 1;
|
|
251
|
+
const oddJ = (j & 1) === 1;
|
|
252
|
+
if (oddI || oddJ) {
|
|
253
|
+
const ka = (oddI ? k - 1 : k) - (oddJ ? b : 0);
|
|
254
|
+
const kb = (oddI ? k + 1 : k) + (oddJ ? b : 0);
|
|
255
|
+
dx = (grid[ka * 3] + grid[kb * 3]) / 2 - grid[p];
|
|
256
|
+
dy = (grid[ka * 3 + 1] + grid[kb * 3 + 1]) / 2 - grid[p + 1];
|
|
257
|
+
dz = (grid[ka * 3 + 2] + grid[kb * 3 + 2]) / 2 - grid[p + 2];
|
|
258
|
+
const d2 = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
259
|
+
if (d2 > error) error = d2;
|
|
260
|
+
}
|
|
261
|
+
tangents[vi * 4] = dx;
|
|
262
|
+
tangents[vi * 4 + 1] = dy;
|
|
263
|
+
tangents[vi * 4 + 2] = dz;
|
|
264
|
+
tangents[vi * 4 + 3] = h;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const rp = layout.ringPoints;
|
|
268
|
+
const depth = spec.skirtDepth;
|
|
269
|
+
for (let s = 0; s < layout.ring; s++) {
|
|
270
|
+
const i = rp[s * 2];
|
|
271
|
+
const j = rp[s * 2 + 1];
|
|
272
|
+
const k = i + 1 + (j + 1) * b;
|
|
273
|
+
const px = d[k * 3] * sx;
|
|
274
|
+
const py = d[k * 3 + 1] * sy;
|
|
275
|
+
const pz = d[k * 3 + 2] * sz;
|
|
276
|
+
const dl = Math.sqrt(px * px + py * py + pz * pz) || 1;
|
|
277
|
+
const src = s;
|
|
278
|
+
const dst = layout.surface + s;
|
|
279
|
+
positions[dst * 3] = positions[src * 3] - px / dl * depth;
|
|
280
|
+
positions[dst * 3 + 1] = positions[src * 3 + 1] - py / dl * depth;
|
|
281
|
+
positions[dst * 3 + 2] = positions[src * 3 + 2] - pz / dl * depth;
|
|
282
|
+
normals.copyWithin(dst * 3, src * 3, src * 3 + 3);
|
|
283
|
+
uvs.copyWithin(dst * 2, src * 2, src * 2 + 2);
|
|
284
|
+
uvs1.copyWithin(dst * 2, src * 2, src * 2 + 2);
|
|
285
|
+
tangents.copyWithin(dst * 4, src * 4, src * 4 + 4);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
center,
|
|
289
|
+
data: { positions, normals, uvs, uvs1, tangents, indices: layout.indices },
|
|
290
|
+
heights,
|
|
291
|
+
minHeight: minH,
|
|
292
|
+
maxHeight: maxH,
|
|
293
|
+
error
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
assembleChunk,
|
|
298
|
+
buildChunk,
|
|
299
|
+
checkResolution,
|
|
300
|
+
chunkCenter,
|
|
301
|
+
chunkIndices,
|
|
302
|
+
chunkLayout,
|
|
303
|
+
lockCode
|
|
304
|
+
};
|