@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.
- package/AGENT_GUIDE.md +194 -0
- package/LICENSE +21 -0
- package/README.md +71 -2
- package/THIRD_PARTY_NOTICES.md +38 -0
- package/dist/editor/index.d.ts +231 -0
- package/dist/editor/index.js +5624 -0
- package/dist/editor/server.d.ts +3 -0
- package/dist/editor/server.js +3688 -0
- package/dist/editor/worker.d.ts +2 -0
- package/dist/editor/worker.js +2212 -0
- package/dist/export.d.ts +54 -0
- package/dist/export.js +297 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +2927 -0
- package/dist/server-ByM2hmYJ.d.ts +630 -0
- package/dist/terrain-gPnkCU6S.d.ts +141 -0
- package/dist/three.d.ts +14 -0
- package/dist/three.js +40 -0
- package/dist/types-Ant8-Axc.d.ts +486 -0
- package/package.json +86 -3
- package/starter/alpine/README.md +29 -0
- package/starter/alpine/assets.json +12 -0
- package/starter/alpine/bake.mjs +118 -0
- package/starter/alpine/recipe.json +66167 -0
- package/starter/alpine/sky.ts +23 -0
- package/starter/alpine/surface.json +53 -0
- package/starter/alpine/world.ts +335 -0
- package/starter/coastal/README.md +37 -0
- package/starter/coastal/assets.json +10 -0
- package/starter/coastal/bake.mjs +118 -0
- package/starter/coastal/recipe.json +66161 -0
- package/starter/coastal/sea.ts +327 -0
- package/starter/coastal/sky.ts +24 -0
- package/starter/coastal/surface.json +41 -0
- package/starter/coastal/world.ts +386 -0
- package/starter/desert/README.md +35 -0
- package/starter/desert/assets.json +12 -0
- package/starter/desert/bake.mjs +118 -0
- package/starter/desert/recipe.json +66156 -0
- package/starter/desert/sky.ts +23 -0
- package/starter/desert/surface.json +54 -0
- package/starter/desert/world.ts +335 -0
- package/starter/forest/README.md +46 -0
- package/starter/forest/assets.json +32 -0
- package/starter/forest/bake.mjs +118 -0
- package/starter/forest/recipe.json +4586 -0
- package/starter/forest/sky.ts +27 -0
- package/starter/forest/surface.json +79 -0
- package/starter/forest/water.ts +266 -0
- package/starter/forest/world.ts +413 -0
- package/starter/tundra/README.md +29 -0
- package/starter/tundra/assets.json +12 -0
- package/starter/tundra/bake.mjs +118 -0
- package/starter/tundra/recipe.json +66325 -0
- package/starter/tundra/sky.ts +27 -0
- package/starter/tundra/surface.json +54 -0
- package/starter/tundra/water.ts +313 -0
- package/starter/tundra/world.ts +366 -0
- package/starter-assets/bark_brown_02/bark_brown_02_diff_1k.jpg +0 -0
- package/starter-assets/bark_brown_02/bark_brown_02_nor_gl_1k.jpg +0 -0
- package/starter-assets/bark_brown_02/bark_brown_02_rough_1k.jpg +0 -0
- package/starter-assets/boulder_01/boulder-mid.glb +0 -0
- package/starter-assets/boulder_01/boulder-near.glb +0 -0
- package/starter-assets/cliff_side/cliff_side_ao_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_diff_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_nor_gl_1k.jpg +0 -0
- package/starter-assets/cliff_side/cliff_side_rough_1k.jpg +0 -0
- package/starter-assets/credits.json +1022 -0
- package/starter-assets/fern_02/fern_02_alpha_512.png +0 -0
- package/starter-assets/fern_02/fern_02_diff_512.jpg +0 -0
- package/starter-assets/fern_02/fern_02_nor_gl_512.jpg +0 -0
- package/starter-assets/fir_tree_01/fir-b-lod1.glb +0 -0
- package/starter-assets/fir_tree_01/fir-b-lod2.glb +0 -0
- package/starter-assets/fir_tree_01/fir-c-lod1.glb +0 -0
- package/starter-assets/fir_tree_01/fir-c-lod2.glb +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_arm_1k.jpg +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_diff_1k.jpg +0 -0
- package/starter-assets/fir_tree_01/fir_tree_01_twig_nor_gl_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_ao_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_diff_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_nor_gl_1k.jpg +0 -0
- package/starter-assets/forest_ground_04/forest_ground_04_rough_1k.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_arm_512.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_diff_512.jpg +0 -0
- package/starter-assets/forest_leaves_02/forest_leaves_02_nor_gl_512.jpg +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_alpha_512.png +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_diff_512.jpg +0 -0
- package/starter-assets/grass_medium_02/grass_medium_02_nor_gl_512.jpg +0 -0
- package/starter-assets/hdri/kloofendal_48d_partly_cloudy_1k.hdr +0 -0
- package/starter-assets/leafy_grass/leafy_grass_ao_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_diff_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_nor_gl_1k.jpg +0 -0
- package/starter-assets/leafy_grass/leafy_grass_rough_1k.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_arm_512.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_diff_512.jpg +0 -0
- package/starter-assets/lichen_rock/lichen_rock_nor_gl_512.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_ao_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_diff_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_nor_gl_1k.jpg +0 -0
- package/starter-assets/mossy_rock/mossy_rock_rough_1k.jpg +0 -0
- package/starter-assets/needle-atlas.png +0 -0
- package/starter-assets/needle-surface.png +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_arm_512.jpg +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_diff_512.jpg +0 -0
- package/starter-assets/river_small_rocks/river_small_rocks_nor_gl_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_arm_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_diff_512.jpg +0 -0
- package/starter-assets/rock_boulder_dry/rock_boulder_dry_nor_gl_512.jpg +0 -0
- package/starter-assets/rocks/boulder-mid.glb +0 -0
- package/starter-assets/rocks/boulder-near.glb +0 -0
- package/starter-assets/rocks/boulder-prep.json +37 -0
- package/starter-assets/rocks/rock01-mid.glb +0 -0
- package/starter-assets/rocks/rock01-near.glb +0 -0
- package/starter-assets/rocks/rock01-prep.json +37 -0
- package/starter-assets/rocks/rock04-mid.glb +0 -0
- package/starter-assets/rocks/rock04-near.glb +0 -0
- package/starter-assets/rocks/rock04-prep.json +37 -0
- package/starter-assets/sand_01/sand_01_ao_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_diff_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_nor_gl_1k.jpg +0 -0
- package/starter-assets/sand_01/sand_01_rough_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_ao_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_diff_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_nor_gl_1k.jpg +0 -0
- package/starter-assets/snow_02/snow_02_rough_1k.jpg +0 -0
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Mesh, Object3D, Matrix4 } from 'three';
|
|
2
|
+
import { I as ITerrainState } from './types-Ant8-Axc.js';
|
|
3
|
+
|
|
4
|
+
interface IWorldGLBInput {
|
|
5
|
+
readonly revision: string;
|
|
6
|
+
readonly snapshotTime: number;
|
|
7
|
+
readonly state: ITerrainState;
|
|
8
|
+
/** The committed canonical terrain with caller-baked UV albedo/normal/roughness/AO. */
|
|
9
|
+
readonly terrain: Mesh;
|
|
10
|
+
/**
|
|
11
|
+
* Actual static models by asset id. A placement resolves through `models` first and this second,
|
|
12
|
+
* so a caller can hand one asset two genuinely different models without renaming the asset. The
|
|
13
|
+
* geometries may still be shared instances; the encoder references one buffer view per attribute,
|
|
14
|
+
* so per-placement models cost nodes rather than bytes.
|
|
15
|
+
*/
|
|
16
|
+
readonly assets?: ReadonlyMap<string, Object3D>;
|
|
17
|
+
/** Per-placement models keyed by durable placement id; the resolved model when both are supplied. */
|
|
18
|
+
readonly models?: ReadonlyMap<string, Object3D>;
|
|
19
|
+
/** Final placement-root matrices after the game's model-bound grounding. Keys are durable IDs. */
|
|
20
|
+
readonly transforms: ReadonlyMap<string, Matrix4>;
|
|
21
|
+
/** Every resolved water body/ribbon, baked at snapshotTime with no stale simulation copy. */
|
|
22
|
+
readonly water?: readonly {
|
|
23
|
+
id: string;
|
|
24
|
+
object: Object3D;
|
|
25
|
+
time: number;
|
|
26
|
+
staleFrames: number;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
interface IWorldGLBExport {
|
|
30
|
+
readonly name: "world.glb";
|
|
31
|
+
readonly mime: "model/gltf-binary";
|
|
32
|
+
readonly bytes: Uint8Array;
|
|
33
|
+
readonly report: {
|
|
34
|
+
revision: string;
|
|
35
|
+
resolution: number;
|
|
36
|
+
snapshotTime: number;
|
|
37
|
+
placementIds: string[];
|
|
38
|
+
waterIds: string[];
|
|
39
|
+
/** Standard GLB carries no running atmosphere, lights, fog, exposure or post chain. */
|
|
40
|
+
receivingGameSupplies: string[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Export a committed evaluated world using ordinary glTF 2.0 nodes and embedded PBR images.
|
|
45
|
+
* @requires npm i -D @threenative/terrain
|
|
46
|
+
* @situation export terrain, resolved models and final manual placement transforms as a portable GLB
|
|
47
|
+
* @situation export the whole terrain world as one glb for another three.js project
|
|
48
|
+
* @constraint browser authoring (FileReader/canvas); caller supplies all appearance and coherent baked water; root and /three remain headless
|
|
49
|
+
* @example const output = await exportWorldGLB({ revision, snapshotTime: 0, state, terrain, assets, transforms });
|
|
50
|
+
* @override actual static models, final matrices and baked PBR maps are supplied by the game
|
|
51
|
+
*/
|
|
52
|
+
declare function exportWorldGLB(input: IWorldGLBInput): Promise<IWorldGLBExport>;
|
|
53
|
+
|
|
54
|
+
export { type IWorldGLBExport, type IWorldGLBInput, exportWorldGLB };
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { Group, Vector3, Quaternion, Mesh, MeshStandardMaterial, Camera, Light, Source, Material, TangentSpaceNormalMap, UnsignedByteType, RGBAFormat, SRGBColorSpace, NoColorSpace } from 'three';
|
|
2
|
+
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
|
|
3
|
+
|
|
4
|
+
// src/export.ts
|
|
5
|
+
function geometry(value, name) {
|
|
6
|
+
const position = value.getAttribute("position");
|
|
7
|
+
if (!position || position.count === 0) throw new Error(`${name}: missing geometry`);
|
|
8
|
+
for (const key of ["position", "normal", "uv"]) {
|
|
9
|
+
const attribute = value.getAttribute(key);
|
|
10
|
+
const size = key === "uv" ? 2 : 3;
|
|
11
|
+
if (!attribute || attribute.count !== position.count || attribute.itemSize !== size)
|
|
12
|
+
throw new Error(`${name}: missing/malformed ${key}`);
|
|
13
|
+
for (let i = 0; i < attribute.count; i++)
|
|
14
|
+
for (let c = 0; c < size; c++)
|
|
15
|
+
if (!Number.isFinite(attribute.getComponent(i, c)))
|
|
16
|
+
throw new Error(`${name}: nonfinite ${key}`);
|
|
17
|
+
}
|
|
18
|
+
if (value.index) {
|
|
19
|
+
for (const index of value.index.array)
|
|
20
|
+
if (!Number.isInteger(index) || index < 0 || index >= position.count)
|
|
21
|
+
throw new Error(`${name}: invalid triangle index`);
|
|
22
|
+
}
|
|
23
|
+
if ((value.index?.count ?? position.count) % 3 !== 0)
|
|
24
|
+
throw new Error(`${name}: incomplete triangles`);
|
|
25
|
+
if (Object.keys(value.morphAttributes).length > 0)
|
|
26
|
+
throw new Error(`${name}: morph deformation must be baked first`);
|
|
27
|
+
}
|
|
28
|
+
function image(texture, name, colour) {
|
|
29
|
+
if (Reflect.get(texture, "isCompressedTexture"))
|
|
30
|
+
throw new Error(`${name}: compressed images must be decoded before portable export`);
|
|
31
|
+
if (texture.type !== UnsignedByteType)
|
|
32
|
+
throw new Error(`${name}: bake HDR/float PBR maps into portable byte images first`);
|
|
33
|
+
const source = texture.image;
|
|
34
|
+
if (!source || !Number.isInteger(source.width) || !Number.isInteger(source.height) || (source.width ?? 0) <= 0 || (source.height ?? 0) <= 0 || (source.width ?? 0) * (source.height ?? 0) > 16777216)
|
|
35
|
+
throw new Error(`${name}: missing or oversized image (maximum 16 megapixels)`);
|
|
36
|
+
if (source.data && (texture.format !== RGBAFormat || !(source.data instanceof Uint8Array || source.data instanceof Uint8ClampedArray) || source.data.length !== (source.width ?? 0) * (source.height ?? 0) * 4))
|
|
37
|
+
throw new Error(`${name}: portable data images require complete byte RGBA pixels`);
|
|
38
|
+
if (texture.colorSpace !== (colour ? SRGBColorSpace : NoColorSpace))
|
|
39
|
+
throw new Error(`${name}: expected ${colour ? "sRGB colour" : "linear data"}`);
|
|
40
|
+
}
|
|
41
|
+
function material(value, name, terrain = false) {
|
|
42
|
+
const label = `${name}: ${value.name || value.type}`;
|
|
43
|
+
if (!(value instanceof MeshStandardMaterial) || Reflect.get(value, "isNodeMaterial") || Reflect.get(value, "isMeshPhysicalMaterial") || value.onBeforeCompile !== Material.prototype.onBeforeCompile)
|
|
44
|
+
throw new Error(`${label}: unsupported unbaked material`);
|
|
45
|
+
for (const role of ["displacementMap", "bumpMap", "lightMap"])
|
|
46
|
+
if (value[role]) throw new Error(`${label}: bake ${role} into the portable surface first`);
|
|
47
|
+
if (value.normalMap && (value.normalMapType !== TangentSpaceNormalMap || Math.abs(value.normalScale.x) !== Math.abs(value.normalScale.y)))
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${label}: bake object-space/anisotropic normal maps into a portable tangent-space map first`
|
|
50
|
+
);
|
|
51
|
+
if (value.alphaMap) throw new Error(`${label}: bake alphaMap into the colour image first`);
|
|
52
|
+
if (![
|
|
53
|
+
...value.color.toArray(),
|
|
54
|
+
...value.emissive.toArray(),
|
|
55
|
+
value.emissiveIntensity,
|
|
56
|
+
value.normalScale.x,
|
|
57
|
+
value.normalScale.y,
|
|
58
|
+
value.opacity,
|
|
59
|
+
value.alphaTest,
|
|
60
|
+
value.roughness,
|
|
61
|
+
value.metalness
|
|
62
|
+
].every(Number.isFinite) || [value.opacity, value.alphaTest, value.roughness, value.metalness].some((v) => v < 0 || v > 1))
|
|
63
|
+
throw new Error(`${label}: invalid PBR factors`);
|
|
64
|
+
for (const role of [
|
|
65
|
+
"map",
|
|
66
|
+
"normalMap",
|
|
67
|
+
"roughnessMap",
|
|
68
|
+
"aoMap",
|
|
69
|
+
"metalnessMap",
|
|
70
|
+
"emissiveMap"
|
|
71
|
+
]) {
|
|
72
|
+
const texture = value[role];
|
|
73
|
+
if (!texture && terrain && ["map", "normalMap", "roughnessMap", "aoMap"].includes(role))
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${label}: baked ${role} required; vertex colours alone are not the full-world surface`
|
|
76
|
+
);
|
|
77
|
+
if (texture) image(texture, `${label}.${role}`, role === "map" || role === "emissiveMap");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function staticModel(source, name, terrain = false) {
|
|
81
|
+
let count = 0;
|
|
82
|
+
source.traverseVisible((object) => {
|
|
83
|
+
if (object instanceof Mesh) {
|
|
84
|
+
if (Reflect.get(object, "isSkinnedMesh") || Reflect.get(object, "isInstancedMesh"))
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${name}/${object.name}: resolve skinning/instancing into static meshes first`
|
|
87
|
+
);
|
|
88
|
+
count++;
|
|
89
|
+
geometry(object.geometry, `${name}/${object.name}`);
|
|
90
|
+
for (const value of Array.isArray(object.material) ? object.material : [object.material])
|
|
91
|
+
material(value, `${name}/${object.name}`, terrain);
|
|
92
|
+
} else if (!["Object3D", "Group", "Scene"].includes(object.type) && !(object instanceof Camera) && !(object instanceof Light)) {
|
|
93
|
+
throw new Error(`${name}/${object.name}: unsupported ${object.type}`);
|
|
94
|
+
}
|
|
95
|
+
if (![
|
|
96
|
+
...object.position.toArray(),
|
|
97
|
+
...object.quaternion.toArray(),
|
|
98
|
+
...object.scale.toArray(),
|
|
99
|
+
...object.matrix.elements
|
|
100
|
+
].every(Number.isFinite))
|
|
101
|
+
throw new Error(`${name}/${object.name}: nonfinite transform`);
|
|
102
|
+
});
|
|
103
|
+
if (!count) throw new Error(`${name}: no static meshes`);
|
|
104
|
+
const copy = source.clone(true);
|
|
105
|
+
const excluded = [];
|
|
106
|
+
copy.traverse((object) => {
|
|
107
|
+
object.userData = {};
|
|
108
|
+
if (object instanceof Camera || object instanceof Light) excluded.push(object);
|
|
109
|
+
});
|
|
110
|
+
for (const object of excluded) object.removeFromParent();
|
|
111
|
+
return copy;
|
|
112
|
+
}
|
|
113
|
+
async function exportWorldGLB(input) {
|
|
114
|
+
const { state } = input;
|
|
115
|
+
if (!/^[a-f0-9]{64}$/.test(input.revision) || !Number.isFinite(input.snapshotTime))
|
|
116
|
+
throw new Error("World export requires a content revision and finite snapshot time");
|
|
117
|
+
const positions = input.terrain.geometry.getAttribute("position");
|
|
118
|
+
const n = state.resolution;
|
|
119
|
+
if (!positions || positions.count !== n * n || state.height.length !== n * n || !Number.isFinite(state.size) || state.size <= 0 || n < 2)
|
|
120
|
+
throw new Error("World export terrain does not match the evaluated grid");
|
|
121
|
+
if (!input.terrain.matrix.equals(input.terrain.matrix.clone().identity()) || input.terrain.position.lengthSq() !== 0 || input.terrain.scale.toArray().some((v) => v !== 1) || input.terrain.quaternion.toArray().some((v, i) => v !== (i === 3 ? 1 : 0)))
|
|
122
|
+
throw new Error("World export terrain must retain the canonical metre frame");
|
|
123
|
+
for (let i = 0; i < positions.count; i++) {
|
|
124
|
+
const expected = [
|
|
125
|
+
Math.fround(-state.size / 2 + i % n * state.size / (n - 1)),
|
|
126
|
+
state.height[i],
|
|
127
|
+
Math.fround(-state.size / 2 + Math.floor(i / n) * state.size / (n - 1))
|
|
128
|
+
];
|
|
129
|
+
if ([positions.getX(i), positions.getY(i), positions.getZ(i)].some(
|
|
130
|
+
(value, c) => value !== expected[c]
|
|
131
|
+
))
|
|
132
|
+
throw new Error(`World export terrain vertex ${i} differs from the committed state`);
|
|
133
|
+
}
|
|
134
|
+
const root = new Group();
|
|
135
|
+
root.name = "terrain-world";
|
|
136
|
+
root.userData = {
|
|
137
|
+
terrainRevision: input.revision,
|
|
138
|
+
units: "metres",
|
|
139
|
+
up: "Y",
|
|
140
|
+
snapshotTime: input.snapshotTime,
|
|
141
|
+
resolution: n
|
|
142
|
+
};
|
|
143
|
+
const terrain = staticModel(input.terrain, "terrain", true);
|
|
144
|
+
terrain.name = "terrain";
|
|
145
|
+
root.add(terrain);
|
|
146
|
+
const ids = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const placement of state.instances) {
|
|
148
|
+
if (ids.has(placement.id)) throw new Error(`Duplicate placement '${placement.id}'`);
|
|
149
|
+
ids.add(placement.id);
|
|
150
|
+
const asset = input.models?.get(placement.id) ?? input.assets?.get(placement.asset);
|
|
151
|
+
if (!asset) throw new Error(`Unresolved placement asset '${placement.asset}'`);
|
|
152
|
+
const matrix = input.transforms.get(placement.id);
|
|
153
|
+
if (!matrix || !matrix.elements.every((value) => Number.isFinite(Math.fround(value))) || !Number.isFinite(matrix.determinant()) || matrix.determinant() <= 0 || matrix.elements.some((v, i) => [3, 7, 11, 15].includes(i) && v !== (i === 15 ? 1 : 0)))
|
|
154
|
+
throw new Error(`Invalid/missing final transform '${placement.id}'`);
|
|
155
|
+
const position = new Vector3();
|
|
156
|
+
const quaternion = new Quaternion();
|
|
157
|
+
const scale = new Vector3();
|
|
158
|
+
matrix.decompose(position, quaternion, scale);
|
|
159
|
+
if (Math.min(scale.x, scale.y, scale.z) <= 0)
|
|
160
|
+
throw new Error(`Nonpositive placement scale '${placement.id}'`);
|
|
161
|
+
const recomposed = matrix.clone().compose(position, quaternion, scale);
|
|
162
|
+
if (matrix.elements.some(
|
|
163
|
+
(value, index) => Math.abs(value - (recomposed.elements[index] ?? Number.NaN)) > 1e-6 * Math.max(1, Math.abs(value))
|
|
164
|
+
))
|
|
165
|
+
throw new Error(`Placement transform '${placement.id}' contains unsupported shear`);
|
|
166
|
+
const node = new Group();
|
|
167
|
+
node.name = placement.id;
|
|
168
|
+
node.matrix.copy(matrix);
|
|
169
|
+
node.matrixAutoUpdate = false;
|
|
170
|
+
node.userData = {
|
|
171
|
+
placementId: placement.id,
|
|
172
|
+
assetId: placement.asset,
|
|
173
|
+
grounding: placement.transform?.grounding ?? true
|
|
174
|
+
};
|
|
175
|
+
node.add(staticModel(asset, `asset:${placement.asset}`));
|
|
176
|
+
root.add(node);
|
|
177
|
+
}
|
|
178
|
+
for (const id of input.transforms.keys())
|
|
179
|
+
if (!ids.has(id)) throw new Error(`Unmatched export transform '${id}'`);
|
|
180
|
+
const waterIds = /* @__PURE__ */ new Set([
|
|
181
|
+
...state.waters.map((water) => water.id),
|
|
182
|
+
...state.rivers.map((river) => river.id)
|
|
183
|
+
]);
|
|
184
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const water of input.water ?? []) {
|
|
186
|
+
if (!waterIds.has(water.id) || snapshots.has(water.id))
|
|
187
|
+
throw new Error(`Unexpected/duplicate water '${water.id}'`);
|
|
188
|
+
snapshots.set(water.id, water);
|
|
189
|
+
}
|
|
190
|
+
for (const id of waterIds) {
|
|
191
|
+
const water = snapshots.get(id);
|
|
192
|
+
if (!water) throw new Error(`Water '${id}' requires a baked snapshot`);
|
|
193
|
+
if (water.staleFrames !== 0) throw new Error(`Water '${id}' has a stale/unresolved snapshot`);
|
|
194
|
+
if (water.time !== input.snapshotTime)
|
|
195
|
+
throw new Error(`Water '${id}' snapshot time differs from requested time`);
|
|
196
|
+
const copy = staticModel(water.object, `water:${id}`);
|
|
197
|
+
copy.name = `water:${id}`;
|
|
198
|
+
root.add(copy);
|
|
199
|
+
}
|
|
200
|
+
if (typeof FileReader === "undefined" || typeof document === "undefined")
|
|
201
|
+
throw new Error("exportWorldGLB needs browser authoring FileReader/canvas");
|
|
202
|
+
const geometries = /* @__PURE__ */ new Map();
|
|
203
|
+
const materials = /* @__PURE__ */ new Map();
|
|
204
|
+
const textures = /* @__PURE__ */ new Map();
|
|
205
|
+
function freezeTexture(source) {
|
|
206
|
+
const found = textures.get(source);
|
|
207
|
+
if (found) return found;
|
|
208
|
+
const copy = source.clone();
|
|
209
|
+
copy.userData = {};
|
|
210
|
+
const image2 = source.image;
|
|
211
|
+
const canvas = document.createElement("canvas");
|
|
212
|
+
canvas.width = image2.width;
|
|
213
|
+
canvas.height = image2.height;
|
|
214
|
+
const context = canvas.getContext("2d");
|
|
215
|
+
if (!context) throw new Error("World export image snapshot canvas unavailable");
|
|
216
|
+
if (image2.data)
|
|
217
|
+
context.putImageData(
|
|
218
|
+
new ImageData(new Uint8ClampedArray(image2.data), image2.width, image2.height),
|
|
219
|
+
0,
|
|
220
|
+
0
|
|
221
|
+
);
|
|
222
|
+
else context.drawImage(source.image, 0, 0);
|
|
223
|
+
copy.source = new Source(canvas);
|
|
224
|
+
textures.set(source, copy);
|
|
225
|
+
return copy;
|
|
226
|
+
}
|
|
227
|
+
function freezeMaterial(source) {
|
|
228
|
+
const found = materials.get(source);
|
|
229
|
+
if (found) return found;
|
|
230
|
+
const copy = source.clone();
|
|
231
|
+
copy.userData = {};
|
|
232
|
+
if (copy instanceof MeshStandardMaterial && source instanceof MeshStandardMaterial) {
|
|
233
|
+
for (const role of [
|
|
234
|
+
"map",
|
|
235
|
+
"normalMap",
|
|
236
|
+
"roughnessMap",
|
|
237
|
+
"aoMap",
|
|
238
|
+
"metalnessMap",
|
|
239
|
+
"emissiveMap"
|
|
240
|
+
])
|
|
241
|
+
if (source[role]) copy[role] = freezeTexture(source[role]);
|
|
242
|
+
}
|
|
243
|
+
materials.set(source, copy);
|
|
244
|
+
return copy;
|
|
245
|
+
}
|
|
246
|
+
let binary;
|
|
247
|
+
try {
|
|
248
|
+
root.traverse((object) => {
|
|
249
|
+
if (!(object instanceof Mesh)) return;
|
|
250
|
+
const copy = geometries.get(object.geometry) ?? object.geometry.clone();
|
|
251
|
+
geometries.set(object.geometry, copy);
|
|
252
|
+
object.geometry = copy;
|
|
253
|
+
object.material = Array.isArray(object.material) ? object.material.map(freezeMaterial) : freezeMaterial(object.material);
|
|
254
|
+
});
|
|
255
|
+
root.updateMatrixWorld(true);
|
|
256
|
+
binary = await new GLTFExporter().parseAsync(root, {
|
|
257
|
+
binary: true,
|
|
258
|
+
onlyVisible: true,
|
|
259
|
+
includeCustomExtensions: false
|
|
260
|
+
});
|
|
261
|
+
} finally {
|
|
262
|
+
for (const value of geometries.values()) value.dispose();
|
|
263
|
+
for (const value of materials.values()) value.dispose();
|
|
264
|
+
for (const value of textures.values()) value.dispose();
|
|
265
|
+
}
|
|
266
|
+
if (!(binary instanceof ArrayBuffer)) throw new Error("GLTFExporter did not return a GLB");
|
|
267
|
+
const header = new DataView(binary);
|
|
268
|
+
if (header.getUint32(0, true) !== 1179937895 || header.getUint32(4, true) !== 2 || header.getUint32(8, true) !== binary.byteLength || header.getUint32(16, true) !== 1313821514)
|
|
269
|
+
throw new Error("GLTFExporter returned a malformed GLB");
|
|
270
|
+
const json = JSON.parse(
|
|
271
|
+
new TextDecoder().decode(new Uint8Array(binary, 20, header.getUint32(12, true)))
|
|
272
|
+
);
|
|
273
|
+
if ([...json.images ?? [], ...json.buffers ?? []].some((resource) => resource.uri) || json.extensionsRequired?.some((name) => !name.startsWith("KHR_")))
|
|
274
|
+
throw new Error("World GLB contains an external resource or nonportable required extension");
|
|
275
|
+
return {
|
|
276
|
+
name: "world.glb",
|
|
277
|
+
mime: "model/gltf-binary",
|
|
278
|
+
bytes: new Uint8Array(binary),
|
|
279
|
+
report: {
|
|
280
|
+
revision: input.revision,
|
|
281
|
+
resolution: n,
|
|
282
|
+
snapshotTime: input.snapshotTime,
|
|
283
|
+
placementIds: [...ids],
|
|
284
|
+
waterIds: [...waterIds],
|
|
285
|
+
receivingGameSupplies: [
|
|
286
|
+
"lighting",
|
|
287
|
+
"sky/environment",
|
|
288
|
+
"fog",
|
|
289
|
+
"exposure",
|
|
290
|
+
"post-processing",
|
|
291
|
+
"live water/wind"
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export { exportWorldGLB };
|