@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,387 @@
|
|
|
1
|
+
import { axialToWorld, buildHexLevels, generateHexGrid } from "../hex";
|
|
2
|
+
import { defaultFieldParams, sampleFieldStack, type FieldSample } from "../fields";
|
|
3
|
+
import {
|
|
4
|
+
MacroBiome,
|
|
5
|
+
MicroFeature,
|
|
6
|
+
SlopeBand,
|
|
7
|
+
SurfaceCover,
|
|
8
|
+
TerrainBiome,
|
|
9
|
+
type HexCell,
|
|
10
|
+
type HexLevelSpec,
|
|
11
|
+
type TerrainCell,
|
|
12
|
+
type MacroBiomeId,
|
|
13
|
+
type MicroFeatureId,
|
|
14
|
+
type TerrainBiomeId,
|
|
15
|
+
type SurfaceCoverId,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
export interface MixedForestOptions {
|
|
19
|
+
seed?: number;
|
|
20
|
+
terrainSeed?: number;
|
|
21
|
+
featureSeed?: number;
|
|
22
|
+
foliageSeed?: number;
|
|
23
|
+
radius?: number;
|
|
24
|
+
topAreaKm2?: number;
|
|
25
|
+
minAreaM2?: number;
|
|
26
|
+
levels?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MixedForestLayer {
|
|
30
|
+
levelSpec: HexLevelSpec;
|
|
31
|
+
cells: HexCell[];
|
|
32
|
+
terrain: TerrainCell[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hash32(x: number): number {
|
|
36
|
+
let v = x >>> 0;
|
|
37
|
+
v ^= v >>> 17;
|
|
38
|
+
v = Math.imul(v, 0xed5ad4bb);
|
|
39
|
+
v ^= v >>> 11;
|
|
40
|
+
v = Math.imul(v, 0xac4c1b51);
|
|
41
|
+
v ^= v >>> 15;
|
|
42
|
+
v = Math.imul(v, 0x31848bab);
|
|
43
|
+
v ^= v >>> 14;
|
|
44
|
+
return v >>> 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hash01(x: number): number {
|
|
48
|
+
return (hash32(x) & 0x00ffffff) / 16777216;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hashCell(cell: HexCell, seed: number, salt: number): number {
|
|
52
|
+
const q = cell.q | 0;
|
|
53
|
+
const r = cell.r | 0;
|
|
54
|
+
const level = cell.level | 0;
|
|
55
|
+
const mixed =
|
|
56
|
+
(Math.imul(q, 1664525) ^ Math.imul(r, 1013904223) ^ Math.imul(level, 747796405) ^ seed ^ salt) >>>
|
|
57
|
+
0;
|
|
58
|
+
return hash01(mixed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function clamp01(value: number): number {
|
|
62
|
+
return Math.min(1, Math.max(0, value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createBiomeFieldParams(seed: number) {
|
|
66
|
+
const params = defaultFieldParams(seed);
|
|
67
|
+
params.heatBias = -0.02;
|
|
68
|
+
params.moistureBias = 0.08;
|
|
69
|
+
return params;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectSurface(sample: FieldSample): SurfaceCoverId {
|
|
73
|
+
if (sample.water > 0.58 || sample.cumulativeHeight < 0.14) {
|
|
74
|
+
return SurfaceCover.Water;
|
|
75
|
+
}
|
|
76
|
+
if (sample.heat < 0.18 && sample.water > 0.36) {
|
|
77
|
+
return SurfaceCover.Ice;
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
sample.heat < 0.3 &&
|
|
81
|
+
(sample.slopeBand === SlopeBand.Upward || sample.cumulativeHeight > 0.52)
|
|
82
|
+
) {
|
|
83
|
+
return sample.obstacleMask > 0.58 ? SurfaceCover.Rock : SurfaceCover.Gravel;
|
|
84
|
+
}
|
|
85
|
+
if (sample.slopeBand === SlopeBand.Downward && sample.moisture > 0.52) {
|
|
86
|
+
return SurfaceCover.Mud;
|
|
87
|
+
}
|
|
88
|
+
if (sample.obstacleMask > 0.72 || sample.slopeBand === SlopeBand.Upward) {
|
|
89
|
+
return sample.heat > 0.72 ? SurfaceCover.Basalt : SurfaceCover.Rock;
|
|
90
|
+
}
|
|
91
|
+
if (sample.heat > 0.74 && sample.moisture < 0.32) {
|
|
92
|
+
return SurfaceCover.Sand;
|
|
93
|
+
}
|
|
94
|
+
const grassCapable =
|
|
95
|
+
sample.heat >= 0.32 &&
|
|
96
|
+
sample.heat <= 0.82 &&
|
|
97
|
+
sample.moisture >= 0.34 &&
|
|
98
|
+
sample.moisture <= 0.88 &&
|
|
99
|
+
sample.slopeBand === SlopeBand.Flat;
|
|
100
|
+
if (grassCapable && sample.foliageMask > 0.4) {
|
|
101
|
+
return SurfaceCover.Grass;
|
|
102
|
+
}
|
|
103
|
+
if (sample.moisture > 0.6) {
|
|
104
|
+
return SurfaceCover.Dirt;
|
|
105
|
+
}
|
|
106
|
+
return sample.obstacleMask > 0.5 ? SurfaceCover.Gravel : SurfaceCover.Dirt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function supportsGroundFoliage(surface: SurfaceCoverId): boolean {
|
|
110
|
+
return (
|
|
111
|
+
surface === SurfaceCover.Grass ||
|
|
112
|
+
surface === SurfaceCover.Dirt ||
|
|
113
|
+
surface === SurfaceCover.Mud
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function supportsObstacleSurface(surface: SurfaceCoverId): boolean {
|
|
118
|
+
return (
|
|
119
|
+
surface === SurfaceCover.Rock ||
|
|
120
|
+
surface === SurfaceCover.Gravel ||
|
|
121
|
+
surface === SurfaceCover.Basalt
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function axialDistance(a: HexCell, b: HexCell): number {
|
|
126
|
+
const dq = a.q - b.q;
|
|
127
|
+
const dr = a.r - b.r;
|
|
128
|
+
return (Math.abs(dq) + Math.abs(dr) + Math.abs(dq + dr)) * 0.5;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function selectFeature(
|
|
132
|
+
surface: SurfaceCoverId,
|
|
133
|
+
sample: FieldSample,
|
|
134
|
+
variation: number,
|
|
135
|
+
nearestTreeDistance: number,
|
|
136
|
+
nearestObstacleDistance: number,
|
|
137
|
+
nearbyObstacleRatio: number,
|
|
138
|
+
isTreeAnchor: boolean
|
|
139
|
+
): MicroFeatureId | undefined {
|
|
140
|
+
if (isTreeAnchor) {
|
|
141
|
+
return MicroFeature.Tree;
|
|
142
|
+
}
|
|
143
|
+
if (surface === SurfaceCover.Water) {
|
|
144
|
+
return variation > 0.88 && sample.moisture > 0.64 ? MicroFeature.Reed : MicroFeature.WaterRipple;
|
|
145
|
+
}
|
|
146
|
+
if (surface === SurfaceCover.Ice && variation > 0.45) {
|
|
147
|
+
return MicroFeature.IceSpike;
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
surface === SurfaceCover.Rock ||
|
|
151
|
+
surface === SurfaceCover.Gravel ||
|
|
152
|
+
surface === SurfaceCover.Basalt
|
|
153
|
+
) {
|
|
154
|
+
return sample.obstacleMask > 0.85 ? MicroFeature.Boulder : MicroFeature.Rock;
|
|
155
|
+
}
|
|
156
|
+
if (surface === SurfaceCover.Mud && variation > 0.78) {
|
|
157
|
+
return MicroFeature.Reed;
|
|
158
|
+
}
|
|
159
|
+
const nearTree = nearestTreeDistance <= 2.0;
|
|
160
|
+
const underCanopy = nearestTreeDistance <= 1.2;
|
|
161
|
+
|
|
162
|
+
if (nearTree && sample.foliageMask > 0.48 && sample.moisture > 0.42 && variation > 0.36) {
|
|
163
|
+
if (surface === SurfaceCover.Mud) {
|
|
164
|
+
return MicroFeature.Bush;
|
|
165
|
+
}
|
|
166
|
+
if (variation > 0.7 || underCanopy) {
|
|
167
|
+
return MicroFeature.Bush;
|
|
168
|
+
}
|
|
169
|
+
return MicroFeature.GrassTuft;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const openArea =
|
|
173
|
+
nearestTreeDistance >= 3.0 &&
|
|
174
|
+
nearestObstacleDistance >= 2.2 &&
|
|
175
|
+
(surface === SurfaceCover.Grass || surface === SurfaceCover.Dirt);
|
|
176
|
+
if (
|
|
177
|
+
openArea &&
|
|
178
|
+
sample.moisture > 0.32 &&
|
|
179
|
+
sample.moisture < 0.76 &&
|
|
180
|
+
sample.heat > 0.28 &&
|
|
181
|
+
sample.heat < 0.82 &&
|
|
182
|
+
variation > 0.9
|
|
183
|
+
) {
|
|
184
|
+
return MicroFeature.Flower;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const obstacleFalloff = Number.isFinite(nearestObstacleDistance)
|
|
188
|
+
? clamp01((3.5 - nearestObstacleDistance) / 3.5)
|
|
189
|
+
: 0;
|
|
190
|
+
const snowLikeGrassDeposit = clamp01(obstacleFalloff * 0.7 + nearbyObstacleRatio * 0.3);
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
supportsGroundFoliage(surface) &&
|
|
194
|
+
sample.heat >= 0.3 &&
|
|
195
|
+
sample.heat <= 0.84 &&
|
|
196
|
+
sample.moisture >= 0.28 &&
|
|
197
|
+
sample.moisture <= 0.9 &&
|
|
198
|
+
sample.water < 0.24 &&
|
|
199
|
+
variation < 0.96
|
|
200
|
+
) {
|
|
201
|
+
const depositThreshold = clamp01(
|
|
202
|
+
0.44 -
|
|
203
|
+
snowLikeGrassDeposit * 0.24 -
|
|
204
|
+
sample.foliageMask * 0.08 +
|
|
205
|
+
Math.max(0, sample.moisture - 0.75) * 0.1
|
|
206
|
+
);
|
|
207
|
+
if (variation > depositThreshold) {
|
|
208
|
+
return MicroFeature.GrassTuft;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.6 && variation > 0.94) {
|
|
213
|
+
return MicroFeature.Ruin;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function selectBiome(surface: SurfaceCoverId, sample: FieldSample): TerrainBiomeId {
|
|
220
|
+
if (surface === SurfaceCover.Water) {
|
|
221
|
+
return sample.heat < 0.25 ? TerrainBiome.Ice : TerrainBiome.River;
|
|
222
|
+
}
|
|
223
|
+
if (surface === SurfaceCover.Ice) {
|
|
224
|
+
return TerrainBiome.Ice;
|
|
225
|
+
}
|
|
226
|
+
if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.68) {
|
|
227
|
+
return sample.heat > 0.72 ? TerrainBiome.Volcanic : TerrainBiome.Mountainous;
|
|
228
|
+
}
|
|
229
|
+
if (sample.heat < 0.3) {
|
|
230
|
+
return TerrainBiome.Tundra;
|
|
231
|
+
}
|
|
232
|
+
if (sample.heat > 0.74 && sample.moisture < 0.33) {
|
|
233
|
+
return TerrainBiome.Savanna;
|
|
234
|
+
}
|
|
235
|
+
if (sample.foliageMask > 0.55 && sample.moisture > 0.45) {
|
|
236
|
+
return TerrainBiome.MixedForest;
|
|
237
|
+
}
|
|
238
|
+
return TerrainBiome.Plains;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function selectMacroBiome(sample: FieldSample): MacroBiomeId {
|
|
242
|
+
if (sample.water > 0.62) {
|
|
243
|
+
return MacroBiome.Freshwater;
|
|
244
|
+
}
|
|
245
|
+
if (sample.slopeBand === SlopeBand.Upward && sample.obstacleMask > 0.68) {
|
|
246
|
+
return MacroBiome.Alpine;
|
|
247
|
+
}
|
|
248
|
+
if (sample.heat > 0.74 && sample.moisture < 0.33) {
|
|
249
|
+
return MacroBiome.Arid;
|
|
250
|
+
}
|
|
251
|
+
if (sample.heat < 0.3) {
|
|
252
|
+
return MacroBiome.ColdTemperate;
|
|
253
|
+
}
|
|
254
|
+
return MacroBiome.Temperate;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function generateTemperateMixedForest(options: MixedForestOptions): MixedForestLayer {
|
|
258
|
+
const terrainSeed = (options.terrainSeed ?? options.seed ?? 1337) >>> 0;
|
|
259
|
+
const featureSeed = (options.featureSeed ?? terrainSeed ^ 0x9e3779b9) >>> 0;
|
|
260
|
+
const foliageSeed = (options.foliageSeed ?? terrainSeed ^ 0x85ebca6b) >>> 0;
|
|
261
|
+
const levelSpecs = buildHexLevels({
|
|
262
|
+
topAreaKm2: options.topAreaKm2 ?? 1000,
|
|
263
|
+
minAreaM2: options.minAreaM2 ?? 10,
|
|
264
|
+
levels: options.levels ?? 6,
|
|
265
|
+
});
|
|
266
|
+
const levelSpec = levelSpecs[levelSpecs.length - 1];
|
|
267
|
+
const radius = options.radius ?? 6;
|
|
268
|
+
const cells = generateHexGrid(radius, levelSpec.level);
|
|
269
|
+
const terrainParams = createBiomeFieldParams(terrainSeed);
|
|
270
|
+
const featureParams = createBiomeFieldParams(featureSeed);
|
|
271
|
+
const foliageParams = createBiomeFieldParams(foliageSeed);
|
|
272
|
+
|
|
273
|
+
const candidates = cells.map((cell) => {
|
|
274
|
+
const world = axialToWorld(cell.q, cell.r, 1);
|
|
275
|
+
const terrainSample = sampleFieldStack(world.x, world.y, terrainParams);
|
|
276
|
+
const featureSample = sampleFieldStack(world.x, world.y, featureParams);
|
|
277
|
+
const foliageSample = sampleFieldStack(world.x, world.y, foliageParams);
|
|
278
|
+
const sample: FieldSample = {
|
|
279
|
+
...terrainSample,
|
|
280
|
+
featureMask: featureSample.featureMask,
|
|
281
|
+
obstacleMask: featureSample.obstacleMask,
|
|
282
|
+
foliageMask: foliageSample.foliageMask,
|
|
283
|
+
};
|
|
284
|
+
const featureNoise = hashCell(cell, featureSeed, 0x77ab);
|
|
285
|
+
const foliageNoise = hashCell(cell, foliageSeed, 0x51ac);
|
|
286
|
+
const variation = clamp01(featureNoise * 0.65 + foliageNoise * 0.35);
|
|
287
|
+
const surface = selectSurface(sample);
|
|
288
|
+
const biome = selectBiome(surface, sample);
|
|
289
|
+
return {
|
|
290
|
+
cell,
|
|
291
|
+
sample,
|
|
292
|
+
variation,
|
|
293
|
+
surface,
|
|
294
|
+
biome,
|
|
295
|
+
macroBiome: selectMacroBiome(sample),
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const treeAnchorIndexes: number[] = [];
|
|
300
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
301
|
+
const candidate = candidates[i];
|
|
302
|
+
if (!supportsGroundFoliage(candidate.surface)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const treeChance = clamp01(
|
|
306
|
+
0.008 +
|
|
307
|
+
candidate.sample.foliageMask * 0.03 +
|
|
308
|
+
Math.max(0, candidate.sample.moisture - 0.45) * 0.04 -
|
|
309
|
+
candidate.sample.obstacleMask * 0.02
|
|
310
|
+
);
|
|
311
|
+
const treeRoll = hashCell(candidate.cell, foliageSeed, 0xa11c);
|
|
312
|
+
if (
|
|
313
|
+
candidate.sample.heat > 0.24 &&
|
|
314
|
+
candidate.sample.heat < 0.82 &&
|
|
315
|
+
candidate.sample.moisture > 0.42 &&
|
|
316
|
+
candidate.sample.water < 0.2 &&
|
|
317
|
+
treeRoll < treeChance
|
|
318
|
+
) {
|
|
319
|
+
treeAnchorIndexes.push(i);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const treeAnchorSet = new Set<number>(treeAnchorIndexes);
|
|
324
|
+
const obstacleAnchorIndexes: number[] = [];
|
|
325
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
326
|
+
const candidate = candidates[i];
|
|
327
|
+
if (supportsObstacleSurface(candidate.surface) || candidate.sample.obstacleMask > 0.72) {
|
|
328
|
+
obstacleAnchorIndexes.push(i);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const obstacleRadius = 4;
|
|
332
|
+
|
|
333
|
+
const terrain = candidates.map((candidate, index) => {
|
|
334
|
+
const isTreeAnchor = treeAnchorSet.has(index);
|
|
335
|
+
let nearestTreeDistance = Number.POSITIVE_INFINITY;
|
|
336
|
+
if (isTreeAnchor) {
|
|
337
|
+
nearestTreeDistance = 0;
|
|
338
|
+
} else {
|
|
339
|
+
for (const treeIndex of treeAnchorIndexes) {
|
|
340
|
+
const distance = axialDistance(candidate.cell, candidates[treeIndex].cell);
|
|
341
|
+
if (distance < nearestTreeDistance) {
|
|
342
|
+
nearestTreeDistance = distance;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let nearestObstacleDistance = Number.POSITIVE_INFINITY;
|
|
348
|
+
let nearbyObstacleCount = 0;
|
|
349
|
+
for (const obstacleIndex of obstacleAnchorIndexes) {
|
|
350
|
+
const distance = axialDistance(candidate.cell, candidates[obstacleIndex].cell);
|
|
351
|
+
if (distance < nearestObstacleDistance) {
|
|
352
|
+
nearestObstacleDistance = distance;
|
|
353
|
+
}
|
|
354
|
+
if (distance <= obstacleRadius) {
|
|
355
|
+
nearbyObstacleCount += 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const nearbyObstacleRatio = clamp01(nearbyObstacleCount / 12);
|
|
359
|
+
|
|
360
|
+
const feature = selectFeature(
|
|
361
|
+
candidate.surface,
|
|
362
|
+
candidate.sample,
|
|
363
|
+
candidate.variation,
|
|
364
|
+
nearestTreeDistance,
|
|
365
|
+
nearestObstacleDistance,
|
|
366
|
+
nearbyObstacleRatio,
|
|
367
|
+
isTreeAnchor
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const cellData: TerrainCell = {
|
|
371
|
+
height: clamp01(candidate.sample.height),
|
|
372
|
+
heat: candidate.sample.heat,
|
|
373
|
+
moisture: candidate.sample.moisture,
|
|
374
|
+
biome: candidate.biome,
|
|
375
|
+
macroBiome: candidate.macroBiome,
|
|
376
|
+
surface: candidate.surface,
|
|
377
|
+
feature,
|
|
378
|
+
obstacle: candidate.sample.obstacleMask,
|
|
379
|
+
foliage: candidate.sample.foliageMask,
|
|
380
|
+
slopeBand: candidate.sample.slopeBand,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return cellData;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return { levelSpec, cells, terrain };
|
|
387
|
+
}
|
package/src/field.wgsl
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
struct FieldParams {
|
|
2
|
+
seed: u32,
|
|
3
|
+
sample_count: u32,
|
|
4
|
+
scale: f32,
|
|
5
|
+
warp_scale: f32,
|
|
6
|
+
warp_strength: f32,
|
|
7
|
+
iterations: u32,
|
|
8
|
+
power: f32,
|
|
9
|
+
detail_scale: f32,
|
|
10
|
+
detail_iterations: u32,
|
|
11
|
+
detail_power: f32,
|
|
12
|
+
ridge_power: f32,
|
|
13
|
+
heat_bias: f32,
|
|
14
|
+
moisture_bias: f32,
|
|
15
|
+
macro_scale: f32,
|
|
16
|
+
macro_warp_strength: f32,
|
|
17
|
+
style_mix_strength: f32,
|
|
18
|
+
terrace_steps: f32,
|
|
19
|
+
terrace_strength: f32,
|
|
20
|
+
crater_strength: f32,
|
|
21
|
+
crater_scale: f32,
|
|
22
|
+
height_min: f32,
|
|
23
|
+
height_max: f32,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
struct FieldSample {
|
|
27
|
+
height: f32,
|
|
28
|
+
heat: f32,
|
|
29
|
+
moisture: f32,
|
|
30
|
+
roughness: f32,
|
|
31
|
+
rockiness: f32,
|
|
32
|
+
water: f32,
|
|
33
|
+
ridge: f32,
|
|
34
|
+
base: f32,
|
|
35
|
+
detail: f32,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@group(1) @binding(0) var<storage, read> points: array<vec2<f32>>;
|
|
39
|
+
@group(1) @binding(1) var<storage, read_write> samples: array<FieldSample>;
|
|
40
|
+
@group(1) @binding(2) var<uniform> params: FieldParams;
|
|
41
|
+
|
|
42
|
+
fn hash32(x: u32) -> u32 {
|
|
43
|
+
var v = x;
|
|
44
|
+
v ^= v >> 17u;
|
|
45
|
+
v *= 0xed5ad4bbu;
|
|
46
|
+
v ^= v >> 11u;
|
|
47
|
+
v *= 0xac4c1b51u;
|
|
48
|
+
v ^= v >> 15u;
|
|
49
|
+
v *= 0x31848babu;
|
|
50
|
+
v ^= v >> 14u;
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn hash01(x: u32) -> f32 {
|
|
55
|
+
let v = hash32(x) & 0x00ffffffu;
|
|
56
|
+
return f32(v) / 16777216.0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn smooth_mandelbrot(cx: f32, cy: f32, iterations: u32, power: f32) -> f32 {
|
|
60
|
+
var zx = 0.0;
|
|
61
|
+
var zy = 0.0;
|
|
62
|
+
var i: u32 = 0u;
|
|
63
|
+
loop {
|
|
64
|
+
if (i >= iterations) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
let r2 = zx * zx + zy * zy;
|
|
68
|
+
if (r2 > 4.0) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
let r = sqrt(r2);
|
|
72
|
+
let theta = atan2(zy, zx);
|
|
73
|
+
let rp = pow(r, power);
|
|
74
|
+
zx = rp * cos(theta * power) + cx;
|
|
75
|
+
zy = rp * sin(theta * power) + cy;
|
|
76
|
+
i = i + 1u;
|
|
77
|
+
}
|
|
78
|
+
if (i >= iterations) {
|
|
79
|
+
return 1.0;
|
|
80
|
+
}
|
|
81
|
+
let r = max(sqrt(zx * zx + zy * zy), 1e-6);
|
|
82
|
+
let nu = log2(log(r));
|
|
83
|
+
let smoothVal = (f32(i) + 1.0 - nu) / f32(iterations);
|
|
84
|
+
return clamp(smoothVal, 0.0, 1.0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn terrace_height(h: f32, steps: f32) -> f32 {
|
|
88
|
+
let count = max(1.0, steps);
|
|
89
|
+
let step = 1.0 / count;
|
|
90
|
+
let clamped = clamp(h, 0.0, 1.0);
|
|
91
|
+
let band = floor(clamped / step);
|
|
92
|
+
let t = fract(clamped / step);
|
|
93
|
+
let blend = t * t * (3.0 - 2.0 * t);
|
|
94
|
+
return (band + blend) * step;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn crater_field(p: vec2<f32>, scale: f32, seed: u32) -> f32 {
|
|
98
|
+
let sp = p * scale;
|
|
99
|
+
let cell = floor(sp);
|
|
100
|
+
let local = sp - cell;
|
|
101
|
+
let cx = i32(cell.x);
|
|
102
|
+
let cz = i32(cell.y);
|
|
103
|
+
let hx = u32(bitcast<u32>(cx));
|
|
104
|
+
let hz = u32(bitcast<u32>(cz));
|
|
105
|
+
let h0 = hash01((hx * 374761393u) ^ (hz * 668265263u) ^ seed ^ 0x9e3779b9u);
|
|
106
|
+
let h1 = hash01((hx * 2246822519u) ^ (hz * 3266489917u) ^ seed ^ 0x85ebca6bu);
|
|
107
|
+
let h2 = hash01((hx * 1597334677u) ^ (hz * 3812015801u) ^ seed ^ 0xc2b2ae35u);
|
|
108
|
+
let center = vec2<f32>(h0, h1);
|
|
109
|
+
let radius = 0.22 + 0.25 * h2;
|
|
110
|
+
let dist = distance(local, center);
|
|
111
|
+
return smoothstep(radius, radius * 0.35, dist);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn macro_map(p: vec2<f32>, iterations: u32, power: f32) -> f32 {
|
|
115
|
+
let macro_iter = max(12u, iterations / 3u);
|
|
116
|
+
let macroA = smooth_mandelbrot(p.x * params.macro_scale, p.y * params.macro_scale, macro_iter, power);
|
|
117
|
+
let macroB = smooth_mandelbrot(
|
|
118
|
+
(p.x + 13.7) * params.macro_scale,
|
|
119
|
+
(p.y - 9.2) * params.macro_scale,
|
|
120
|
+
macro_iter,
|
|
121
|
+
power + 0.35
|
|
122
|
+
);
|
|
123
|
+
let warp = vec2<f32>(macroA - 0.5, macroB - 0.5) * params.macro_warp_strength;
|
|
124
|
+
let macroC = smooth_mandelbrot(
|
|
125
|
+
(p.x + warp.x) * params.macro_scale,
|
|
126
|
+
(p.y + warp.y) * params.macro_scale,
|
|
127
|
+
macro_iter,
|
|
128
|
+
power
|
|
129
|
+
);
|
|
130
|
+
return clamp((macroC - 0.5) * params.style_mix_strength + 0.5, 0.0, 1.0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn sample_field(p: vec2<f32>) -> FieldSample {
|
|
134
|
+
let seed = params.seed;
|
|
135
|
+
let offX = hash01(seed ^ 0x7f4a7c15u) * 4.0 - 2.0;
|
|
136
|
+
let offZ = hash01(seed ^ 0xa9d84d2bu) * 4.0 - 2.0;
|
|
137
|
+
let warpOffX = hash01(seed ^ 0x8c2f1d3bu) * 6.0 - 3.0;
|
|
138
|
+
let warpOffZ = hash01(seed ^ 0x5d2c79e9u) * 6.0 - 3.0;
|
|
139
|
+
|
|
140
|
+
let warpIter = max(16u, params.iterations * 6u / 10u);
|
|
141
|
+
let warpA = smooth_mandelbrot(
|
|
142
|
+
(p.x + warpOffX) * params.warp_scale,
|
|
143
|
+
(p.y + warpOffZ) * params.warp_scale,
|
|
144
|
+
warpIter,
|
|
145
|
+
params.power
|
|
146
|
+
);
|
|
147
|
+
let warpB = smooth_mandelbrot(
|
|
148
|
+
(p.x - warpOffZ) * params.warp_scale,
|
|
149
|
+
(p.y + warpOffX) * params.warp_scale,
|
|
150
|
+
warpIter,
|
|
151
|
+
params.power
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
let warped = vec2<f32>(
|
|
155
|
+
p.x + (warpA - 0.5) * params.warp_strength,
|
|
156
|
+
p.y + (warpB - 0.5) * params.warp_strength
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
let base = smooth_mandelbrot(
|
|
160
|
+
warped.x * params.scale + offX,
|
|
161
|
+
warped.y * params.scale + offZ,
|
|
162
|
+
params.iterations,
|
|
163
|
+
params.power
|
|
164
|
+
);
|
|
165
|
+
let mid = smooth_mandelbrot(
|
|
166
|
+
warped.x * params.scale * 2.15 + offX * 0.6,
|
|
167
|
+
warped.y * params.scale * 2.15 + offZ * 0.6,
|
|
168
|
+
max(18u, params.iterations * 7u / 10u),
|
|
169
|
+
params.power + 0.2
|
|
170
|
+
);
|
|
171
|
+
let detail = smooth_mandelbrot(
|
|
172
|
+
warped.x * params.scale * params.detail_scale + offX * 1.4,
|
|
173
|
+
warped.y * params.scale * params.detail_scale + offZ * 1.4,
|
|
174
|
+
params.detail_iterations,
|
|
175
|
+
params.detail_power
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let ridge = 1.0 - abs(2.0 * mid - 1.0);
|
|
179
|
+
let baseHeight = pow(base, 0.9) * pow(mid, 1.05) * pow(detail, 1.1);
|
|
180
|
+
|
|
181
|
+
let styleMask = macro_map(p, params.iterations, params.power);
|
|
182
|
+
let terrace = terrace_height(baseHeight, params.terrace_steps);
|
|
183
|
+
let crater = crater_field(p, params.crater_scale, seed);
|
|
184
|
+
|
|
185
|
+
let styleA = clamp(pow(baseHeight, 0.8) + pow(ridge, 1.4) * 0.2, 0.0, 1.0);
|
|
186
|
+
let styleB = clamp(
|
|
187
|
+
baseHeight * (1.0 - params.terrace_strength) +
|
|
188
|
+
terrace * params.terrace_strength -
|
|
189
|
+
crater * params.crater_strength +
|
|
190
|
+
pow(ridge, 1.6) * 0.12,
|
|
191
|
+
0.0,
|
|
192
|
+
1.0
|
|
193
|
+
);
|
|
194
|
+
let mixed = mix(styleA, styleB, styleMask);
|
|
195
|
+
let ridgeBoost = pow(ridge, 1.35) * 0.22;
|
|
196
|
+
let centered = (mixed - 0.5) * 2.0;
|
|
197
|
+
let shaped = sign(centered) * pow(abs(centered), 0.75);
|
|
198
|
+
let macroOffset = (styleMask - 0.5) * 0.25;
|
|
199
|
+
let rawHeight = clamp(
|
|
200
|
+
0.5 + shaped * 0.8 + macroOffset + ridgeBoost,
|
|
201
|
+
params.height_min,
|
|
202
|
+
params.height_max
|
|
203
|
+
);
|
|
204
|
+
let height01 = clamp(rawHeight, 0.0, 1.0);
|
|
205
|
+
|
|
206
|
+
let roughness = clamp(pow(ridge, params.ridge_power) * 0.7 + detail * 0.3, 0.0, 1.0);
|
|
207
|
+
let heat = clamp(0.55 * mid + 0.35 * (1.0 - height01) + params.heat_bias, 0.0, 1.0);
|
|
208
|
+
let moisture = clamp(
|
|
209
|
+
0.55 * detail + 0.35 * (1.0 - height01) - (heat - 0.5) * 0.1 + params.moisture_bias,
|
|
210
|
+
0.0,
|
|
211
|
+
1.0
|
|
212
|
+
);
|
|
213
|
+
let rockiness = clamp(roughness * 0.6 + height01 * 0.4, 0.0, 1.0);
|
|
214
|
+
let water = clamp((0.32 - height01) * 3.0 + (moisture - 0.5) * 0.2, 0.0, 1.0);
|
|
215
|
+
|
|
216
|
+
return FieldSample(rawHeight, heat, moisture, roughness, rockiness, water, ridge, base, detail);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn process_job(job_index: u32, job_type: u32, payload_words: u32) {
|
|
220
|
+
if (job_index >= params.sample_count) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let p = points[job_index];
|
|
224
|
+
samples[job_index] = sample_field(p);
|
|
225
|
+
}
|