@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.
Files changed (43) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +73 -0
  3. package/dist/field.wgsl +225 -0
  4. package/dist/fractal-prepass.wgsl +290 -0
  5. package/dist/index.cjs +1942 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +447 -0
  8. package/dist/index.d.ts +447 -0
  9. package/dist/index.js +1848 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/terrain.wgsl +451 -0
  12. package/docs/adrs/adr-0001-package-scope.md +18 -0
  13. package/docs/adrs/adr-0002-world-tiling-lod-stitching.md +28 -0
  14. package/docs/adrs/adr-0003-terrain-generation-style-mixing.md +21 -0
  15. package/docs/adrs/index.md +5 -0
  16. package/docs/biomes.md +206 -0
  17. package/docs/lod-zoning.md +22 -0
  18. package/docs/plan.md +107 -0
  19. package/docs/procedural-surface.md +73 -0
  20. package/docs/resources.md +55 -0
  21. package/package.json +53 -0
  22. package/src/biomes/temperate.ts +387 -0
  23. package/src/field.wgsl +225 -0
  24. package/src/fields.ts +321 -0
  25. package/src/fractal-prepass.ts +237 -0
  26. package/src/fractal-prepass.wgsl +290 -0
  27. package/src/generator.ts +106 -0
  28. package/src/hex.ts +54 -0
  29. package/src/index.ts +11 -0
  30. package/src/mesh.ts +285 -0
  31. package/src/perf-monitor.ts +133 -0
  32. package/src/shaders/demo-terrain.wgsl +193 -0
  33. package/src/shaders/demo-trees.wgsl +172 -0
  34. package/src/shaders/demo-water-sim.wgsl +361 -0
  35. package/src/shaders/library/common.wgsl +38 -0
  36. package/src/shaders/library/materials.wgsl +175 -0
  37. package/src/terrain.wgsl +451 -0
  38. package/src/tile-cache.ts +274 -0
  39. package/src/tiles.ts +417 -0
  40. package/src/types.ts +220 -0
  41. package/src/wgsl/field.job.wgsl +225 -0
  42. package/src/wgsl/terrain.job.wgsl +451 -0
  43. package/src/wgsl.ts +77 -0
package/src/fields.ts ADDED
@@ -0,0 +1,321 @@
1
+ import { SlopeBand, type SlopeBandId } from "./types";
2
+
3
+ export const FIELD_DOWNWARD_MAX = 0.2;
4
+ export const FIELD_UPWARD_MIN = 0.8;
5
+
6
+ export type FieldParams = {
7
+ seed: number;
8
+ scale: number;
9
+ warpScale: number;
10
+ warpStrength: number;
11
+ iterations: number;
12
+ power: number;
13
+ detailScale: number;
14
+ detailIterations: number;
15
+ detailPower: number;
16
+ ridgePower: number;
17
+ heatBias: number;
18
+ moistureBias: number;
19
+ macroScale: number;
20
+ macroWarpStrength: number;
21
+ styleMixStrength: number;
22
+ terraceSteps: number;
23
+ terraceStrength: number;
24
+ craterStrength: number;
25
+ craterScale: number;
26
+ heightMin: number;
27
+ heightMax: number;
28
+ };
29
+
30
+ export type FieldSample = {
31
+ height: number;
32
+ cumulativeHeight: number;
33
+ slopeBand: SlopeBandId;
34
+ heat: number;
35
+ moisture: number;
36
+ roughness: number;
37
+ rockiness: number;
38
+ water: number;
39
+ featureMask: number;
40
+ obstacleMask: number;
41
+ foliageMask: number;
42
+ ridge: number;
43
+ base: number;
44
+ detail: number;
45
+ };
46
+
47
+ export function classifySlopeBand(cumulativeHeight: number): SlopeBandId {
48
+ if (cumulativeHeight < FIELD_DOWNWARD_MAX) {
49
+ return SlopeBand.Downward;
50
+ }
51
+ if (cumulativeHeight >= FIELD_UPWARD_MIN) {
52
+ return SlopeBand.Upward;
53
+ }
54
+ return SlopeBand.Flat;
55
+ }
56
+
57
+ export function defaultFieldParams(seed = 1337): FieldParams {
58
+ return {
59
+ seed,
60
+ scale: 0.14,
61
+ warpScale: 0.5,
62
+ warpStrength: 0.75,
63
+ iterations: 64,
64
+ power: 2.2,
65
+ detailScale: 3.2,
66
+ detailIterations: 28,
67
+ detailPower: 2.0,
68
+ ridgePower: 1.25,
69
+ heatBias: 0,
70
+ moistureBias: 0,
71
+ macroScale: 0.035,
72
+ macroWarpStrength: 0.18,
73
+ styleMixStrength: 1.0,
74
+ terraceSteps: 6,
75
+ terraceStrength: 0.35,
76
+ craterStrength: 0.25,
77
+ craterScale: 0.18,
78
+ heightMin: -0.35,
79
+ heightMax: 1.6,
80
+ };
81
+ }
82
+
83
+ function clamp(value: number, min: number, max: number) {
84
+ return Math.min(max, Math.max(min, value));
85
+ }
86
+
87
+ function smoothstep(edge0: number, edge1: number, x: number) {
88
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
89
+ return t * t * (3 - 2 * t);
90
+ }
91
+
92
+ function hash01(seed: number) {
93
+ const s = Math.sin(seed) * 43758.5453123;
94
+ return s - Math.floor(s);
95
+ }
96
+
97
+ function smoothMandelbrot(cx: number, cy: number, iterations: number, power: number) {
98
+ let zx = 0;
99
+ let zy = 0;
100
+ let i = 0;
101
+ for (; i < iterations; i += 1) {
102
+ const r2 = zx * zx + zy * zy;
103
+ if (r2 > 4) {
104
+ break;
105
+ }
106
+ const r = Math.sqrt(r2);
107
+ const theta = Math.atan2(zy, zx);
108
+ const rp = Math.pow(r, power);
109
+ zx = rp * Math.cos(theta * power) + cx;
110
+ zy = rp * Math.sin(theta * power) + cy;
111
+ }
112
+ if (i >= iterations) {
113
+ return 1;
114
+ }
115
+ const r = Math.max(Math.sqrt(zx * zx + zy * zy), 1e-6);
116
+ const nu = Math.log2(Math.log(r));
117
+ const smooth = (i + 1 - nu) / iterations;
118
+ return clamp(smooth, 0, 1);
119
+ }
120
+
121
+ function terrace(height: number, steps: number) {
122
+ const count = Math.max(1, Math.round(steps));
123
+ const step = 1 / count;
124
+ const h = clamp(height, 0, 1);
125
+ const band = Math.floor(h / step);
126
+ const t = (h - band * step) / step;
127
+ const smoothed = t * t * (3 - 2 * t);
128
+ return (band + smoothed) * step;
129
+ }
130
+
131
+ function craterField(x: number, z: number, scale: number, seed: number) {
132
+ const sx = x * scale;
133
+ const sz = z * scale;
134
+ const cellX = Math.floor(sx);
135
+ const cellZ = Math.floor(sz);
136
+ const fx = sx - cellX;
137
+ const fz = sz - cellZ;
138
+ const baseSeed = cellX * 374761393 + cellZ * 668265263 + seed * 1442695041;
139
+ const h0 = hash01(baseSeed * 0.17);
140
+ const h1 = hash01(baseSeed * 0.31 + 17.13);
141
+ const h2 = hash01(baseSeed * 0.47 + 9.2);
142
+ const cx = h0;
143
+ const cz = h1;
144
+ const radius = 0.22 + 0.25 * h2;
145
+ const dx = fx - cx;
146
+ const dz = fz - cz;
147
+ const dist = Math.hypot(dx, dz);
148
+ return smoothstep(radius, radius * 0.35, dist);
149
+ }
150
+
151
+ export function sampleFieldStack(x: number, z: number, params: FieldParams): FieldSample {
152
+ const seed = params.seed;
153
+ const offX = hash01(seed * 0.137 + 0.11) * 4 - 2;
154
+ const offZ = hash01(seed * 0.173 + 0.27) * 4 - 2;
155
+ const warpOffX = hash01(seed * 0.91 + 1.1) * 6 - 3;
156
+ const warpOffZ = hash01(seed * 1.07 + 2.2) * 6 - 3;
157
+
158
+ const warpA = smoothMandelbrot(
159
+ (x + warpOffX) * params.warpScale,
160
+ (z + warpOffZ) * params.warpScale,
161
+ Math.max(16, Math.floor(params.iterations * 0.6)),
162
+ params.power
163
+ );
164
+ const warpB = smoothMandelbrot(
165
+ (x - warpOffZ) * params.warpScale,
166
+ (z + warpOffX) * params.warpScale,
167
+ Math.max(16, Math.floor(params.iterations * 0.6)),
168
+ params.power
169
+ );
170
+
171
+ const warpedX = x + (warpA - 0.5) * params.warpStrength;
172
+ const warpedZ = z + (warpB - 0.5) * params.warpStrength;
173
+
174
+ const base = smoothMandelbrot(
175
+ warpedX * params.scale + offX,
176
+ warpedZ * params.scale + offZ,
177
+ params.iterations,
178
+ params.power
179
+ );
180
+ const mid = smoothMandelbrot(
181
+ warpedX * params.scale * 2.15 + offX * 0.6,
182
+ warpedZ * params.scale * 2.15 + offZ * 0.6,
183
+ Math.max(18, Math.floor(params.iterations * 0.7)),
184
+ params.power + 0.2
185
+ );
186
+ const detail = smoothMandelbrot(
187
+ warpedX * params.scale * params.detailScale + offX * 1.4,
188
+ warpedZ * params.scale * params.detailScale + offZ * 1.4,
189
+ params.detailIterations,
190
+ params.detailPower
191
+ );
192
+
193
+ const ridge = 1 - Math.abs(2 * mid - 1);
194
+ const baseHeight =
195
+ Math.pow(base, 0.9) * Math.pow(mid, 1.05) * Math.pow(detail, 1.1);
196
+
197
+ const macroIter = Math.max(12, Math.floor(params.iterations * 0.35));
198
+ const macroA = smoothMandelbrot(
199
+ x * params.macroScale + offX * 0.2,
200
+ z * params.macroScale + offZ * 0.2,
201
+ macroIter,
202
+ params.power
203
+ );
204
+ const macroB = smoothMandelbrot(
205
+ (x + offZ) * params.macroScale,
206
+ (z - offX) * params.macroScale,
207
+ macroIter,
208
+ params.power + 0.35
209
+ );
210
+ const macroWarpX = (macroA - 0.5) * params.macroWarpStrength;
211
+ const macroWarpZ = (macroB - 0.5) * params.macroWarpStrength;
212
+ const macroMask = smoothMandelbrot(
213
+ (x + macroWarpX) * params.macroScale,
214
+ (z + macroWarpZ) * params.macroScale,
215
+ macroIter,
216
+ params.power
217
+ );
218
+ const styleMask = clamp((macroMask - 0.5) * params.styleMixStrength + 0.5, 0, 1);
219
+
220
+ const terraceHeight = terrace(baseHeight, params.terraceSteps);
221
+ const crater = craterField(x, z, params.craterScale, seed);
222
+ const styleA = clamp(Math.pow(baseHeight, 0.8) + Math.pow(ridge, 1.4) * 0.2, 0, 1);
223
+ const styleB = clamp(
224
+ baseHeight * (1 - params.terraceStrength) +
225
+ terraceHeight * params.terraceStrength -
226
+ crater * params.craterStrength +
227
+ Math.pow(ridge, 1.6) * 0.12,
228
+ 0,
229
+ 1
230
+ );
231
+ const mixed = styleA * (1 - styleMask) + styleB * styleMask;
232
+ const cumulativeHeight = clamp(
233
+ base * 0.38 + mid * 0.33 + detail * 0.21 + styleMask * 0.08,
234
+ 0,
235
+ 1
236
+ );
237
+ const slopeBand = classifySlopeBand(cumulativeHeight);
238
+ const downwardStrength = 1 - smoothstep(0, FIELD_DOWNWARD_MAX, cumulativeHeight);
239
+ const upwardStrength = smoothstep(FIELD_UPWARD_MIN, 1, cumulativeHeight);
240
+ const flatStrength = clamp(1 - Math.max(downwardStrength, upwardStrength), 0, 1);
241
+
242
+ const ridgeBoost = Math.pow(ridge, 1.35) * 0.22;
243
+ const centered = (mixed - 0.5) * 2;
244
+ const shaped = Math.sign(centered) * Math.pow(Math.abs(centered), 0.75);
245
+ const macroOffset = (styleMask - 0.5) * 0.25;
246
+ const rawHeight = clamp(
247
+ 0.5 +
248
+ shaped * 0.8 +
249
+ macroOffset +
250
+ ridgeBoost +
251
+ upwardStrength * 0.22 -
252
+ downwardStrength * 0.22,
253
+ params.heightMin,
254
+ params.heightMax
255
+ );
256
+ const height01 = clamp(rawHeight, 0, 1);
257
+
258
+ const roughness = clamp(
259
+ Math.pow(ridge, params.ridgePower) * 0.7 + detail * 0.3,
260
+ 0,
261
+ 1
262
+ );
263
+ const heat = clamp(0.55 * mid + 0.35 * (1 - height01) + params.heatBias, 0, 1);
264
+ const moisture = clamp(
265
+ 0.55 * detail +
266
+ 0.35 * (1 - height01) -
267
+ (heat - 0.5) * 0.1 +
268
+ params.moistureBias,
269
+ 0,
270
+ 1
271
+ );
272
+ const rockiness = clamp(roughness * 0.6 + height01 * 0.4, 0, 1);
273
+ const water = clamp((0.32 - height01) * 3.0 + (moisture - 0.5) * 0.2, 0, 1);
274
+ const featureMask = smoothMandelbrot(
275
+ warpedX * params.scale * (params.detailScale + 1.25) - offX * 0.85,
276
+ warpedZ * params.scale * (params.detailScale + 1.25) - offZ * 0.85,
277
+ Math.max(14, Math.floor(params.detailIterations * 0.9)),
278
+ params.detailPower + 0.15
279
+ );
280
+ const obstacleMask = clamp(
281
+ featureMask * 0.58 +
282
+ roughness * 0.25 +
283
+ upwardStrength * 0.25 -
284
+ moisture * 0.16 -
285
+ water * 0.2,
286
+ 0,
287
+ 1
288
+ );
289
+ const foliageField = smoothMandelbrot(
290
+ warpedX * params.scale * (params.detailScale * 1.85 + 0.35) + offX * 1.9,
291
+ warpedZ * params.scale * (params.detailScale * 1.85 + 0.35) + offZ * 1.9,
292
+ Math.max(16, Math.floor(params.detailIterations * 1.1)),
293
+ Math.max(1.6, params.detailPower - 0.2)
294
+ );
295
+ const foliageMask = clamp(
296
+ foliageField *
297
+ moisture *
298
+ (1 - water) *
299
+ (0.35 + flatStrength * 0.65) *
300
+ (1 - obstacleMask * 0.82),
301
+ 0,
302
+ 1
303
+ );
304
+
305
+ return {
306
+ height: rawHeight,
307
+ cumulativeHeight,
308
+ slopeBand,
309
+ heat,
310
+ moisture,
311
+ roughness,
312
+ rockiness,
313
+ water,
314
+ featureMask,
315
+ obstacleMask,
316
+ foliageMask,
317
+ ridge,
318
+ base,
319
+ detail,
320
+ };
321
+ }
@@ -0,0 +1,237 @@
1
+ import { defaultFieldParams, type FieldParams } from "./fields";
2
+
3
+ const GPUBufferUsage = (globalThis as any).GPUBufferUsage;
4
+ const GPUMapMode = (globalThis as any).GPUMapMode;
5
+
6
+ type GPUDeviceLike = any;
7
+
8
+ export const FRACTAL_ASSET_VERSION = 2;
9
+ export const FRACTAL_SAMPLE_STRIDE = 8;
10
+
11
+ export type FractalMandelSettings = {
12
+ scale: number;
13
+ strength: number;
14
+ rockBoost: number;
15
+ };
16
+
17
+ export const defaultFractalMandelSettings: FractalMandelSettings = {
18
+ scale: 0.16,
19
+ strength: 0.85,
20
+ rockBoost: 0.7,
21
+ };
22
+
23
+ export type FractalAsset = {
24
+ seed: number;
25
+ extent: number;
26
+ gridSize: number;
27
+ heightScale: number;
28
+ samples: Float32Array;
29
+ };
30
+
31
+ export type FractalAssetPayload = {
32
+ version: number;
33
+ seed: number;
34
+ extent: number;
35
+ gridSize: number;
36
+ heightScale: number;
37
+ sampleStride: number;
38
+ samples: number[];
39
+ };
40
+
41
+ export type FractalPrepassRunOptions = {
42
+ seed: number;
43
+ extent: number;
44
+ heightScale: number;
45
+ fieldParams?: Partial<FieldParams>;
46
+ mandel?: Partial<FractalMandelSettings>;
47
+ };
48
+
49
+ export type FractalPrepassRunner = {
50
+ gridSize: number;
51
+ gridPoints: number;
52
+ sampleCount: number;
53
+ run: (options: FractalPrepassRunOptions) => Promise<FractalAsset>;
54
+ };
55
+
56
+ export function serializeFractalAsset(asset: FractalAsset): FractalAssetPayload {
57
+ return {
58
+ version: FRACTAL_ASSET_VERSION,
59
+ seed: asset.seed,
60
+ extent: asset.extent,
61
+ gridSize: asset.gridSize,
62
+ heightScale: asset.heightScale,
63
+ sampleStride: FRACTAL_SAMPLE_STRIDE,
64
+ samples: Array.from(asset.samples),
65
+ };
66
+ }
67
+
68
+ export function parseFractalAsset(payload: unknown): FractalAsset | null {
69
+ if (!payload || typeof payload !== "object") return null;
70
+ const data = payload as FractalAssetPayload;
71
+ if (data.version !== FRACTAL_ASSET_VERSION) return null;
72
+ if (!Array.isArray(data.samples)) return null;
73
+ if (data.sampleStride !== FRACTAL_SAMPLE_STRIDE) return null;
74
+ const gridSize = Number(data.gridSize);
75
+ const heightScale = Number(data.heightScale);
76
+ if (!Number.isFinite(gridSize) || gridSize <= 0) return null;
77
+ if (!Number.isFinite(heightScale)) return null;
78
+ const expected = (gridSize + 1) * (gridSize + 1) * FRACTAL_SAMPLE_STRIDE;
79
+ if (data.samples.length !== expected) return null;
80
+ return {
81
+ seed: Number(data.seed),
82
+ extent: Number(data.extent),
83
+ gridSize,
84
+ heightScale,
85
+ samples: new Float32Array(data.samples),
86
+ };
87
+ }
88
+
89
+ export function assetMatches(
90
+ asset: FractalAsset | null,
91
+ config: { seed: number; extent: number; gridSize: number }
92
+ ) {
93
+ if (!asset) return false;
94
+ if (asset.seed !== config.seed) return false;
95
+ if (asset.gridSize !== config.gridSize) return false;
96
+ if (Math.abs(asset.extent - config.extent) > 1e-3) return false;
97
+ if (!Number.isFinite(asset.heightScale)) return false;
98
+ const expected = (asset.gridSize + 1) * (asset.gridSize + 1) * FRACTAL_SAMPLE_STRIDE;
99
+ if (asset.samples.length !== expected) return false;
100
+ return true;
101
+ }
102
+
103
+ export function createFractalPrepassRunner(options: {
104
+ device: GPUDeviceLike;
105
+ wgsl: string;
106
+ gridSize: number;
107
+ }): FractalPrepassRunner {
108
+ if (!GPUBufferUsage || !GPUMapMode) {
109
+ throw new Error("WebGPU globals not available. Ensure this runs in a WebGPU context.");
110
+ }
111
+ const { device, wgsl, gridSize } = options;
112
+ const gridPoints = gridSize + 1;
113
+ const sampleCount = gridPoints * gridPoints;
114
+ const byteSize = sampleCount * FRACTAL_SAMPLE_STRIDE * 4;
115
+
116
+ const uniformBuffer = device.createBuffer({
117
+ size: 7 * 16,
118
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
119
+ });
120
+
121
+ const baseBuffer = device.createBuffer({
122
+ size: byteSize,
123
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
124
+ });
125
+
126
+ const accentBuffer = device.createBuffer({
127
+ size: byteSize,
128
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
129
+ });
130
+
131
+ const readbackBuffer = device.createBuffer({
132
+ size: byteSize,
133
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
134
+ });
135
+
136
+ const module = device.createShaderModule({ code: wgsl });
137
+ const basePipeline = device.createComputePipeline({
138
+ layout: "auto",
139
+ compute: { module, entryPoint: "main" },
140
+ });
141
+ const accentPipeline = device.createComputePipeline({
142
+ layout: "auto",
143
+ compute: { module, entryPoint: "accent_heights" },
144
+ });
145
+
146
+ const baseBindGroup = device.createBindGroup({
147
+ layout: basePipeline.getBindGroupLayout(0),
148
+ entries: [
149
+ { binding: 0, resource: { buffer: uniformBuffer } },
150
+ { binding: 1, resource: { buffer: baseBuffer } },
151
+ ],
152
+ });
153
+ const accentBindGroup = device.createBindGroup({
154
+ layout: accentPipeline.getBindGroupLayout(0),
155
+ entries: [
156
+ { binding: 0, resource: { buffer: uniformBuffer } },
157
+ { binding: 1, resource: { buffer: accentBuffer } },
158
+ { binding: 2, resource: { buffer: baseBuffer } },
159
+ ],
160
+ });
161
+
162
+ const workgroups = Math.ceil(gridPoints / 8);
163
+
164
+ const run = async (runOptions: FractalPrepassRunOptions): Promise<FractalAsset> => {
165
+ const fieldDefaults = defaultFieldParams(runOptions.seed);
166
+ const params: FieldParams = {
167
+ ...fieldDefaults,
168
+ ...runOptions.fieldParams,
169
+ seed: runOptions.seed,
170
+ };
171
+ const mandel = { ...defaultFractalMandelSettings, ...runOptions.mandel };
172
+ const step = (runOptions.extent * 2) / gridSize;
173
+
174
+ const uniformData = new Float32Array(28);
175
+ uniformData.set([gridPoints, runOptions.extent, step, runOptions.seed], 0);
176
+ uniformData.set(
177
+ [params.scale, params.warpScale, params.warpStrength, params.power],
178
+ 4
179
+ );
180
+ uniformData.set(
181
+ [params.detailScale, params.detailPower, params.ridgePower, params.heatBias],
182
+ 8
183
+ );
184
+ uniformData.set(
185
+ [params.moistureBias, mandel.scale, mandel.strength, mandel.rockBoost],
186
+ 12
187
+ );
188
+ uniformData.set(
189
+ [params.iterations, params.detailIterations, params.macroScale, params.macroWarpStrength],
190
+ 16
191
+ );
192
+ uniformData.set(
193
+ [
194
+ params.styleMixStrength,
195
+ params.terraceSteps,
196
+ params.terraceStrength,
197
+ params.craterStrength,
198
+ ],
199
+ 20
200
+ );
201
+ uniformData.set([params.craterScale, params.heightMin, params.heightMax, 0], 24);
202
+
203
+ device.queue.writeBuffer(uniformBuffer, 0, uniformData);
204
+
205
+ const encoder = device.createCommandEncoder();
206
+ const pass = encoder.beginComputePass();
207
+ pass.setPipeline(basePipeline);
208
+ pass.setBindGroup(0, baseBindGroup);
209
+ pass.dispatchWorkgroups(workgroups, workgroups);
210
+ pass.setPipeline(accentPipeline);
211
+ pass.setBindGroup(0, accentBindGroup);
212
+ pass.dispatchWorkgroups(workgroups, workgroups);
213
+ pass.end();
214
+ encoder.copyBufferToBuffer(accentBuffer, 0, readbackBuffer, 0, byteSize);
215
+ device.queue.submit([encoder.finish()]);
216
+
217
+ await readbackBuffer.mapAsync(GPUMapMode.READ);
218
+ const copy = readbackBuffer.getMappedRange();
219
+ const data = new Float32Array(copy.slice(0));
220
+ readbackBuffer.unmap();
221
+
222
+ return {
223
+ seed: runOptions.seed,
224
+ extent: runOptions.extent,
225
+ gridSize,
226
+ heightScale: runOptions.heightScale,
227
+ samples: data,
228
+ };
229
+ };
230
+
231
+ return {
232
+ gridSize,
233
+ gridPoints,
234
+ sampleCount,
235
+ run,
236
+ };
237
+ }