@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
@@ -0,0 +1,274 @@
1
+ import type { TileAsset, TileAssetPayload, TileBakeWriter, TileKey } from "./tiles";
2
+ import { bakeTileAsset } from "./tiles";
3
+
4
+ export type TileCacheStatus = "pending" | "ready" | "error";
5
+
6
+ export interface TileCacheEntry {
7
+ key: TileKey;
8
+ status: TileCacheStatus;
9
+ asset?: TileAsset;
10
+ payload?: TileAssetPayload;
11
+ binary?: ArrayBuffer;
12
+ bytes: number;
13
+ lastAccess: number;
14
+ error?: unknown;
15
+ }
16
+
17
+ export type TileGenerator = (key: TileKey) => TileAsset | Promise<TileAsset>;
18
+
19
+ export interface TileCacheOptions {
20
+ maxEntries?: number;
21
+ maxBytes?: number;
22
+ keepBinary?: boolean;
23
+ keepJson?: boolean;
24
+ writer?: TileBakeWriter;
25
+ now?: () => number;
26
+ onEvict?: (entry: TileCacheEntry) => void;
27
+ }
28
+
29
+ export const DEFAULT_TILE_SIZE_WORLD = 64;
30
+
31
+ export function normalizeTileKey(key: TileKey): TileKey {
32
+ const tileSizeWorld =
33
+ key.tileSizeWorld !== undefined && Number.isFinite(key.tileSizeWorld)
34
+ ? Number(key.tileSizeWorld)
35
+ : undefined;
36
+ return {
37
+ seed: key.seed >>> 0,
38
+ tx: Math.trunc(key.tx),
39
+ tz: Math.trunc(key.tz),
40
+ level: Math.max(0, Math.trunc(key.level)),
41
+ tileSizeWorld,
42
+ };
43
+ }
44
+
45
+ export function tileKeyToString(key: TileKey): string {
46
+ const normalized = normalizeTileKey(key);
47
+ const tileSize =
48
+ normalized.tileSizeWorld === undefined
49
+ ? "default"
50
+ : normalized.tileSizeWorld.toFixed(3);
51
+ return [
52
+ normalized.seed,
53
+ normalized.level,
54
+ normalized.tx,
55
+ normalized.tz,
56
+ tileSize,
57
+ ].join(":");
58
+ }
59
+
60
+ export function resolveTileSizeWorld(
61
+ key: TileKey,
62
+ defaultTileSizeWorld = DEFAULT_TILE_SIZE_WORLD
63
+ ) {
64
+ return key.tileSizeWorld ?? defaultTileSizeWorld;
65
+ }
66
+
67
+ export function tileBoundsWorld(
68
+ key: TileKey,
69
+ defaultTileSizeWorld = DEFAULT_TILE_SIZE_WORLD
70
+ ) {
71
+ const size = resolveTileSizeWorld(key, defaultTileSizeWorld);
72
+ const minX = key.tx * size;
73
+ const minZ = key.tz * size;
74
+ return {
75
+ minX,
76
+ minZ,
77
+ maxX: minX + size,
78
+ maxZ: minZ + size,
79
+ centerX: minX + size * 0.5,
80
+ centerZ: minZ + size * 0.5,
81
+ size,
82
+ };
83
+ }
84
+
85
+ export function tileKeyFromWorldPosition(options: {
86
+ seed: number;
87
+ x: number;
88
+ z: number;
89
+ level?: number;
90
+ tileSizeWorld?: number;
91
+ defaultTileSizeWorld?: number;
92
+ }): TileKey {
93
+ const size =
94
+ options.tileSizeWorld ?? options.defaultTileSizeWorld ?? DEFAULT_TILE_SIZE_WORLD;
95
+ const tx = Math.floor(options.x / size);
96
+ const tz = Math.floor(options.z / size);
97
+ return {
98
+ seed: options.seed,
99
+ tx,
100
+ tz,
101
+ level: options.level ?? 0,
102
+ tileSizeWorld: options.tileSizeWorld,
103
+ };
104
+ }
105
+
106
+ export function tileAssetFileStem(key: TileKey): string {
107
+ const normalized = normalizeTileKey(key);
108
+ const suffix =
109
+ normalized.tileSizeWorld === undefined
110
+ ? ""
111
+ : `-ts${normalized.tileSizeWorld.toFixed(3)}`;
112
+ return `tile-${normalized.seed}-${normalized.level}-${normalized.tx}-${normalized.tz}${suffix}`;
113
+ }
114
+
115
+ function estimateAssetBytes(asset: TileAsset, payload?: TileAssetPayload, binary?: ArrayBuffer) {
116
+ let bytes = asset.height.length * 4;
117
+ if (asset.fields) bytes += asset.fields.length * 4;
118
+ if (asset.materials) bytes += asset.materials.length;
119
+ if (asset.features) bytes += asset.features.length * 4;
120
+ if (binary) bytes += binary.byteLength;
121
+ if (payload) {
122
+ // Rough estimate of JSON footprint to help eviction logic.
123
+ bytes += payload.height.length * 4;
124
+ }
125
+ return bytes;
126
+ }
127
+
128
+ export class TileCache {
129
+ private entries = new Map<string, TileCacheEntry>();
130
+ private inflight = new Map<string, Promise<TileCacheEntry>>();
131
+ private totalBytes = 0;
132
+ private options: Required<Omit<TileCacheOptions, "writer">> & {
133
+ writer?: TileBakeWriter;
134
+ };
135
+
136
+ constructor(options: TileCacheOptions = {}) {
137
+ this.options = {
138
+ maxEntries: options.maxEntries ?? 128,
139
+ maxBytes: options.maxBytes ?? 128 * 1024 * 1024,
140
+ keepBinary: options.keepBinary ?? true,
141
+ keepJson: options.keepJson ?? true,
142
+ writer: options.writer,
143
+ now: options.now ?? (() => Date.now()),
144
+ onEvict: options.onEvict ?? (() => {}),
145
+ };
146
+ }
147
+
148
+ getStats() {
149
+ return {
150
+ entries: this.entries.size,
151
+ bytes: this.totalBytes,
152
+ inflight: this.inflight.size,
153
+ };
154
+ }
155
+
156
+ has(key: TileKey) {
157
+ return this.entries.has(tileKeyToString(key));
158
+ }
159
+
160
+ get(key: TileKey): TileCacheEntry | undefined {
161
+ const id = tileKeyToString(key);
162
+ const entry = this.entries.get(id);
163
+ if (entry) {
164
+ entry.lastAccess = this.options.now();
165
+ }
166
+ return entry;
167
+ }
168
+
169
+ async getOrCreate(key: TileKey, generator: TileGenerator): Promise<TileCacheEntry> {
170
+ const normalized = normalizeTileKey(key);
171
+ const id = tileKeyToString(normalized);
172
+ const cached = this.entries.get(id);
173
+ if (cached && cached.status === "ready") {
174
+ cached.lastAccess = this.options.now();
175
+ return cached;
176
+ }
177
+ const inflight = this.inflight.get(id);
178
+ if (inflight) {
179
+ return inflight;
180
+ }
181
+
182
+ const entry: TileCacheEntry = {
183
+ key: normalized,
184
+ status: "pending",
185
+ bytes: 0,
186
+ lastAccess: this.options.now(),
187
+ };
188
+ this.entries.set(id, entry);
189
+
190
+ const promise = (async () => {
191
+ try {
192
+ const asset = await generator(normalized);
193
+ const baked = await bakeTileAsset(asset, this.options.writer);
194
+
195
+ const payload = this.options.keepJson ? baked.payload : undefined;
196
+ const binary = this.options.keepBinary ? baked.binary : undefined;
197
+ const bytes = estimateAssetBytes(asset, payload, binary);
198
+
199
+ const ready: TileCacheEntry = {
200
+ key: normalized,
201
+ status: "ready",
202
+ asset,
203
+ payload,
204
+ binary,
205
+ bytes,
206
+ lastAccess: this.options.now(),
207
+ };
208
+ this.replaceEntry(id, ready);
209
+ this.inflight.delete(id);
210
+ this.evictIfNeeded();
211
+ return ready;
212
+ } catch (error) {
213
+ const failed: TileCacheEntry = {
214
+ key: normalized,
215
+ status: "error",
216
+ bytes: 0,
217
+ lastAccess: this.options.now(),
218
+ error,
219
+ };
220
+ this.replaceEntry(id, failed);
221
+ this.inflight.delete(id);
222
+ return failed;
223
+ }
224
+ })();
225
+
226
+ this.inflight.set(id, promise);
227
+ return promise;
228
+ }
229
+
230
+ delete(key: TileKey) {
231
+ const id = tileKeyToString(key);
232
+ const entry = this.entries.get(id);
233
+ if (!entry) return false;
234
+ this.entries.delete(id);
235
+ this.totalBytes -= entry.bytes;
236
+ this.options.onEvict(entry);
237
+ return true;
238
+ }
239
+
240
+ private replaceEntry(id: string, entry: TileCacheEntry) {
241
+ const existing = this.entries.get(id);
242
+ if (existing) {
243
+ this.totalBytes -= existing.bytes;
244
+ }
245
+ this.entries.set(id, entry);
246
+ this.totalBytes += entry.bytes;
247
+ }
248
+
249
+ private evictIfNeeded() {
250
+ const { maxEntries, maxBytes } = this.options;
251
+ while (this.entries.size > maxEntries || this.totalBytes > maxBytes) {
252
+ let oldestId: string | null = null;
253
+ let oldestTime = Infinity;
254
+ for (const [id, entry] of this.entries) {
255
+ if (entry.status !== "ready") continue;
256
+ if (entry.lastAccess < oldestTime) {
257
+ oldestTime = entry.lastAccess;
258
+ oldestId = id;
259
+ }
260
+ }
261
+ if (!oldestId) {
262
+ break;
263
+ }
264
+ const entry = this.entries.get(oldestId);
265
+ if (entry) {
266
+ this.entries.delete(oldestId);
267
+ this.totalBytes -= entry.bytes;
268
+ this.options.onEvict(entry);
269
+ } else {
270
+ break;
271
+ }
272
+ }
273
+ }
274
+ }
package/src/tiles.ts ADDED
@@ -0,0 +1,417 @@
1
+ export type TileKey = {
2
+ seed: number;
3
+ tx: number;
4
+ tz: number;
5
+ level: number;
6
+ tileSizeWorld?: number;
7
+ };
8
+
9
+ export interface TileAsset {
10
+ key: TileKey;
11
+ gridSize: number;
12
+ heightScale: number;
13
+ height: Float32Array;
14
+ fields?: Float32Array;
15
+ fieldStride?: number;
16
+ materials?: Uint8Array;
17
+ materialStride?: number;
18
+ features?: Float32Array;
19
+ featureStride?: number;
20
+ }
21
+
22
+ export interface TileAssetPayload {
23
+ version: number;
24
+ key: TileKey;
25
+ gridSize: number;
26
+ heightScale: number;
27
+ height: number[];
28
+ fields?: number[];
29
+ fieldStride?: number;
30
+ materials?: number[];
31
+ materialStride?: number;
32
+ features?: number[];
33
+ featureStride?: number;
34
+ }
35
+
36
+ export const TILE_ASSET_VERSION = 1;
37
+
38
+ const MAGIC_BYTES = [0x54, 0x57, 0x4c, 0x44]; // "TWLD"
39
+ const HEADER_BYTES = 68;
40
+
41
+ const FLAG_HAS_TILE_SIZE = 1 << 0;
42
+ const FLAG_HAS_FIELDS = 1 << 1;
43
+ const FLAG_HAS_MATERIALS = 1 << 2;
44
+ const FLAG_HAS_FEATURES = 1 << 3;
45
+
46
+ function align4(offset: number) {
47
+ return (offset + 3) & ~3;
48
+ }
49
+
50
+ function expectedHeightCount(gridSize: number) {
51
+ const gridPoints = gridSize + 1;
52
+ return gridPoints * gridPoints;
53
+ }
54
+
55
+ export function validateTileAssetPayload(payload: TileAssetPayload): string[] {
56
+ const errors: string[] = [];
57
+ if (!payload || typeof payload !== "object") {
58
+ return ["payload must be an object"];
59
+ }
60
+ const key = payload.key as TileKey;
61
+ if (!key || typeof key !== "object") {
62
+ errors.push("key is required");
63
+ } else {
64
+ const keyFields: Array<keyof TileKey> = ["seed", "tx", "tz", "level"];
65
+ for (const field of keyFields) {
66
+ const value = key[field];
67
+ if (!Number.isFinite(value)) {
68
+ errors.push(`key.${field} must be a number`);
69
+ }
70
+ }
71
+ if (key.tileSizeWorld !== undefined && !Number.isFinite(key.tileSizeWorld)) {
72
+ errors.push("key.tileSizeWorld must be a number when provided");
73
+ }
74
+ }
75
+
76
+ if (!Number.isFinite(payload.gridSize) || payload.gridSize < 1) {
77
+ errors.push("gridSize must be a positive number");
78
+ }
79
+ if (!Number.isFinite(payload.heightScale)) {
80
+ errors.push("heightScale must be a number");
81
+ }
82
+ if (!Array.isArray(payload.height)) {
83
+ errors.push("height must be an array");
84
+ } else {
85
+ const expected = expectedHeightCount(payload.gridSize);
86
+ if (payload.height.length !== expected) {
87
+ errors.push(`height length must be ${expected} for gridSize ${payload.gridSize}`);
88
+ }
89
+ }
90
+
91
+ const materialStride =
92
+ payload.materialStride ?? (payload.materials ? 1 : 0);
93
+ if (payload.materials) {
94
+ if (!Number.isFinite(materialStride) || materialStride <= 0) {
95
+ errors.push("materialStride must be > 0 when materials are provided");
96
+ } else if (payload.materials.length % materialStride !== 0) {
97
+ errors.push("materials length must be divisible by materialStride");
98
+ }
99
+ }
100
+
101
+ if (payload.fields) {
102
+ if (!Number.isFinite(payload.fieldStride) || (payload.fieldStride ?? 0) <= 0) {
103
+ errors.push("fieldStride must be > 0 when fields are provided");
104
+ } else if (payload.fields.length % payload.fieldStride! !== 0) {
105
+ errors.push("fields length must be divisible by fieldStride");
106
+ }
107
+ }
108
+
109
+ if (payload.features) {
110
+ if (!Number.isFinite(payload.featureStride) || (payload.featureStride ?? 0) <= 0) {
111
+ errors.push("featureStride must be > 0 when features are provided");
112
+ } else if (payload.features.length % payload.featureStride! !== 0) {
113
+ errors.push("features length must be divisible by featureStride");
114
+ }
115
+ }
116
+
117
+ return errors;
118
+ }
119
+
120
+ export function serializeTileAssetJson(asset: TileAsset): TileAssetPayload {
121
+ return {
122
+ version: TILE_ASSET_VERSION,
123
+ key: asset.key,
124
+ gridSize: asset.gridSize,
125
+ heightScale: asset.heightScale,
126
+ height: Array.from(asset.height),
127
+ fields: asset.fields ? Array.from(asset.fields) : undefined,
128
+ fieldStride: asset.fieldStride,
129
+ materials: asset.materials ? Array.from(asset.materials) : undefined,
130
+ materialStride: asset.materialStride ?? (asset.materials ? 1 : undefined),
131
+ features: asset.features ? Array.from(asset.features) : undefined,
132
+ featureStride: asset.featureStride,
133
+ };
134
+ }
135
+
136
+ export function parseTileAssetJson(payload: TileAssetPayload): TileAsset {
137
+ const errors = validateTileAssetPayload(payload);
138
+ if (errors.length) {
139
+ throw new Error(`Invalid tile asset payload: ${errors.join("; ")}`);
140
+ }
141
+
142
+ const materialStride =
143
+ payload.materialStride ?? (payload.materials ? 1 : undefined);
144
+
145
+ return {
146
+ key: payload.key,
147
+ gridSize: payload.gridSize,
148
+ heightScale: payload.heightScale,
149
+ height: Float32Array.from(payload.height),
150
+ fields: payload.fields ? Float32Array.from(payload.fields) : undefined,
151
+ fieldStride: payload.fieldStride,
152
+ materials: payload.materials ? Uint8Array.from(payload.materials) : undefined,
153
+ materialStride,
154
+ features: payload.features ? Float32Array.from(payload.features) : undefined,
155
+ featureStride: payload.featureStride,
156
+ };
157
+ }
158
+
159
+ export interface TileBakeOutput {
160
+ asset: TileAsset;
161
+ payload: TileAssetPayload;
162
+ binary: ArrayBuffer;
163
+ }
164
+
165
+ export interface TileBakeWriter {
166
+ writeJson?: (payload: TileAssetPayload, asset: TileAsset) => void | Promise<void>;
167
+ writeBinary?: (
168
+ binary: ArrayBuffer,
169
+ asset: TileAsset,
170
+ payload: TileAssetPayload
171
+ ) => void | Promise<void>;
172
+ }
173
+
174
+ export async function bakeTileAsset(
175
+ asset: TileAsset,
176
+ writer?: TileBakeWriter
177
+ ): Promise<TileBakeOutput> {
178
+ const payload = serializeTileAssetJson(asset);
179
+ const errors = validateTileAssetPayload(payload);
180
+ if (errors.length) {
181
+ throw new Error(`Invalid tile asset payload: ${errors.join("; ")}`);
182
+ }
183
+ if (writer?.writeJson) {
184
+ await writer.writeJson(payload, asset);
185
+ }
186
+ const binary = serializeTileAssetBinaryFromJson(payload);
187
+ if (writer?.writeBinary) {
188
+ await writer.writeBinary(binary, asset, payload);
189
+ }
190
+ return { asset, payload, binary };
191
+ }
192
+
193
+ export function serializeTileAssetBinary(asset: TileAsset): ArrayBuffer {
194
+ const heightCount = asset.height.length;
195
+ const expected = expectedHeightCount(asset.gridSize);
196
+ if (heightCount !== expected) {
197
+ throw new Error(`height length must be ${expected} for gridSize ${asset.gridSize}`);
198
+ }
199
+
200
+ const fieldStride = asset.fieldStride ?? 0;
201
+ if (asset.fields && fieldStride <= 0) {
202
+ throw new Error("fieldStride must be provided when fields exist");
203
+ }
204
+ const materialStride = asset.materialStride ?? (asset.materials ? 1 : 0);
205
+ if (asset.materials && materialStride <= 0) {
206
+ throw new Error("materialStride must be provided when materials exist");
207
+ }
208
+ const featureStride = asset.featureStride ?? 0;
209
+ if (asset.features && featureStride <= 0) {
210
+ throw new Error("featureStride must be provided when features exist");
211
+ }
212
+
213
+ const fieldCount = asset.fields ? asset.fields.length : 0;
214
+ const materialCount = asset.materials ? asset.materials.length : 0;
215
+ const featureCount = asset.features ? asset.features.length : 0;
216
+
217
+ const heightBytes = heightCount * 4;
218
+ const fieldBytes = fieldCount * 4;
219
+ const materialBytes = materialCount;
220
+ const featureBytes = featureCount * 4;
221
+
222
+ let offset = HEADER_BYTES + heightBytes + fieldBytes + materialBytes;
223
+ offset = align4(offset);
224
+ const totalBytes = offset + featureBytes;
225
+
226
+ const buffer = new ArrayBuffer(totalBytes);
227
+ const view = new DataView(buffer);
228
+ const u8 = new Uint8Array(buffer);
229
+
230
+ u8.set(MAGIC_BYTES, 0);
231
+ let cursor = 4;
232
+
233
+ view.setUint32(cursor, TILE_ASSET_VERSION, true);
234
+ cursor += 4;
235
+
236
+ let flags = 0;
237
+ if (asset.key.tileSizeWorld !== undefined) flags |= FLAG_HAS_TILE_SIZE;
238
+ if (fieldCount) flags |= FLAG_HAS_FIELDS;
239
+ if (materialCount) flags |= FLAG_HAS_MATERIALS;
240
+ if (featureCount) flags |= FLAG_HAS_FEATURES;
241
+ view.setUint32(cursor, flags, true);
242
+ cursor += 4;
243
+
244
+ view.setInt32(cursor, asset.key.tx | 0, true);
245
+ cursor += 4;
246
+ view.setInt32(cursor, asset.key.tz | 0, true);
247
+ cursor += 4;
248
+ view.setUint32(cursor, asset.key.level >>> 0, true);
249
+ cursor += 4;
250
+ view.setUint32(cursor, asset.key.seed >>> 0, true);
251
+ cursor += 4;
252
+ view.setFloat32(cursor, asset.key.tileSizeWorld ?? 0, true);
253
+ cursor += 4;
254
+
255
+ view.setUint32(cursor, asset.gridSize >>> 0, true);
256
+ cursor += 4;
257
+ view.setFloat32(cursor, asset.heightScale, true);
258
+ cursor += 4;
259
+
260
+ view.setUint32(cursor, heightCount >>> 0, true);
261
+ cursor += 4;
262
+ view.setUint32(cursor, fieldCount >>> 0, true);
263
+ cursor += 4;
264
+ view.setUint32(cursor, materialCount >>> 0, true);
265
+ cursor += 4;
266
+ view.setUint32(cursor, featureCount >>> 0, true);
267
+ cursor += 4;
268
+ view.setUint32(cursor, fieldStride >>> 0, true);
269
+ cursor += 4;
270
+ view.setUint32(cursor, materialStride >>> 0, true);
271
+ cursor += 4;
272
+ view.setUint32(cursor, featureStride >>> 0, true);
273
+ cursor += 4;
274
+
275
+ let writeOffset = HEADER_BYTES;
276
+ new Float32Array(buffer, writeOffset, heightCount).set(asset.height);
277
+ writeOffset += heightBytes;
278
+
279
+ if (fieldCount) {
280
+ new Float32Array(buffer, writeOffset, fieldCount).set(asset.fields!);
281
+ writeOffset += fieldBytes;
282
+ }
283
+
284
+ if (materialCount) {
285
+ new Uint8Array(buffer, writeOffset, materialCount).set(asset.materials!);
286
+ writeOffset += materialBytes;
287
+ }
288
+
289
+ writeOffset = align4(writeOffset);
290
+
291
+ if (featureCount) {
292
+ new Float32Array(buffer, writeOffset, featureCount).set(asset.features!);
293
+ }
294
+
295
+ return buffer;
296
+ }
297
+
298
+ export function parseTileAssetBinary(input: ArrayBuffer | ArrayBufferView): TileAsset {
299
+ const buffer =
300
+ input instanceof ArrayBuffer
301
+ ? input
302
+ : input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
303
+
304
+ if (buffer.byteLength < HEADER_BYTES) {
305
+ throw new Error("Buffer too small for tile asset header");
306
+ }
307
+
308
+ const u8 = new Uint8Array(buffer);
309
+ for (let i = 0; i < MAGIC_BYTES.length; i += 1) {
310
+ if (u8[i] !== MAGIC_BYTES[i]) {
311
+ throw new Error("Invalid tile asset magic header");
312
+ }
313
+ }
314
+
315
+ const view = new DataView(buffer);
316
+ let cursor = 4;
317
+ const version = view.getUint32(cursor, true);
318
+ cursor += 4;
319
+ if (version !== TILE_ASSET_VERSION) {
320
+ throw new Error(`Unsupported tile asset version ${version}`);
321
+ }
322
+
323
+ const flags = view.getUint32(cursor, true);
324
+ cursor += 4;
325
+ const tx = view.getInt32(cursor, true);
326
+ cursor += 4;
327
+ const tz = view.getInt32(cursor, true);
328
+ cursor += 4;
329
+ const level = view.getUint32(cursor, true);
330
+ cursor += 4;
331
+ const seed = view.getUint32(cursor, true);
332
+ cursor += 4;
333
+ const tileSizeWorld = view.getFloat32(cursor, true);
334
+ cursor += 4;
335
+
336
+ const gridSize = view.getUint32(cursor, true);
337
+ cursor += 4;
338
+ const heightScale = view.getFloat32(cursor, true);
339
+ cursor += 4;
340
+
341
+ const heightCount = view.getUint32(cursor, true);
342
+ cursor += 4;
343
+ const fieldCount = view.getUint32(cursor, true);
344
+ cursor += 4;
345
+ const materialCount = view.getUint32(cursor, true);
346
+ cursor += 4;
347
+ const featureCount = view.getUint32(cursor, true);
348
+ cursor += 4;
349
+ const fieldStride = view.getUint32(cursor, true);
350
+ cursor += 4;
351
+ const materialStride = view.getUint32(cursor, true);
352
+ cursor += 4;
353
+ const featureStride = view.getUint32(cursor, true);
354
+ cursor += 4;
355
+
356
+ const heightBytes = heightCount * 4;
357
+ const fieldBytes = fieldCount * 4;
358
+ const materialBytes = materialCount;
359
+ const featureBytes = featureCount * 4;
360
+ let required = HEADER_BYTES + heightBytes + fieldBytes + materialBytes;
361
+ required = align4(required) + featureBytes;
362
+ if (buffer.byteLength < required) {
363
+ throw new Error("Tile asset buffer is truncated");
364
+ }
365
+
366
+ let readOffset = HEADER_BYTES;
367
+ const height = new Float32Array(buffer, readOffset, heightCount);
368
+ readOffset += heightCount * 4;
369
+
370
+ const fields = fieldCount
371
+ ? new Float32Array(buffer, readOffset, fieldCount)
372
+ : undefined;
373
+ readOffset += fieldCount * 4;
374
+
375
+ const materials = materialCount
376
+ ? new Uint8Array(buffer, readOffset, materialCount)
377
+ : undefined;
378
+ readOffset += materialCount;
379
+ readOffset = align4(readOffset);
380
+
381
+ const features = featureCount
382
+ ? new Float32Array(buffer, readOffset, featureCount)
383
+ : undefined;
384
+
385
+ const key: TileKey = {
386
+ seed,
387
+ tx,
388
+ tz,
389
+ level,
390
+ };
391
+ if (flags & FLAG_HAS_TILE_SIZE) {
392
+ key.tileSizeWorld = tileSizeWorld;
393
+ }
394
+
395
+ return {
396
+ key,
397
+ gridSize,
398
+ heightScale,
399
+ height,
400
+ fields,
401
+ fieldStride: fieldCount ? fieldStride : undefined,
402
+ materials,
403
+ materialStride: materialCount ? materialStride : undefined,
404
+ features,
405
+ featureStride: featureCount ? featureStride : undefined,
406
+ };
407
+ }
408
+
409
+ export function serializeTileAssetBinaryFromJson(payload: TileAssetPayload): ArrayBuffer {
410
+ return serializeTileAssetBinary(parseTileAssetJson(payload));
411
+ }
412
+
413
+ export function serializeTileAssetJsonFromBinary(
414
+ input: ArrayBuffer | ArrayBufferView
415
+ ): TileAssetPayload {
416
+ return serializeTileAssetJson(parseTileAssetBinary(input));
417
+ }