@threenative/terrain 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENT_GUIDE.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +71 -2
  4. package/THIRD_PARTY_NOTICES.md +38 -0
  5. package/dist/editor/index.d.ts +231 -0
  6. package/dist/editor/index.js +5624 -0
  7. package/dist/editor/server.d.ts +3 -0
  8. package/dist/editor/server.js +3688 -0
  9. package/dist/editor/worker.d.ts +2 -0
  10. package/dist/editor/worker.js +2212 -0
  11. package/dist/export.d.ts +54 -0
  12. package/dist/export.js +297 -0
  13. package/dist/index.d.ts +335 -0
  14. package/dist/index.js +2927 -0
  15. package/dist/server-ByM2hmYJ.d.ts +630 -0
  16. package/dist/terrain-gPnkCU6S.d.ts +141 -0
  17. package/dist/three.d.ts +14 -0
  18. package/dist/three.js +40 -0
  19. package/dist/types-Ant8-Axc.d.ts +486 -0
  20. package/package.json +86 -3
  21. package/starter/alpine/README.md +29 -0
  22. package/starter/alpine/assets.json +12 -0
  23. package/starter/alpine/bake.mjs +118 -0
  24. package/starter/alpine/recipe.json +66167 -0
  25. package/starter/alpine/sky.ts +23 -0
  26. package/starter/alpine/surface.json +53 -0
  27. package/starter/alpine/world.ts +335 -0
  28. package/starter/coastal/README.md +37 -0
  29. package/starter/coastal/assets.json +10 -0
  30. package/starter/coastal/bake.mjs +118 -0
  31. package/starter/coastal/recipe.json +66161 -0
  32. package/starter/coastal/sea.ts +327 -0
  33. package/starter/coastal/sky.ts +24 -0
  34. package/starter/coastal/surface.json +41 -0
  35. package/starter/coastal/world.ts +386 -0
  36. package/starter/desert/README.md +35 -0
  37. package/starter/desert/assets.json +12 -0
  38. package/starter/desert/bake.mjs +118 -0
  39. package/starter/desert/recipe.json +66156 -0
  40. package/starter/desert/sky.ts +23 -0
  41. package/starter/desert/surface.json +54 -0
  42. package/starter/desert/world.ts +335 -0
  43. package/starter/forest/README.md +46 -0
  44. package/starter/forest/assets.json +32 -0
  45. package/starter/forest/bake.mjs +118 -0
  46. package/starter/forest/recipe.json +4586 -0
  47. package/starter/forest/sky.ts +27 -0
  48. package/starter/forest/surface.json +79 -0
  49. package/starter/forest/water.ts +266 -0
  50. package/starter/forest/world.ts +413 -0
  51. package/starter/tundra/README.md +29 -0
  52. package/starter/tundra/assets.json +12 -0
  53. package/starter/tundra/bake.mjs +118 -0
  54. package/starter/tundra/recipe.json +66325 -0
  55. package/starter/tundra/sky.ts +27 -0
  56. package/starter/tundra/surface.json +54 -0
  57. package/starter/tundra/water.ts +313 -0
  58. package/starter/tundra/world.ts +366 -0
  59. package/starter-assets/bark_brown_02/bark_brown_02_diff_1k.jpg +0 -0
  60. package/starter-assets/bark_brown_02/bark_brown_02_nor_gl_1k.jpg +0 -0
  61. package/starter-assets/bark_brown_02/bark_brown_02_rough_1k.jpg +0 -0
  62. package/starter-assets/boulder_01/boulder-mid.glb +0 -0
  63. package/starter-assets/boulder_01/boulder-near.glb +0 -0
  64. package/starter-assets/cliff_side/cliff_side_ao_1k.jpg +0 -0
  65. package/starter-assets/cliff_side/cliff_side_diff_1k.jpg +0 -0
  66. package/starter-assets/cliff_side/cliff_side_nor_gl_1k.jpg +0 -0
  67. package/starter-assets/cliff_side/cliff_side_rough_1k.jpg +0 -0
  68. package/starter-assets/credits.json +1022 -0
  69. package/starter-assets/fern_02/fern_02_alpha_512.png +0 -0
  70. package/starter-assets/fern_02/fern_02_diff_512.jpg +0 -0
  71. package/starter-assets/fern_02/fern_02_nor_gl_512.jpg +0 -0
  72. package/starter-assets/fir_tree_01/fir-b-lod1.glb +0 -0
  73. package/starter-assets/fir_tree_01/fir-b-lod2.glb +0 -0
  74. package/starter-assets/fir_tree_01/fir-c-lod1.glb +0 -0
  75. package/starter-assets/fir_tree_01/fir-c-lod2.glb +0 -0
  76. package/starter-assets/fir_tree_01/fir_tree_01_twig_arm_1k.jpg +0 -0
  77. package/starter-assets/fir_tree_01/fir_tree_01_twig_diff_1k.jpg +0 -0
  78. package/starter-assets/fir_tree_01/fir_tree_01_twig_nor_gl_1k.jpg +0 -0
  79. package/starter-assets/forest_ground_04/forest_ground_04_ao_1k.jpg +0 -0
  80. package/starter-assets/forest_ground_04/forest_ground_04_diff_1k.jpg +0 -0
  81. package/starter-assets/forest_ground_04/forest_ground_04_nor_gl_1k.jpg +0 -0
  82. package/starter-assets/forest_ground_04/forest_ground_04_rough_1k.jpg +0 -0
  83. package/starter-assets/forest_leaves_02/forest_leaves_02_arm_512.jpg +0 -0
  84. package/starter-assets/forest_leaves_02/forest_leaves_02_diff_512.jpg +0 -0
  85. package/starter-assets/forest_leaves_02/forest_leaves_02_nor_gl_512.jpg +0 -0
  86. package/starter-assets/grass_medium_02/grass_medium_02_alpha_512.png +0 -0
  87. package/starter-assets/grass_medium_02/grass_medium_02_diff_512.jpg +0 -0
  88. package/starter-assets/grass_medium_02/grass_medium_02_nor_gl_512.jpg +0 -0
  89. package/starter-assets/hdri/kloofendal_48d_partly_cloudy_1k.hdr +0 -0
  90. package/starter-assets/leafy_grass/leafy_grass_ao_1k.jpg +0 -0
  91. package/starter-assets/leafy_grass/leafy_grass_diff_1k.jpg +0 -0
  92. package/starter-assets/leafy_grass/leafy_grass_nor_gl_1k.jpg +0 -0
  93. package/starter-assets/leafy_grass/leafy_grass_rough_1k.jpg +0 -0
  94. package/starter-assets/lichen_rock/lichen_rock_arm_512.jpg +0 -0
  95. package/starter-assets/lichen_rock/lichen_rock_diff_512.jpg +0 -0
  96. package/starter-assets/lichen_rock/lichen_rock_nor_gl_512.jpg +0 -0
  97. package/starter-assets/mossy_rock/mossy_rock_ao_1k.jpg +0 -0
  98. package/starter-assets/mossy_rock/mossy_rock_diff_1k.jpg +0 -0
  99. package/starter-assets/mossy_rock/mossy_rock_nor_gl_1k.jpg +0 -0
  100. package/starter-assets/mossy_rock/mossy_rock_rough_1k.jpg +0 -0
  101. package/starter-assets/needle-atlas.png +0 -0
  102. package/starter-assets/needle-surface.png +0 -0
  103. package/starter-assets/river_small_rocks/river_small_rocks_arm_512.jpg +0 -0
  104. package/starter-assets/river_small_rocks/river_small_rocks_diff_512.jpg +0 -0
  105. package/starter-assets/river_small_rocks/river_small_rocks_nor_gl_512.jpg +0 -0
  106. package/starter-assets/rock_boulder_dry/rock_boulder_dry_arm_512.jpg +0 -0
  107. package/starter-assets/rock_boulder_dry/rock_boulder_dry_diff_512.jpg +0 -0
  108. package/starter-assets/rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg +0 -0
  109. package/starter-assets/rocks/boulder-mid.glb +0 -0
  110. package/starter-assets/rocks/boulder-near.glb +0 -0
  111. package/starter-assets/rocks/boulder-prep.json +37 -0
  112. package/starter-assets/rocks/rock01-mid.glb +0 -0
  113. package/starter-assets/rocks/rock01-near.glb +0 -0
  114. package/starter-assets/rocks/rock01-prep.json +37 -0
  115. package/starter-assets/rocks/rock04-mid.glb +0 -0
  116. package/starter-assets/rocks/rock04-near.glb +0 -0
  117. package/starter-assets/rocks/rock04-prep.json +37 -0
  118. package/starter-assets/sand_01/sand_01_ao_1k.jpg +0 -0
  119. package/starter-assets/sand_01/sand_01_diff_1k.jpg +0 -0
  120. package/starter-assets/sand_01/sand_01_nor_gl_1k.jpg +0 -0
  121. package/starter-assets/sand_01/sand_01_rough_1k.jpg +0 -0
  122. package/starter-assets/snow_02/snow_02_ao_1k.jpg +0 -0
  123. package/starter-assets/snow_02/snow_02_diff_1k.jpg +0 -0
  124. package/starter-assets/snow_02/snow_02_nor_gl_1k.jpg +0 -0
  125. package/starter-assets/snow_02/snow_02_rough_1k.jpg +0 -0
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@threenative/terrain",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "description": "Headless terrain authoring — noise, erosion, sculpting, roads, rivers, material splats and scatter — evaluated into ordinary typed arrays and indexed Three.js geometry.",
6
+ "keywords": [
7
+ "threenative",
8
+ "terrain",
9
+ "heightfield",
10
+ "three",
11
+ "threejs",
12
+ "procedural",
13
+ "webgpu"
14
+ ],
15
+ "homepage": "https://github.com/ThreeNativeHQ/threenative#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/ThreeNativeHQ/threenative/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ThreeNativeHQ/threenative.git",
22
+ "directory": "packages/terrain"
23
+ },
24
+ "type": "module",
25
+ "files": [
26
+ "dist",
27
+ "starter",
28
+ "starter-assets",
29
+ "README.md",
30
+ "AGENT_GUIDE.md",
31
+ "THIRD_PARTY_NOTICES.md"
32
+ ],
33
+ "main": "./dist/index.js",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js"
40
+ },
41
+ "./three": {
42
+ "types": "./dist/three.d.ts",
43
+ "import": "./dist/three.js"
44
+ },
45
+ "./export": {
46
+ "types": "./dist/export.d.ts",
47
+ "import": "./dist/export.js"
48
+ },
49
+ "./package.json": "./package.json",
50
+ "./editor/server": {
51
+ "types": "./dist/editor/server.d.ts",
52
+ "import": "./dist/editor/server.js"
53
+ },
54
+ "./editor": {
55
+ "types": "./dist/editor/index.d.ts",
56
+ "import": "./dist/editor/index.js"
57
+ }
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "sideEffects": false,
63
+ "devDependencies": {
64
+ "@types/node": "20.19.43",
65
+ "@types/three": "0.185.3",
66
+ "publint": "0.3.22",
67
+ "three": "0.185.1",
68
+ "tsup": "8.5.1",
69
+ "typescript": "npm:@typescript/typescript6@6.0.2",
70
+ "@typescript/native": "npm:typescript@7.0.2",
71
+ "vite": "8.2.0",
72
+ "@threenative/core": "0.3.5",
73
+ "@threenative/physics": "0.3.5"
74
+ },
75
+ "peerDependencies": {
76
+ "three": ">=0.185.0 <0.186.0",
77
+ "vite": ">=7.0.0 <9.0.0"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "vite": {
81
+ "optional": true
82
+ }
83
+ },
84
+ "scripts": {
85
+ "build": "tsup --config tsup.config.ts && publint",
86
+ "typecheck": "tsc --noEmit",
87
+ "test": "pnpm run build && vitest run --root ../.. terrain/__tests__ && publint --strict"
88
+ }
6
89
  }
@@ -0,0 +1,29 @@
1
+ # Alpine starter kit
2
+
3
+ A 512 m alpine headwall on a real survey (USGS 1 m DEM, Longs Peak, Colorado), with hydraulic and
4
+ thermal erosion, rock base, dirt on slopes, snow on the high steep ground and boulder scree. It is
5
+ game source: once copied, every file is yours to edit.
6
+
7
+ | File | What it decides |
8
+ | --- | --- |
9
+ | `recipe.json` | The terrain: the survey heightmap, erosion, the rock, dirt and snow surfaces, and the scree. |
10
+ | `assets.json` | The boulder model, its LOD distance and draw distance, and its bounds. |
11
+ | `surface.json` | The ground: the rock base, the dirt and snow layers, tiles, tints and blend thresholds. |
12
+ | `bake.mjs` | Writes the world package from the three files above. |
13
+ | `world.ts` | The runtime: streams the baked world, one heightfield for the ground, sphere colliders for the boulders near the player, and the sky as the props' light. |
14
+ | `sky.ts` | The daylight: the sun, sky, fill, haze, exposure and tone curve. |
15
+
16
+ Art is CC0 (Poly Haven); provenance is in `@threenative/terrain/starter-assets/credits.json`.
17
+
18
+ ## Status
19
+
20
+ The kit bakes and streams. The kit ships no scene: the game writes one. The terrain is too steep to
21
+ stand on near its centre, so a scene picks its spawn from the loaded heightfield. The proof's scene
22
+ rings the nearest boulder with gentle ground and a clear walk.
23
+
24
+ ## Steps
25
+
26
+ 1. Copy `node_modules/@threenative/terrain/starter/alpine` to `src/terrain/alpine`.
27
+ 2. Run `node src/terrain/alpine/bake.mjs --out assets/terrain/alpine` (needs `@threenative/terrain`
28
+ as a devDependency). The same recipe always writes the same bytes.
29
+ 3. In the scene, call `addAlpine(ctx, follow)` for the world and `alpineDaylight(follow)` for the light.
@@ -0,0 +1,12 @@
1
+ {
2
+ "boulder": {
3
+ "near": "boulder_01/boulder-near.glb",
4
+ "mid": "boulder_01/boulder-mid.glb",
5
+ "midDistance": 35,
6
+ "maxDistance": 300,
7
+ "bounds": {
8
+ "min": [-0.716, 0, -0.991],
9
+ "max": [0.557, 1.004, 0.841]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,118 @@
1
+ // Bakes this folder's recipe into a self-contained world package. Run it once from your game
2
+ // project, then serve the folder it writes:
3
+ // node src/terrain/alpine/bake.mjs [--assets <dir>] [--out <dir>]
4
+ // The engine's `WorldCells` streams `world/world.json` and `loadTerrainSplat` textures its ground;
5
+ // the game never evaluates terrain at runtime.
6
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { Terrain, applyPlacementOverrides, bakeWorldPackage } from "@threenative/terrain";
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+
13
+ /** `--key value` after the script; `--assets` and `--out` default as noted below. */
14
+ function flag(name, fallback) {
15
+ const at = process.argv.indexOf(`--${name}`);
16
+ return at === -1 ? fallback : resolve(process.argv[at + 1] ?? "");
17
+ }
18
+
19
+ // The installed kit ships its models and textures beside the package manifest, so a copied kit
20
+ // finds them without asking the game for a path. `--assets` overrides that for a local checkout.
21
+ const installed = dirname(fileURLToPath(import.meta.resolve("@threenative/terrain/package.json")));
22
+ const assetsDir = flag("assets", join(installed, "starter-assets"));
23
+ const outDir = flag("out", join(here, "world"));
24
+
25
+ const recipe = JSON.parse(await readFile(join(here, "recipe.json"), "utf8"));
26
+ const assetTable = JSON.parse(await readFile(join(here, "assets.json"), "utf8"));
27
+ const surface = JSON.parse(await readFile(join(here, "surface.json"), "utf8"));
28
+
29
+ const state = applyPlacementOverrides(
30
+ Terrain.fromJSON(recipe.recipe).evaluate(),
31
+ recipe.placementOverrides ?? {},
32
+ );
33
+
34
+ const assets = Object.fromEntries(
35
+ Object.entries(assetTable).map(([id, spec]) => [
36
+ id,
37
+ {
38
+ bounds: spec.bounds,
39
+ glb: `models/${spec.near.split("/").at(-1)}`,
40
+ ...(spec.mid
41
+ ? { lods: [{ distance: spec.midDistance, glb: `models/${spec.mid.split("/").at(-1)}` }] }
42
+ : {}),
43
+ maxDistance: spec.maxDistance,
44
+ },
45
+ ]),
46
+ );
47
+
48
+ const started = performance.now();
49
+ const { manifest, splat, files } = bakeWorldPackage(state, {
50
+ assets,
51
+ layers: { table: "terrain-table.json" },
52
+ });
53
+ const written = new Map();
54
+
55
+ async function write(relative, bytes) {
56
+ await mkdir(join(outDir, dirname(relative)), { recursive: true });
57
+ await writeFile(join(outDir, relative), bytes);
58
+ written.set(relative, bytes.length ?? bytes.byteLength);
59
+ }
60
+
61
+ async function copy(from, to) {
62
+ await mkdir(join(outDir, dirname(to)), { recursive: true });
63
+ await copyFile(join(assetsDir, from), join(outDir, to));
64
+ written.set(to, (await readFile(join(outDir, to))).byteLength);
65
+ }
66
+
67
+ await write("world.json", `${JSON.stringify(manifest, null, 2)}\n`);
68
+ // The lake and river the generator evaluated, so the game draws the water the bake shaped.
69
+ const round = (value) => Math.round(value * 1_000) / 1_000;
70
+ await write(
71
+ "water.json",
72
+ `${JSON.stringify(
73
+ {
74
+ lakes: state.waters.map((lake) => ({
75
+ id: lake.id,
76
+ at: lake.at,
77
+ radius: lake.radius,
78
+ level: lake.level,
79
+ })),
80
+ rivers: state.rivers.map((river) => ({
81
+ id: river.id,
82
+ width: round(river.width),
83
+ points: river.points.map((point) => point.map(round)),
84
+ })),
85
+ },
86
+ null,
87
+ 2,
88
+ )}\n`,
89
+ );
90
+ for (const [name, bytes] of Object.entries(files)) await write(name, bytes);
91
+ for (const spec of Object.values(assetTable)) {
92
+ await copy(spec.near, `models/${spec.near.split("/").at(-1)}`);
93
+ if (spec.mid) await copy(spec.mid, `models/${spec.mid.split("/").at(-1)}`);
94
+ }
95
+ // The sky the props are lit by (CC0 Poly Haven Kloofendal); world.ts gives it to their materials.
96
+ await copy("hdri/kloofendal_48d_partly_cloudy_1k.hdr", "sky.hdr");
97
+ for (const layer of [surface.base, ...surface.layers]) {
98
+ const source = surface.sources[layer.id];
99
+ await copy(source.diff, `${surface.textures}/${layer.id}_diff.jpg`);
100
+ await copy(source.nrm, `${surface.textures}/${layer.id}_nrm.jpg`);
101
+ }
102
+ // `sources` is the bake's own lookup: the runtime reads only base/layers/splat/textures.
103
+ await write(
104
+ "terrain-table.json",
105
+ `${JSON.stringify({ ...surface, splat, textures: "textures" }, null, 2)}\n`,
106
+ );
107
+
108
+ const perAsset = {};
109
+ for (const cell of manifest.cells)
110
+ for (const run of cell.runs) perAsset[run.asset] = (perAsset[run.asset] ?? 0) + run.count;
111
+ const total = [...written.values()].reduce((sum, bytes) => sum + bytes, 0);
112
+ console.log(
113
+ `${Object.entries(perAsset)
114
+ .map(([asset, count]) => `${asset} ${String(count)}`)
115
+ .join(", ")} placements; ${[...written]
116
+ .map(([name, bytes]) => `${name} ${String(bytes)} B`)
117
+ .join(", ")}; ${String(total)} B total; ${Math.round(performance.now() - started)} ms`,
118
+ );