@plasius/gpu-world-generator 0.0.4
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 +203 -0
- package/README.md +73 -0
- package/dist/field.wgsl +225 -0
- package/dist/fractal-prepass.wgsl +290 -0
- package/dist/index.cjs +1942 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +447 -0
- package/dist/index.d.ts +447 -0
- package/dist/index.js +1848 -0
- package/dist/index.js.map +1 -0
- package/dist/terrain.wgsl +451 -0
- package/docs/adrs/adr-0001-package-scope.md +18 -0
- package/docs/adrs/adr-0002-world-tiling-lod-stitching.md +28 -0
- package/docs/adrs/adr-0003-terrain-generation-style-mixing.md +21 -0
- package/docs/adrs/index.md +5 -0
- package/docs/biomes.md +206 -0
- package/docs/lod-zoning.md +22 -0
- package/docs/plan.md +107 -0
- package/docs/procedural-surface.md +73 -0
- package/docs/resources.md +55 -0
- package/package.json +53 -0
- package/src/biomes/temperate.ts +387 -0
- package/src/field.wgsl +225 -0
- package/src/fields.ts +321 -0
- package/src/fractal-prepass.ts +237 -0
- package/src/fractal-prepass.wgsl +290 -0
- package/src/generator.ts +106 -0
- package/src/hex.ts +54 -0
- package/src/index.ts +11 -0
- package/src/mesh.ts +285 -0
- package/src/perf-monitor.ts +133 -0
- package/src/shaders/demo-terrain.wgsl +193 -0
- package/src/shaders/demo-trees.wgsl +172 -0
- package/src/shaders/demo-water-sim.wgsl +361 -0
- package/src/shaders/library/common.wgsl +38 -0
- package/src/shaders/library/materials.wgsl +175 -0
- package/src/terrain.wgsl +451 -0
- package/src/tile-cache.ts +274 -0
- package/src/tiles.ts +417 -0
- package/src/types.ts +220 -0
- package/src/wgsl/field.job.wgsl +225 -0
- package/src/wgsl/terrain.job.wgsl +451 -0
- package/src/wgsl.ts +77 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
struct HexCell {
|
|
2
|
+
q: i32,
|
|
3
|
+
r: i32,
|
|
4
|
+
level: i32,
|
|
5
|
+
flags: i32,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
struct TerrainCell {
|
|
9
|
+
height: f32,
|
|
10
|
+
heat: f32,
|
|
11
|
+
moisture: f32,
|
|
12
|
+
biome: u32,
|
|
13
|
+
surface: u32,
|
|
14
|
+
feature: u32,
|
|
15
|
+
foliage: f32,
|
|
16
|
+
slope_band: u32,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
struct TerrainParams {
|
|
20
|
+
seed: u32,
|
|
21
|
+
cell_count: u32,
|
|
22
|
+
heat_bias: f32,
|
|
23
|
+
height_scale: f32,
|
|
24
|
+
macro_scale: f32,
|
|
25
|
+
macro_warp_strength: f32,
|
|
26
|
+
style_mix_strength: f32,
|
|
27
|
+
terrace_steps: f32,
|
|
28
|
+
terrace_strength: f32,
|
|
29
|
+
crater_strength: f32,
|
|
30
|
+
crater_scale: f32,
|
|
31
|
+
height_min: f32,
|
|
32
|
+
height_max: f32,
|
|
33
|
+
slope_low: f32,
|
|
34
|
+
slope_high: f32,
|
|
35
|
+
pad0: f32,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@group(1) @binding(0) var<storage, read> cells: array<HexCell>;
|
|
39
|
+
@group(1) @binding(1) var<storage, read_write> terrain: array<TerrainCell>;
|
|
40
|
+
@group(1) @binding(2) var<uniform> params: TerrainParams;
|
|
41
|
+
|
|
42
|
+
const BIOME_PLAINS: u32 = 0u;
|
|
43
|
+
const BIOME_TUNDRA: u32 = 1u;
|
|
44
|
+
const BIOME_SAVANNA: u32 = 2u;
|
|
45
|
+
const BIOME_RIVER: u32 = 3u;
|
|
46
|
+
const BIOME_CITY: u32 = 4u;
|
|
47
|
+
const BIOME_VILLAGE: u32 = 5u;
|
|
48
|
+
const BIOME_ICE: u32 = 6u;
|
|
49
|
+
const BIOME_MOUNTAINOUS: u32 = 8u;
|
|
50
|
+
const BIOME_VOLCANIC: u32 = 9u;
|
|
51
|
+
const BIOME_ROAD: u32 = 10u;
|
|
52
|
+
const BIOME_TOWN: u32 = 11u;
|
|
53
|
+
const BIOME_CASTLE: u32 = 12u;
|
|
54
|
+
const BIOME_MIXED_FOREST: u32 = 13u;
|
|
55
|
+
|
|
56
|
+
const SURFACE_GRASS: u32 = 0u;
|
|
57
|
+
const SURFACE_DIRT: u32 = 1u;
|
|
58
|
+
const SURFACE_SAND: u32 = 2u;
|
|
59
|
+
const SURFACE_ROCK: u32 = 3u;
|
|
60
|
+
const SURFACE_GRAVEL: u32 = 4u;
|
|
61
|
+
const SURFACE_ICE: u32 = 6u;
|
|
62
|
+
const SURFACE_MUD: u32 = 7u;
|
|
63
|
+
const SURFACE_WATER: u32 = 11u;
|
|
64
|
+
const SURFACE_BASALT: u32 = 12u;
|
|
65
|
+
|
|
66
|
+
const FEATURE_TREE: u32 = 0u;
|
|
67
|
+
const FEATURE_BUSH: u32 = 1u;
|
|
68
|
+
const FEATURE_GRASS_TUFT: u32 = 2u;
|
|
69
|
+
const FEATURE_REED: u32 = 3u;
|
|
70
|
+
const FEATURE_ROCK: u32 = 4u;
|
|
71
|
+
const FEATURE_BOULDER: u32 = 5u;
|
|
72
|
+
const FEATURE_WATER_RIPPLE: u32 = 6u;
|
|
73
|
+
const FEATURE_ICE_SPIKE: u32 = 7u;
|
|
74
|
+
const FEATURE_RUIN: u32 = 13u;
|
|
75
|
+
const FEATURE_NONE: u32 = 4294967295u;
|
|
76
|
+
|
|
77
|
+
const SLOPE_DOWNWARD: u32 = 0u;
|
|
78
|
+
const SLOPE_FLAT: u32 = 1u;
|
|
79
|
+
const SLOPE_UPWARD: u32 = 2u;
|
|
80
|
+
|
|
81
|
+
fn hash32(x: u32) -> u32 {
|
|
82
|
+
var v = x;
|
|
83
|
+
v ^= v >> 17u;
|
|
84
|
+
v *= 0xed5ad4bbu;
|
|
85
|
+
v ^= v >> 11u;
|
|
86
|
+
v *= 0xac4c1b51u;
|
|
87
|
+
v ^= v >> 15u;
|
|
88
|
+
v *= 0x31848babu;
|
|
89
|
+
v ^= v >> 14u;
|
|
90
|
+
return v;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn hash01(x: u32) -> f32 {
|
|
94
|
+
let v = hash32(x) & 0x00ffffffu;
|
|
95
|
+
return f32(v) / 16777216.0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn smooth_mandelbrot(cx: f32, cy: f32, iterations: u32, power: f32) -> f32 {
|
|
99
|
+
var zx = 0.0;
|
|
100
|
+
var zy = 0.0;
|
|
101
|
+
var i: u32 = 0u;
|
|
102
|
+
loop {
|
|
103
|
+
if (i >= iterations) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
let r2 = zx * zx + zy * zy;
|
|
107
|
+
if (r2 > 4.0) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
let r = sqrt(r2);
|
|
111
|
+
let theta = atan2(zy, zx);
|
|
112
|
+
let rp = pow(r, power);
|
|
113
|
+
zx = rp * cos(theta * power) + cx;
|
|
114
|
+
zy = rp * sin(theta * power) + cy;
|
|
115
|
+
i = i + 1u;
|
|
116
|
+
}
|
|
117
|
+
if (i >= iterations) {
|
|
118
|
+
return 1.0;
|
|
119
|
+
}
|
|
120
|
+
let r = max(sqrt(zx * zx + zy * zy), 1e-6);
|
|
121
|
+
let nu = log2(log(r));
|
|
122
|
+
let smoothVal = (f32(i) + 1.0 - nu) / f32(iterations);
|
|
123
|
+
return clamp(smoothVal, 0.0, 1.0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn terrace_height(h: f32, steps: f32) -> f32 {
|
|
127
|
+
let count = max(1.0, steps);
|
|
128
|
+
let step = 1.0 / count;
|
|
129
|
+
let clamped = clamp(h, 0.0, 1.0);
|
|
130
|
+
let band = floor(clamped / step);
|
|
131
|
+
let t = fract(clamped / step);
|
|
132
|
+
let blend = t * t * (3.0 - 2.0 * t);
|
|
133
|
+
return (band + blend) * step;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn crater_field(p: vec2<f32>, scale: f32, seed: u32) -> f32 {
|
|
137
|
+
let sp = p * scale;
|
|
138
|
+
let cell = floor(sp);
|
|
139
|
+
let local = sp - cell;
|
|
140
|
+
let cx = i32(cell.x);
|
|
141
|
+
let cz = i32(cell.y);
|
|
142
|
+
let hx = u32(bitcast<u32>(cx));
|
|
143
|
+
let hz = u32(bitcast<u32>(cz));
|
|
144
|
+
let h0 = hash01((hx * 374761393u) ^ (hz * 668265263u) ^ seed ^ 0x9e3779b9u);
|
|
145
|
+
let h1 = hash01((hx * 2246822519u) ^ (hz * 3266489917u) ^ seed ^ 0x85ebca6bu);
|
|
146
|
+
let h2 = hash01((hx * 1597334677u) ^ (hz * 3812015801u) ^ seed ^ 0xc2b2ae35u);
|
|
147
|
+
let center = vec2<f32>(h0, h1);
|
|
148
|
+
let radius = 0.22 + 0.25 * h2;
|
|
149
|
+
let dist = distance(local, center);
|
|
150
|
+
return smoothstep(radius, radius * 0.35, dist);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn macro_map(p: vec2<f32>, iterations: u32, power: f32) -> f32 {
|
|
154
|
+
let macro_iter = max(10u, iterations / 3u);
|
|
155
|
+
let macroA = smooth_mandelbrot(p.x * params.macro_scale, p.y * params.macro_scale, macro_iter, power);
|
|
156
|
+
let macroB = smooth_mandelbrot(
|
|
157
|
+
(p.x + 13.7) * params.macro_scale,
|
|
158
|
+
(p.y - 9.2) * params.macro_scale,
|
|
159
|
+
macro_iter,
|
|
160
|
+
power + 0.35
|
|
161
|
+
);
|
|
162
|
+
let warp = vec2<f32>(macroA - 0.5, macroB - 0.5) * params.macro_warp_strength;
|
|
163
|
+
let macroC = smooth_mandelbrot(
|
|
164
|
+
(p.x + warp.x) * params.macro_scale,
|
|
165
|
+
(p.y + warp.y) * params.macro_scale,
|
|
166
|
+
macro_iter,
|
|
167
|
+
power
|
|
168
|
+
);
|
|
169
|
+
return clamp((macroC - 0.5) * params.style_mix_strength + 0.5, 0.0, 1.0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn hash_cell(cell: HexCell, seed: u32, salt: u32) -> f32 {
|
|
173
|
+
let q = u32(bitcast<u32>(cell.q));
|
|
174
|
+
let r = u32(bitcast<u32>(cell.r));
|
|
175
|
+
let lvl = u32(bitcast<u32>(cell.level));
|
|
176
|
+
let mixed = q * 1664525u ^ r * 1013904223u ^ lvl * 747796405u ^ seed ^ salt;
|
|
177
|
+
return hash01(mixed);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn classify_slope(cumulative_height: f32, low: f32, high: f32) -> u32 {
|
|
181
|
+
if (cumulative_height < low) {
|
|
182
|
+
return SLOPE_DOWNWARD;
|
|
183
|
+
}
|
|
184
|
+
if (cumulative_height >= high) {
|
|
185
|
+
return SLOPE_UPWARD;
|
|
186
|
+
}
|
|
187
|
+
return SLOPE_FLAT;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn classify_biome(
|
|
191
|
+
heat: f32,
|
|
192
|
+
moisture: f32,
|
|
193
|
+
surface: u32,
|
|
194
|
+
slope_band: u32,
|
|
195
|
+
obstacle: f32,
|
|
196
|
+
foliage: f32
|
|
197
|
+
) -> u32 {
|
|
198
|
+
if (surface == SURFACE_WATER) {
|
|
199
|
+
if (heat < 0.25) {
|
|
200
|
+
return BIOME_ICE;
|
|
201
|
+
}
|
|
202
|
+
return BIOME_RIVER;
|
|
203
|
+
}
|
|
204
|
+
if (surface == SURFACE_ICE) {
|
|
205
|
+
return BIOME_ICE;
|
|
206
|
+
}
|
|
207
|
+
if (slope_band == SLOPE_UPWARD && obstacle > 0.68) {
|
|
208
|
+
if (heat > 0.72) {
|
|
209
|
+
return BIOME_VOLCANIC;
|
|
210
|
+
}
|
|
211
|
+
return BIOME_MOUNTAINOUS;
|
|
212
|
+
}
|
|
213
|
+
if (heat < 0.3) {
|
|
214
|
+
return BIOME_TUNDRA;
|
|
215
|
+
}
|
|
216
|
+
if (heat > 0.74 && moisture < 0.33) {
|
|
217
|
+
return BIOME_SAVANNA;
|
|
218
|
+
}
|
|
219
|
+
if (foliage > 0.55 && moisture > 0.45) {
|
|
220
|
+
return BIOME_MIXED_FOREST;
|
|
221
|
+
}
|
|
222
|
+
return BIOME_PLAINS;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn maybe_settlement(current: u32, height: f32, cell: HexCell, seed: u32, obstacle: f32) -> u32 {
|
|
226
|
+
let settlement = hash_cell(cell, seed, 0x9e3779b9u);
|
|
227
|
+
let level = cell.level;
|
|
228
|
+
if (obstacle > 0.8) {
|
|
229
|
+
return current;
|
|
230
|
+
}
|
|
231
|
+
if (height > 0.6 && settlement > 0.997) {
|
|
232
|
+
return BIOME_CASTLE;
|
|
233
|
+
}
|
|
234
|
+
if (level <= 0 && settlement > 0.994) {
|
|
235
|
+
return BIOME_CITY;
|
|
236
|
+
}
|
|
237
|
+
if (level <= 1 && settlement > 0.992) {
|
|
238
|
+
return BIOME_TOWN;
|
|
239
|
+
}
|
|
240
|
+
if (level <= 2 && settlement > 0.988) {
|
|
241
|
+
return BIOME_VILLAGE;
|
|
242
|
+
}
|
|
243
|
+
let roadNoise = hash_cell(cell, seed, 0x6c8e9cf5u);
|
|
244
|
+
if (level >= 2 && roadNoise > 0.996) {
|
|
245
|
+
return BIOME_ROAD;
|
|
246
|
+
}
|
|
247
|
+
return current;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
|
|
251
|
+
if (job_index >= params.cell_count) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let cell = cells[job_index];
|
|
256
|
+
let p = vec2<f32>(f32(cell.q), f32(cell.r));
|
|
257
|
+
let seed = params.seed;
|
|
258
|
+
|
|
259
|
+
let scale = 0.14;
|
|
260
|
+
let warp_scale = 0.5;
|
|
261
|
+
let warp_strength = 0.75;
|
|
262
|
+
let power = 2.2;
|
|
263
|
+
let detail_scale = 3.2;
|
|
264
|
+
let detail_power = 2.0;
|
|
265
|
+
let iterations = 24u;
|
|
266
|
+
let detail_iterations = 14u;
|
|
267
|
+
|
|
268
|
+
let offX = hash01(seed ^ 0x7f4a7c15u) * 4.0 - 2.0;
|
|
269
|
+
let offZ = hash01(seed ^ 0xa9d84d2bu) * 4.0 - 2.0;
|
|
270
|
+
let warpOffX = hash01(seed ^ 0x8c2f1d3bu) * 6.0 - 3.0;
|
|
271
|
+
let warpOffZ = hash01(seed ^ 0x5d2c79e9u) * 6.0 - 3.0;
|
|
272
|
+
|
|
273
|
+
let warpIter = max(12u, iterations * 6u / 10u);
|
|
274
|
+
let warpA = smooth_mandelbrot(
|
|
275
|
+
(p.x + warpOffX) * warp_scale,
|
|
276
|
+
(p.y + warpOffZ) * warp_scale,
|
|
277
|
+
warpIter,
|
|
278
|
+
power
|
|
279
|
+
);
|
|
280
|
+
let warpB = smooth_mandelbrot(
|
|
281
|
+
(p.x - warpOffZ) * warp_scale,
|
|
282
|
+
(p.y + warpOffX) * warp_scale,
|
|
283
|
+
warpIter,
|
|
284
|
+
power
|
|
285
|
+
);
|
|
286
|
+
let warped = vec2<f32>(
|
|
287
|
+
p.x + (warpA - 0.5) * warp_strength,
|
|
288
|
+
p.y + (warpB - 0.5) * warp_strength
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
let base = smooth_mandelbrot(
|
|
292
|
+
warped.x * scale + offX,
|
|
293
|
+
warped.y * scale + offZ,
|
|
294
|
+
iterations,
|
|
295
|
+
power
|
|
296
|
+
);
|
|
297
|
+
let mid = smooth_mandelbrot(
|
|
298
|
+
warped.x * scale * 2.15 + offX * 0.6,
|
|
299
|
+
warped.y * scale * 2.15 + offZ * 0.6,
|
|
300
|
+
max(14u, iterations * 7u / 10u),
|
|
301
|
+
power + 0.2
|
|
302
|
+
);
|
|
303
|
+
let detail = smooth_mandelbrot(
|
|
304
|
+
warped.x * scale * detail_scale + offX * 1.4,
|
|
305
|
+
warped.y * scale * detail_scale + offZ * 1.4,
|
|
306
|
+
detail_iterations,
|
|
307
|
+
detail_power
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
let ridge = 1.0 - abs(2.0 * mid - 1.0);
|
|
311
|
+
let baseHeight = pow(base, 0.9) * pow(mid, 1.05) * pow(detail, 1.1);
|
|
312
|
+
let styleMask = macro_map(warped, iterations, power);
|
|
313
|
+
let terrace = terrace_height(baseHeight, params.terrace_steps);
|
|
314
|
+
let crater = crater_field(warped, params.crater_scale, seed);
|
|
315
|
+
|
|
316
|
+
let styleA = clamp(pow(baseHeight, 0.8) + pow(ridge, 1.4) * 0.2, 0.0, 1.0);
|
|
317
|
+
let styleB = clamp(
|
|
318
|
+
baseHeight * (1.0 - params.terrace_strength) +
|
|
319
|
+
terrace * params.terrace_strength -
|
|
320
|
+
crater * params.crater_strength +
|
|
321
|
+
pow(ridge, 1.6) * 0.12,
|
|
322
|
+
0.0,
|
|
323
|
+
1.0
|
|
324
|
+
);
|
|
325
|
+
let mixed = mix(styleA, styleB, styleMask);
|
|
326
|
+
let cumulative_height = clamp(
|
|
327
|
+
base * 0.38 + mid * 0.33 + detail * 0.21 + styleMask * 0.08,
|
|
328
|
+
0.0,
|
|
329
|
+
1.0
|
|
330
|
+
);
|
|
331
|
+
let slopeLow = clamp(params.slope_low, 0.01, 0.49);
|
|
332
|
+
let slopeHigh = clamp(max(slopeLow + 0.01, params.slope_high), 0.5, 0.99);
|
|
333
|
+
let slopeBand = classify_slope(cumulative_height, slopeLow, slopeHigh);
|
|
334
|
+
let downwardStrength = 1.0 - smoothstep(0.0, slopeLow, cumulative_height);
|
|
335
|
+
let upwardStrength = smoothstep(slopeHigh, 1.0, cumulative_height);
|
|
336
|
+
let flatStrength = clamp(1.0 - max(downwardStrength, upwardStrength), 0.0, 1.0);
|
|
337
|
+
|
|
338
|
+
let ridgeBoost = pow(ridge, 1.35) * 0.22;
|
|
339
|
+
let centered = (mixed - 0.5) * 2.0;
|
|
340
|
+
let shaped = sign(centered) * pow(abs(centered), 0.75);
|
|
341
|
+
let macroOffset = (styleMask - 0.5) * 0.25;
|
|
342
|
+
let rawHeight = clamp(
|
|
343
|
+
0.5 +
|
|
344
|
+
shaped * 0.8 +
|
|
345
|
+
macroOffset +
|
|
346
|
+
ridgeBoost +
|
|
347
|
+
upwardStrength * 0.22 -
|
|
348
|
+
downwardStrength * 0.22,
|
|
349
|
+
params.height_min,
|
|
350
|
+
params.height_max
|
|
351
|
+
);
|
|
352
|
+
let height01 = clamp(rawHeight, 0.0, 1.0);
|
|
353
|
+
|
|
354
|
+
let roughness = clamp(pow(ridge, 1.25) * 0.7 + detail * 0.3, 0.0, 1.0);
|
|
355
|
+
let heat = clamp(0.55 * mid + 0.35 * (1.0 - height01) + params.heat_bias, 0.0, 1.0);
|
|
356
|
+
let moisture = clamp(
|
|
357
|
+
0.55 * detail + 0.35 * (1.0 - height01) - (heat - 0.5) * 0.1,
|
|
358
|
+
0.0,
|
|
359
|
+
1.0
|
|
360
|
+
);
|
|
361
|
+
let rockiness = clamp(roughness * 0.6 + height01 * 0.4, 0.0, 1.0);
|
|
362
|
+
let water = clamp((0.32 - height01) * 3.0 + (moisture - 0.5) * 0.2, 0.0, 1.0);
|
|
363
|
+
|
|
364
|
+
let feature_mask = smooth_mandelbrot(
|
|
365
|
+
warped.x * scale * (detail_scale + 1.25) - offX * 0.85,
|
|
366
|
+
warped.y * scale * (detail_scale + 1.25) - offZ * 0.85,
|
|
367
|
+
max(12u, detail_iterations),
|
|
368
|
+
detail_power + 0.15
|
|
369
|
+
);
|
|
370
|
+
let obstacle = clamp(
|
|
371
|
+
feature_mask * 0.58 +
|
|
372
|
+
roughness * 0.25 +
|
|
373
|
+
upwardStrength * 0.25 -
|
|
374
|
+
moisture * 0.16 -
|
|
375
|
+
water * 0.2,
|
|
376
|
+
0.0,
|
|
377
|
+
1.0
|
|
378
|
+
);
|
|
379
|
+
let foliage_field = smooth_mandelbrot(
|
|
380
|
+
warped.x * scale * (detail_scale * 1.85 + 0.35) + offX * 1.9,
|
|
381
|
+
warped.y * scale * (detail_scale * 1.85 + 0.35) + offZ * 1.9,
|
|
382
|
+
max(16u, detail_iterations + 2u),
|
|
383
|
+
max(1.6, detail_power - 0.2)
|
|
384
|
+
);
|
|
385
|
+
let foliage = clamp(
|
|
386
|
+
foliage_field * moisture * (1.0 - water) * (0.35 + flatStrength * 0.65) * (1.0 - obstacle * 0.82),
|
|
387
|
+
0.0,
|
|
388
|
+
1.0
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
var surface = SURFACE_DIRT;
|
|
392
|
+
if (water > 0.58 || cumulative_height < 0.14) {
|
|
393
|
+
surface = SURFACE_WATER;
|
|
394
|
+
} else if (heat < 0.2 && water > 0.42) {
|
|
395
|
+
surface = SURFACE_ICE;
|
|
396
|
+
} else if (slopeBand == SLOPE_DOWNWARD && moisture > 0.52) {
|
|
397
|
+
surface = SURFACE_MUD;
|
|
398
|
+
} else if (obstacle > 0.72 || slopeBand == SLOPE_UPWARD) {
|
|
399
|
+
surface = SURFACE_ROCK;
|
|
400
|
+
if (heat > 0.72) {
|
|
401
|
+
surface = SURFACE_BASALT;
|
|
402
|
+
}
|
|
403
|
+
} else if (heat > 0.74 && moisture < 0.32) {
|
|
404
|
+
surface = SURFACE_SAND;
|
|
405
|
+
} else if (foliage > 0.45 && moisture > 0.42) {
|
|
406
|
+
surface = SURFACE_GRASS;
|
|
407
|
+
} else if (moisture > 0.6) {
|
|
408
|
+
surface = SURFACE_DIRT;
|
|
409
|
+
} else if (obstacle > 0.5) {
|
|
410
|
+
surface = SURFACE_GRAVEL;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let featureNoise = hash_cell(cell, seed, 0x77abu);
|
|
414
|
+
var feature = FEATURE_NONE;
|
|
415
|
+
if (surface == SURFACE_WATER) {
|
|
416
|
+
feature = FEATURE_WATER_RIPPLE;
|
|
417
|
+
} else if (surface == SURFACE_ICE && featureNoise > 0.45) {
|
|
418
|
+
feature = FEATURE_ICE_SPIKE;
|
|
419
|
+
} else if (
|
|
420
|
+
surface == SURFACE_ROCK ||
|
|
421
|
+
surface == SURFACE_GRAVEL ||
|
|
422
|
+
surface == SURFACE_BASALT
|
|
423
|
+
) {
|
|
424
|
+
feature = FEATURE_ROCK;
|
|
425
|
+
if (obstacle > 0.85) {
|
|
426
|
+
feature = FEATURE_BOULDER;
|
|
427
|
+
}
|
|
428
|
+
} else if (surface == SURFACE_MUD && featureNoise > 0.75) {
|
|
429
|
+
feature = FEATURE_REED;
|
|
430
|
+
} else if (foliage > 0.72 && moisture > 0.55 && featureNoise > 0.35) {
|
|
431
|
+
feature = FEATURE_TREE;
|
|
432
|
+
} else if (foliage > 0.5 && featureNoise > 0.28) {
|
|
433
|
+
feature = FEATURE_BUSH;
|
|
434
|
+
} else if (surface == SURFACE_GRASS && foliage > 0.3 && featureNoise > 0.15) {
|
|
435
|
+
feature = FEATURE_GRASS_TUFT;
|
|
436
|
+
} else if (slopeBand == SLOPE_UPWARD && obstacle > 0.6 && featureNoise > 0.94) {
|
|
437
|
+
feature = FEATURE_RUIN;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
var biome = classify_biome(heat, moisture, surface, slopeBand, obstacle, foliage);
|
|
441
|
+
biome = maybe_settlement(biome, height01, cell, params.seed, obstacle);
|
|
442
|
+
|
|
443
|
+
terrain[job_index].height = rawHeight * params.height_scale;
|
|
444
|
+
terrain[job_index].heat = heat;
|
|
445
|
+
terrain[job_index].moisture = moisture;
|
|
446
|
+
terrain[job_index].biome = biome;
|
|
447
|
+
terrain[job_index].surface = surface;
|
|
448
|
+
terrain[job_index].feature = feature;
|
|
449
|
+
terrain[job_index].foliage = foliage;
|
|
450
|
+
terrain[job_index].slope_band = slopeBand;
|
|
451
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# ADR-0001: GPU World Generator Package Scope
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted
|
|
4
|
+
- **Date**: 2026-02-02
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
We need a dedicated package to pre-generate hierarchical hex-grid terrain on the GPU. The generator must support multi-level zones (1000 km² down to ~10 m²) and emit height/heat/moisture + biome classifications for runtime streaming. It should integrate cleanly with `@plasius/gpu-worker` to run WGSL jobs.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
Create `@plasius/gpu-world-generator` as a standalone package that ships:
|
|
11
|
+
- A WGSL job (`terrain.wgsl`) compatible with `@plasius/gpu-worker`.
|
|
12
|
+
- TypeScript utilities for hex grid generation and buffer packing.
|
|
13
|
+
- Biome id mappings and plan documentation.
|
|
14
|
+
|
|
15
|
+
## Consequences
|
|
16
|
+
- We can iterate on terrain synthesis independently of rendering and HUD work.
|
|
17
|
+
- Future shader jobs (rivers, roads, foliage) can live in the same package.
|
|
18
|
+
- Consumers should load WGSL via `?raw` for bundlers or `loadTerrainWgsl` for direct fetch.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# ADR-0002: Tiled World Generation, LOD, and Stitching
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted
|
|
4
|
+
- **Date**: 2026-02-03
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
We need a deterministic way to generate, store, and render large worlds by splitting them into parts that can be streamed and stitched seamlessly. The system must support multi-LOD rendering, keep seams invisible across tiles and LOD boundaries, and enable consistent material/shader assignment per terrain type. We also want the pipeline to work with GPU compute prepasses, allow on-demand generation for an effectively infinite world, and emit baked assets that can be reused or distributed.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
Adopt a tile-based world pipeline with deterministic sampling and explicit LOD stitching rules:
|
|
11
|
+
- **Partitioning**: The world is divided into tiles keyed by `(tx, tz, level, tileSizeWorld)`, each with a world size and grid resolution derived from the LOD level. `tileSizeWorld` is optional and enables sub-division for complex areas (e.g., cities) without changing the global world rules.
|
|
12
|
+
- **Shared borders**: Each tile generates a `(gridSize + 1)` vertex lattice so adjacent tiles share identical border samples. A 1–2 cell halo is sampled for normals/filters and discarded after generation.
|
|
13
|
+
- **Generation pipeline**: GPU compute prepass produces base height + field maps (heat, moisture, rockiness, water). Material masks and feature placement are derived from those fields. Tiles are generated on demand and can be baked to reusable assets as part of the same pipeline.
|
|
14
|
+
- **LOD generation**: Lower LODs are created via deterministic downsampling aligned to parent tiles, then re-classified for materials/features to avoid drift.
|
|
15
|
+
- **Stitching**:
|
|
16
|
+
- Same-LOD seams are avoided by shared border samples.
|
|
17
|
+
- Cross-LOD seams use skirts initially, with optional edge-stitch indices and geomorphing for higher fidelity.
|
|
18
|
+
- **Shader assignment**: Tiles store material ids or blend weights; shaders are selected by material classification, not per-tile shader variants.
|
|
19
|
+
- **Storage**: Tile assets cache heightfields, material masks, features, and LOD mesh buffers for streaming and reuse. Assets are produced from on-demand generation and can be serialized for reuse across sessions or sharing.
|
|
20
|
+
|
|
21
|
+
## Consequences
|
|
22
|
+
- Deterministic sampling enables seamless stitching but requires strict coordinate conventions and halo handling.
|
|
23
|
+
- LOD generation becomes a first-class asset pipeline with extra memory and bake time.
|
|
24
|
+
- Stitching techniques (skirts/edge-stitch/geomorph) must be maintained alongside mesh builders.
|
|
25
|
+
- Tile identity must include `tileSizeWorld` (or equivalent) to avoid mismatched stitching when sub-division is used.
|
|
26
|
+
- Infinite-world streaming requires cache eviction and background bake queues to avoid memory growth.
|
|
27
|
+
- Material assignment is centralized and data-driven, improving consistency but requiring more per-vertex metadata.
|
|
28
|
+
- Supports both offline baking and on-demand generation while keeping the runtime renderer simple.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ADR-0003: Terrain Generation Style Mixing (Shader-Based)
|
|
2
|
+
|
|
3
|
+
- **Status**: Accepted
|
|
4
|
+
- **Date**: 2026-02-04
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
The current terrain outputs are too flat and homogeneous. We need a more dramatic and unusual surface with stronger height variation, terraces, cratered basins, and sharper ridges. The height variation should be produced by the shader generators themselves (WGSL) so that prepass and LOD sampling stay deterministic and consistent.
|
|
8
|
+
|
|
9
|
+
## Decision
|
|
10
|
+
Adopt a shader-based, mixed-style height pipeline with signed heights:
|
|
11
|
+
- **Macro style map**: A low-frequency fractal map selects between two terrain styles per region.
|
|
12
|
+
- **Style A (earth-like dramatic)**: ridged multifractal with broad ranges and smoother slopes.
|
|
13
|
+
- **Style B (surreal)**: terracing + crater basins with sharper ridge boosts.
|
|
14
|
+
- **Signed heights**: heights are allowed to go below 0 and above 1 internally; a clamped `height01` is used for biome/heat/moisture/water.
|
|
15
|
+
- **Shader-first**: the WGSL field generators and fractal prepass compute the final height variation, not CPU post-processing.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
- Terrain outputs are more varied and less uniform, with large-scale regions that differ in style.
|
|
19
|
+
- Terrain height values can be negative or greater than 1; consumers must clamp if needed.
|
|
20
|
+
- Terrain params expand to include macro/style/terrace/crater controls.
|
|
21
|
+
- CPU fallbacks must mirror the shader logic to stay consistent.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Architecture Decision Records
|
|
2
|
+
|
|
3
|
+
- [ADR-0001: GPU World Generator Package Scope](./adr-0001-package-scope.md)
|
|
4
|
+
- [ADR-0002: Tiled World Generation, LOD, and Stitching](./adr-0002-world-tiling-lod-stitching.md)
|
|
5
|
+
- [ADR-0003: Terrain Generation Style Mixing (Shader-Based)](./adr-0003-terrain-generation-style-mixing.md)
|