@ggez/three-runtime 0.1.0 → 0.1.2
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/dist/index.js +47 -1786
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,225 +1,5 @@
|
|
|
1
|
-
// ../shared/src/utils.ts
|
|
2
|
-
import { Euler, Matrix4, Quaternion, Vector3 } from "three";
|
|
3
|
-
function createBlockoutTextureDataUri(color, edgeColor = "#f5f2ea", edgeThickness = 0.018) {
|
|
4
|
-
const size = 256;
|
|
5
|
-
const frame = Math.max(2, Math.min(6, Math.round(size * edgeThickness)));
|
|
6
|
-
const innerInset = frame + 3;
|
|
7
|
-
const seamInset = innerInset + 5;
|
|
8
|
-
const corner = 18;
|
|
9
|
-
const highlight = mixHexColors(edgeColor, "#ffffff", 0.42);
|
|
10
|
-
const frameColor = mixHexColors(edgeColor, color, 0.12);
|
|
11
|
-
const innerShadow = mixHexColors(edgeColor, color, 0.28);
|
|
12
|
-
const svg = `
|
|
13
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
14
|
-
<rect width="${size}" height="${size}" rx="${corner}" fill="${color}" />
|
|
15
|
-
<rect x="${frame / 2}" y="${frame / 2}" width="${size - frame}" height="${size - frame}" rx="${corner - 2}" fill="none" stroke="${frameColor}" stroke-width="${frame}" />
|
|
16
|
-
<rect x="${innerInset}" y="${innerInset}" width="${size - innerInset * 2}" height="${size - innerInset * 2}" rx="${corner - 5}" fill="none" stroke="${highlight}" stroke-opacity="0.42" stroke-width="1" />
|
|
17
|
-
<rect x="${seamInset}" y="${seamInset}" width="${size - seamInset * 2}" height="${size - seamInset * 2}" rx="${corner - 9}" fill="none" stroke="${innerShadow}" stroke-opacity="0.12" stroke-width="1" />
|
|
18
|
-
<path d="M ${innerInset} ${size * 0.28} H ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.08" stroke-width="1" />
|
|
19
|
-
<path d="M ${size * 0.28} ${innerInset} V ${size - innerInset}" stroke="${highlight}" stroke-opacity="0.06" stroke-width="1" />
|
|
20
|
-
</svg>
|
|
21
|
-
`.trim();
|
|
22
|
-
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
|
23
|
-
}
|
|
24
|
-
function vec3(x, y, z) {
|
|
25
|
-
return { x, y, z };
|
|
26
|
-
}
|
|
27
|
-
function mixHexColors(left, right, t) {
|
|
28
|
-
const normalizedLeft = normalizeHex(left);
|
|
29
|
-
const normalizedRight = normalizeHex(right);
|
|
30
|
-
const leftValue = Number.parseInt(normalizedLeft.slice(1), 16);
|
|
31
|
-
const rightValue = Number.parseInt(normalizedRight.slice(1), 16);
|
|
32
|
-
const channels = [16, 8, 0].map((shift) => {
|
|
33
|
-
const leftChannel = leftValue >> shift & 255;
|
|
34
|
-
const rightChannel = rightValue >> shift & 255;
|
|
35
|
-
return Math.round(leftChannel + (rightChannel - leftChannel) * t).toString(16).padStart(2, "0");
|
|
36
|
-
});
|
|
37
|
-
return `#${channels.join("")}`;
|
|
38
|
-
}
|
|
39
|
-
function normalizeHex(color) {
|
|
40
|
-
if (/^#[0-9a-f]{6}$/i.test(color)) {
|
|
41
|
-
return color;
|
|
42
|
-
}
|
|
43
|
-
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
44
|
-
return `#${color.slice(1).split("").map((channel) => `${channel}${channel}`).join("")}`;
|
|
45
|
-
}
|
|
46
|
-
return "#808080";
|
|
47
|
-
}
|
|
48
|
-
function addVec3(left, right) {
|
|
49
|
-
return vec3(left.x + right.x, left.y + right.y, left.z + right.z);
|
|
50
|
-
}
|
|
51
|
-
function subVec3(left, right) {
|
|
52
|
-
return vec3(left.x - right.x, left.y - right.y, left.z - right.z);
|
|
53
|
-
}
|
|
54
|
-
function scaleVec3(vector, scalar) {
|
|
55
|
-
return vec3(vector.x * scalar, vector.y * scalar, vector.z * scalar);
|
|
56
|
-
}
|
|
57
|
-
function dotVec3(left, right) {
|
|
58
|
-
return left.x * right.x + left.y * right.y + left.z * right.z;
|
|
59
|
-
}
|
|
60
|
-
function crossVec3(left, right) {
|
|
61
|
-
return vec3(
|
|
62
|
-
left.y * right.z - left.z * right.y,
|
|
63
|
-
left.z * right.x - left.x * right.z,
|
|
64
|
-
left.x * right.y - left.y * right.x
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
function lengthVec3(vector) {
|
|
68
|
-
return Math.sqrt(dotVec3(vector, vector));
|
|
69
|
-
}
|
|
70
|
-
function normalizeVec3(vector, epsilon = 1e-6) {
|
|
71
|
-
const length = lengthVec3(vector);
|
|
72
|
-
if (length <= epsilon) {
|
|
73
|
-
return vec3(0, 0, 0);
|
|
74
|
-
}
|
|
75
|
-
return scaleVec3(vector, 1 / length);
|
|
76
|
-
}
|
|
77
|
-
function averageVec3(vectors) {
|
|
78
|
-
if (vectors.length === 0) {
|
|
79
|
-
return vec3(0, 0, 0);
|
|
80
|
-
}
|
|
81
|
-
const total = vectors.reduce((sum, vector) => addVec3(sum, vector), vec3(0, 0, 0));
|
|
82
|
-
return scaleVec3(total, 1 / vectors.length);
|
|
83
|
-
}
|
|
84
|
-
var tempPosition = new Vector3();
|
|
85
|
-
var tempQuaternion = new Quaternion();
|
|
86
|
-
var tempScale = new Vector3();
|
|
87
|
-
function composeTransforms(parent, child) {
|
|
88
|
-
const matrix = transformToMatrix(parent).multiply(transformToMatrix(child));
|
|
89
|
-
return matrixToTransform(matrix, child.pivot);
|
|
90
|
-
}
|
|
91
|
-
function resolveSceneGraph(nodes, entities = []) {
|
|
92
|
-
const nodeList = Array.from(nodes);
|
|
93
|
-
const entityList = Array.from(entities);
|
|
94
|
-
const nodesById = new Map(nodeList.map((node) => [node.id, node]));
|
|
95
|
-
const nodeWorldTransforms = /* @__PURE__ */ new Map();
|
|
96
|
-
const entityWorldTransforms = /* @__PURE__ */ new Map();
|
|
97
|
-
const nodeChildrenByParentId = /* @__PURE__ */ new Map();
|
|
98
|
-
const entityChildrenByParentId = /* @__PURE__ */ new Map();
|
|
99
|
-
const rootNodeIds = [];
|
|
100
|
-
const rootEntityIds = [];
|
|
101
|
-
const nodeStack = /* @__PURE__ */ new Set();
|
|
102
|
-
const appendNodeChild = (parentId, childId) => {
|
|
103
|
-
const children = nodeChildrenByParentId.get(parentId);
|
|
104
|
-
if (children) {
|
|
105
|
-
children.push(childId);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
nodeChildrenByParentId.set(parentId, [childId]);
|
|
109
|
-
};
|
|
110
|
-
const appendEntityChild = (parentId, childId) => {
|
|
111
|
-
const children = entityChildrenByParentId.get(parentId);
|
|
112
|
-
if (children) {
|
|
113
|
-
children.push(childId);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
entityChildrenByParentId.set(parentId, [childId]);
|
|
117
|
-
};
|
|
118
|
-
const resolveNodeTransform = (node) => {
|
|
119
|
-
const cached = nodeWorldTransforms.get(node.id);
|
|
120
|
-
if (cached) {
|
|
121
|
-
return cached;
|
|
122
|
-
}
|
|
123
|
-
if (nodeStack.has(node.id)) {
|
|
124
|
-
const fallback = structuredClone(node.transform);
|
|
125
|
-
nodeWorldTransforms.set(node.id, fallback);
|
|
126
|
-
return fallback;
|
|
127
|
-
}
|
|
128
|
-
nodeStack.add(node.id);
|
|
129
|
-
const parent = node.parentId && node.parentId !== node.id ? nodesById.get(node.parentId) : void 0;
|
|
130
|
-
const resolved = parent ? composeTransforms(resolveNodeTransform(parent), node.transform) : structuredClone(node.transform);
|
|
131
|
-
nodeWorldTransforms.set(node.id, resolved);
|
|
132
|
-
nodeStack.delete(node.id);
|
|
133
|
-
return resolved;
|
|
134
|
-
};
|
|
135
|
-
nodeList.forEach((node) => {
|
|
136
|
-
resolveNodeTransform(node);
|
|
137
|
-
const hasValidParent = Boolean(
|
|
138
|
-
node.parentId && node.parentId !== node.id && nodesById.has(node.parentId)
|
|
139
|
-
);
|
|
140
|
-
if (hasValidParent) {
|
|
141
|
-
appendNodeChild(node.parentId, node.id);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
rootNodeIds.push(node.id);
|
|
145
|
-
});
|
|
146
|
-
entityList.forEach((entity) => {
|
|
147
|
-
const parent = entity.parentId && nodesById.has(entity.parentId) ? nodesById.get(entity.parentId) : void 0;
|
|
148
|
-
entityWorldTransforms.set(
|
|
149
|
-
entity.id,
|
|
150
|
-
parent ? composeTransforms(resolveNodeTransform(parent), entity.transform) : structuredClone(entity.transform)
|
|
151
|
-
);
|
|
152
|
-
if (parent) {
|
|
153
|
-
appendEntityChild(parent.id, entity.id);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
rootEntityIds.push(entity.id);
|
|
157
|
-
});
|
|
158
|
-
return {
|
|
159
|
-
entityChildrenByParentId,
|
|
160
|
-
entityWorldTransforms,
|
|
161
|
-
nodeChildrenByParentId,
|
|
162
|
-
nodeWorldTransforms,
|
|
163
|
-
rootEntityIds,
|
|
164
|
-
rootNodeIds
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function isBrushNode(node) {
|
|
168
|
-
return node.kind === "brush";
|
|
169
|
-
}
|
|
170
|
-
function isMeshNode(node) {
|
|
171
|
-
return node.kind === "mesh";
|
|
172
|
-
}
|
|
173
|
-
function isGroupNode(node) {
|
|
174
|
-
return node.kind === "group";
|
|
175
|
-
}
|
|
176
|
-
function isModelNode(node) {
|
|
177
|
-
return node.kind === "model";
|
|
178
|
-
}
|
|
179
|
-
function isPrimitiveNode(node) {
|
|
180
|
-
return node.kind === "primitive";
|
|
181
|
-
}
|
|
182
|
-
function isInstancingNode(node) {
|
|
183
|
-
return node.kind === "instancing";
|
|
184
|
-
}
|
|
185
|
-
function isInstancingSourceNode(node) {
|
|
186
|
-
return isBrushNode(node) || isMeshNode(node) || isPrimitiveNode(node) || isModelNode(node);
|
|
187
|
-
}
|
|
188
|
-
function resolveInstancingSourceNode(nodes, nodeOrId, maxDepth = 32) {
|
|
189
|
-
const nodesById = new Map(Array.from(nodes, (node) => [node.id, node]));
|
|
190
|
-
let current = typeof nodeOrId === "string" ? nodesById.get(nodeOrId) : nodeOrId;
|
|
191
|
-
let depth = 0;
|
|
192
|
-
while (current && depth <= maxDepth) {
|
|
193
|
-
if (isInstancingSourceNode(current)) {
|
|
194
|
-
return current;
|
|
195
|
-
}
|
|
196
|
-
if (!isInstancingNode(current)) {
|
|
197
|
-
return void 0;
|
|
198
|
-
}
|
|
199
|
-
current = nodesById.get(current.data.sourceNodeId);
|
|
200
|
-
depth += 1;
|
|
201
|
-
}
|
|
202
|
-
return void 0;
|
|
203
|
-
}
|
|
204
|
-
function transformToMatrix(transform) {
|
|
205
|
-
return new Matrix4().compose(
|
|
206
|
-
new Vector3(transform.position.x, transform.position.y, transform.position.z),
|
|
207
|
-
new Quaternion().setFromEuler(new Euler(transform.rotation.x, transform.rotation.y, transform.rotation.z, "XYZ")),
|
|
208
|
-
new Vector3(transform.scale.x, transform.scale.y, transform.scale.z)
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
function matrixToTransform(matrix, pivot) {
|
|
212
|
-
matrix.decompose(tempPosition, tempQuaternion, tempScale);
|
|
213
|
-
const rotation = new Euler().setFromQuaternion(tempQuaternion, "XYZ");
|
|
214
|
-
return {
|
|
215
|
-
pivot: pivot ? vec3(pivot.x, pivot.y, pivot.z) : void 0,
|
|
216
|
-
position: vec3(tempPosition.x, tempPosition.y, tempPosition.z),
|
|
217
|
-
rotation: vec3(rotation.x, rotation.y, rotation.z),
|
|
218
|
-
scale: vec3(tempScale.x, tempScale.y, tempScale.z)
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
1
|
// src/loader.ts
|
|
2
|
+
import { resolveInstancingSourceNode as resolveInstancingSourceNode2, resolveSceneGraph as resolveSceneGraph2, vec3 as vec32 } from "@ggez/shared";
|
|
223
3
|
import {
|
|
224
4
|
AmbientLight as AmbientLight2,
|
|
225
5
|
BackSide as BackSide2,
|
|
@@ -230,7 +10,7 @@ import {
|
|
|
230
10
|
DirectionalLight as DirectionalLight2,
|
|
231
11
|
DoubleSide as DoubleSide2,
|
|
232
12
|
EquirectangularReflectionMapping,
|
|
233
|
-
Euler as
|
|
13
|
+
Euler as Euler2,
|
|
234
14
|
Fog,
|
|
235
15
|
Float32BufferAttribute as Float32BufferAttribute2,
|
|
236
16
|
FrontSide as FrontSide2,
|
|
@@ -238,108 +18,30 @@ import {
|
|
|
238
18
|
InstancedMesh as InstancedMesh2,
|
|
239
19
|
HemisphereLight as HemisphereLight2,
|
|
240
20
|
LOD as LOD2,
|
|
241
|
-
Matrix4 as
|
|
21
|
+
Matrix4 as Matrix42,
|
|
242
22
|
Mesh as Mesh2,
|
|
243
23
|
MeshStandardMaterial as MeshStandardMaterial2,
|
|
244
24
|
Object3D as Object3D2,
|
|
245
25
|
PointLight as PointLight2,
|
|
246
|
-
Quaternion as
|
|
26
|
+
Quaternion as Quaternion2,
|
|
247
27
|
SRGBColorSpace as SRGBColorSpace2,
|
|
248
28
|
SpotLight as SpotLight2,
|
|
249
29
|
TextureLoader as TextureLoader2,
|
|
250
30
|
RepeatWrapping as RepeatWrapping2,
|
|
251
|
-
Vector3 as
|
|
31
|
+
Vector3 as Vector32
|
|
252
32
|
} from "three";
|
|
253
33
|
import { GLTFLoader as GLTFLoader2 } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
254
34
|
import { HDRLoader } from "three/examples/jsm/loaders/HDRLoader.js";
|
|
255
35
|
import { MTLLoader as MTLLoader2 } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
256
36
|
import { OBJLoader as OBJLoader2 } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// ../runtime-format/src/format.ts
|
|
264
|
-
function isRuntimeScene(value) {
|
|
265
|
-
return validateRuntimeScene(value).ok;
|
|
266
|
-
}
|
|
267
|
-
function validateRuntimeScene(value) {
|
|
268
|
-
const errors = [];
|
|
269
|
-
if (!value || typeof value !== "object") {
|
|
270
|
-
return invalid("Runtime scene must be an object.");
|
|
271
|
-
}
|
|
272
|
-
const candidate = value;
|
|
273
|
-
if (candidate.metadata?.format !== RUNTIME_SCENE_FORMAT) {
|
|
274
|
-
errors.push(`Runtime scene metadata.format must be "${RUNTIME_SCENE_FORMAT}".`);
|
|
275
|
-
}
|
|
276
|
-
if (typeof candidate.metadata?.version !== "number") {
|
|
277
|
-
errors.push("Runtime scene metadata.version must be a number.");
|
|
278
|
-
} else if (candidate.metadata.version < MIN_RUNTIME_SCENE_VERSION) {
|
|
279
|
-
errors.push(`Runtime scene metadata.version must be >= ${MIN_RUNTIME_SCENE_VERSION}.`);
|
|
280
|
-
}
|
|
281
|
-
if (!Array.isArray(candidate.nodes)) {
|
|
282
|
-
errors.push("Runtime scene nodes must be an array.");
|
|
283
|
-
}
|
|
284
|
-
if (!Array.isArray(candidate.assets)) {
|
|
285
|
-
errors.push("Runtime scene assets must be an array.");
|
|
286
|
-
}
|
|
287
|
-
if (!Array.isArray(candidate.materials)) {
|
|
288
|
-
errors.push("Runtime scene materials must be an array.");
|
|
289
|
-
}
|
|
290
|
-
if (!Array.isArray(candidate.entities)) {
|
|
291
|
-
errors.push("Runtime scene entities must be an array.");
|
|
292
|
-
}
|
|
293
|
-
if (!Array.isArray(candidate.layers)) {
|
|
294
|
-
errors.push("Runtime scene layers must be an array.");
|
|
295
|
-
}
|
|
296
|
-
if (!candidate.settings || typeof candidate.settings !== "object") {
|
|
297
|
-
errors.push("Runtime scene settings must be an object.");
|
|
298
|
-
}
|
|
299
|
-
if (errors.length > 0) {
|
|
300
|
-
return {
|
|
301
|
-
errors,
|
|
302
|
-
ok: false
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
return {
|
|
306
|
-
errors: [],
|
|
307
|
-
ok: true,
|
|
308
|
-
value: candidate
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
function migrateRuntimeScene(scene) {
|
|
312
|
-
const migrated = structuredClone(scene);
|
|
313
|
-
migrated.metadata = {
|
|
314
|
-
...migrated.metadata,
|
|
315
|
-
format: RUNTIME_SCENE_FORMAT,
|
|
316
|
-
version: CURRENT_RUNTIME_SCENE_VERSION
|
|
317
|
-
};
|
|
318
|
-
return migrated;
|
|
319
|
-
}
|
|
320
|
-
function parseRuntimeScene(text) {
|
|
321
|
-
const parsed = JSON.parse(text);
|
|
322
|
-
const validation = validateRuntimeScene(parsed);
|
|
323
|
-
if (!validation.ok) {
|
|
324
|
-
throw new Error(validation.errors.join(" "));
|
|
325
|
-
}
|
|
326
|
-
return migrateRuntimeScene(validation.value);
|
|
327
|
-
}
|
|
328
|
-
function isRuntimeBundle(value) {
|
|
329
|
-
if (!value || typeof value !== "object") {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
const candidate = value;
|
|
333
|
-
return Array.isArray(candidate.files) && isRuntimeScene(candidate.manifest);
|
|
334
|
-
}
|
|
335
|
-
function invalid(message) {
|
|
336
|
-
return {
|
|
337
|
-
errors: [message],
|
|
338
|
-
ok: false
|
|
339
|
-
};
|
|
340
|
-
}
|
|
37
|
+
import {
|
|
38
|
+
isRuntimeBundle,
|
|
39
|
+
isRuntimeScene,
|
|
40
|
+
parseRuntimeScene
|
|
41
|
+
} from "@ggez/runtime-format";
|
|
341
42
|
|
|
342
43
|
// src/object-factory.ts
|
|
44
|
+
import { resolveInstancingSourceNode, resolveSceneGraph, vec3 } from "@ggez/shared";
|
|
343
45
|
import {
|
|
344
46
|
AmbientLight,
|
|
345
47
|
BackSide,
|
|
@@ -348,24 +50,24 @@ import {
|
|
|
348
50
|
BufferGeometry,
|
|
349
51
|
DirectionalLight,
|
|
350
52
|
DoubleSide,
|
|
351
|
-
Euler
|
|
53
|
+
Euler,
|
|
352
54
|
Float32BufferAttribute,
|
|
353
55
|
FrontSide,
|
|
354
56
|
Group,
|
|
355
57
|
HemisphereLight,
|
|
356
58
|
InstancedMesh,
|
|
357
59
|
LOD,
|
|
358
|
-
Matrix4
|
|
60
|
+
Matrix4,
|
|
359
61
|
Mesh,
|
|
360
62
|
MeshStandardMaterial,
|
|
361
63
|
Object3D,
|
|
362
64
|
PointLight,
|
|
363
|
-
Quaternion
|
|
65
|
+
Quaternion,
|
|
364
66
|
RepeatWrapping,
|
|
365
67
|
SRGBColorSpace,
|
|
366
68
|
SpotLight,
|
|
367
69
|
TextureLoader,
|
|
368
|
-
Vector3
|
|
70
|
+
Vector3
|
|
369
71
|
} from "three";
|
|
370
72
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
371
73
|
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
@@ -374,12 +76,12 @@ var textureLoader = new TextureLoader();
|
|
|
374
76
|
var gltfLoader = new GLTFLoader();
|
|
375
77
|
var mtlLoader = new MTLLoader();
|
|
376
78
|
var modelTextureLoader = new TextureLoader();
|
|
377
|
-
var tempModelInstanceMatrix = new
|
|
378
|
-
var tempModelChildMatrix = new
|
|
379
|
-
var tempPivotMatrix = new
|
|
380
|
-
var tempInstancePosition = new
|
|
381
|
-
var tempInstanceQuaternion = new
|
|
382
|
-
var tempInstanceScale = new
|
|
79
|
+
var tempModelInstanceMatrix = new Matrix4();
|
|
80
|
+
var tempModelChildMatrix = new Matrix4();
|
|
81
|
+
var tempPivotMatrix = new Matrix4();
|
|
82
|
+
var tempInstancePosition = new Vector3();
|
|
83
|
+
var tempInstanceQuaternion = new Quaternion();
|
|
84
|
+
var tempInstanceScale = new Vector3();
|
|
383
85
|
function createWebHammerSceneObjectFactory(engineScene, options = {}) {
|
|
384
86
|
const resources = {
|
|
385
87
|
assetsById: new Map(engineScene.assets.map((asset) => [asset.id, asset])),
|
|
@@ -858,7 +560,7 @@ function centerObject(object, center) {
|
|
|
858
560
|
}
|
|
859
561
|
function computeObjectCenter(object) {
|
|
860
562
|
const box = new Box3().setFromObject(object);
|
|
861
|
-
const center = box.getCenter(new
|
|
563
|
+
const center = box.getCenter(new Vector3());
|
|
862
564
|
return {
|
|
863
565
|
x: center.x,
|
|
864
566
|
y: center.y,
|
|
@@ -939,9 +641,9 @@ function composeGeometryInstanceMatrix(transform, pivot) {
|
|
|
939
641
|
}
|
|
940
642
|
function composeTransformMatrix(transform) {
|
|
941
643
|
tempInstancePosition.set(transform.position.x, transform.position.y, transform.position.z);
|
|
942
|
-
tempInstanceQuaternion.setFromEuler(new
|
|
644
|
+
tempInstanceQuaternion.setFromEuler(new Euler(transform.rotation.x, transform.rotation.y, transform.rotation.z, "XYZ"));
|
|
943
645
|
tempInstanceScale.set(transform.scale.x, transform.scale.y, transform.scale.z);
|
|
944
|
-
return new
|
|
646
|
+
return new Matrix4().compose(tempInstancePosition, tempInstanceQuaternion, tempInstanceScale);
|
|
945
647
|
}
|
|
946
648
|
function resolveSceneLodOptions(lod) {
|
|
947
649
|
if (!lod) {
|
|
@@ -1043,7 +745,7 @@ async function createThreeRuntimeSceneInstance(input, options = {}) {
|
|
|
1043
745
|
const lights = [];
|
|
1044
746
|
const physicsDescriptors = [];
|
|
1045
747
|
const runtimeNodesById = new Map(engineScene.nodes.map((node) => [node.id, node]));
|
|
1046
|
-
const sceneGraph =
|
|
748
|
+
const sceneGraph = resolveSceneGraph2(engineScene.nodes, engineScene.entities);
|
|
1047
749
|
const objectFactory = createWebHammerSceneObjectFactory(engineScene, options);
|
|
1048
750
|
const createdObjects = await Promise.all(
|
|
1049
751
|
engineScene.nodes.map(async (node) => [node.id, await objectFactory.createNodeObject(node)])
|
|
@@ -1280,1461 +982,20 @@ async function loadSkyboxTexture(path, skybox) {
|
|
|
1280
982
|
return texture;
|
|
1281
983
|
}
|
|
1282
984
|
|
|
1283
|
-
// ../runtime-build/src/bundle.ts
|
|
1284
|
-
import { unzipSync, zipSync } from "fflate";
|
|
1285
|
-
var TEXTURE_FIELDS = ["baseColorTexture", "metallicRoughnessTexture", "normalTexture"];
|
|
1286
|
-
async function externalizeRuntimeAssets(scene, options = {}) {
|
|
1287
|
-
const manifest = structuredClone(scene);
|
|
1288
|
-
const files = [];
|
|
1289
|
-
const assetDir = trimSlashes(options.assetDir ?? "assets");
|
|
1290
|
-
const copyExternalAssets = options.copyExternalAssets ?? true;
|
|
1291
|
-
const pathBySource = /* @__PURE__ */ new Map();
|
|
1292
|
-
const usedPaths = /* @__PURE__ */ new Set();
|
|
1293
|
-
for (const material of manifest.materials) {
|
|
1294
|
-
for (const field of TEXTURE_FIELDS) {
|
|
1295
|
-
const source = material[field];
|
|
1296
|
-
if (!source) {
|
|
1297
|
-
continue;
|
|
1298
|
-
}
|
|
1299
|
-
const bundledPath = await materializeSource(source, {
|
|
1300
|
-
copyExternalAssets,
|
|
1301
|
-
files,
|
|
1302
|
-
pathBySource,
|
|
1303
|
-
preferredStem: `${assetDir}/textures/${slugify(material.id)}-${textureFieldSuffix(field)}`,
|
|
1304
|
-
usedPaths
|
|
1305
|
-
});
|
|
1306
|
-
if (bundledPath) {
|
|
1307
|
-
material[field] = bundledPath;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
for (const asset of manifest.assets) {
|
|
1312
|
-
if (asset.type !== "model") {
|
|
1313
|
-
continue;
|
|
1314
|
-
}
|
|
1315
|
-
const bundledPath = await materializeSource(asset.path, {
|
|
1316
|
-
copyExternalAssets,
|
|
1317
|
-
files,
|
|
1318
|
-
pathBySource,
|
|
1319
|
-
preferredExtension: inferModelExtension(asset.path, asset.metadata.modelFormat),
|
|
1320
|
-
preferredStem: `${assetDir}/models/${slugify(asset.id)}`,
|
|
1321
|
-
usedPaths
|
|
1322
|
-
});
|
|
1323
|
-
if (bundledPath) {
|
|
1324
|
-
asset.path = bundledPath;
|
|
1325
|
-
}
|
|
1326
|
-
const texturePath = asset.metadata.texturePath;
|
|
1327
|
-
if (typeof texturePath === "string" && texturePath.length > 0) {
|
|
1328
|
-
const bundledTexturePath = await materializeSource(texturePath, {
|
|
1329
|
-
copyExternalAssets,
|
|
1330
|
-
files,
|
|
1331
|
-
pathBySource,
|
|
1332
|
-
preferredStem: `${assetDir}/model-textures/${slugify(asset.id)}`,
|
|
1333
|
-
usedPaths
|
|
1334
|
-
});
|
|
1335
|
-
if (bundledTexturePath) {
|
|
1336
|
-
asset.metadata.texturePath = bundledTexturePath;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
const skyboxSource = manifest.settings.world.skybox.source;
|
|
1341
|
-
if (skyboxSource) {
|
|
1342
|
-
const bundledSkyboxPath = await materializeSource(skyboxSource, {
|
|
1343
|
-
copyExternalAssets,
|
|
1344
|
-
files,
|
|
1345
|
-
pathBySource,
|
|
1346
|
-
preferredExtension: manifest.settings.world.skybox.format === "hdr" ? "hdr" : inferExtensionFromPath(skyboxSource),
|
|
1347
|
-
preferredStem: `${assetDir}/skyboxes/${slugify(manifest.settings.world.skybox.name || "skybox")}`,
|
|
1348
|
-
usedPaths
|
|
1349
|
-
});
|
|
1350
|
-
if (bundledSkyboxPath) {
|
|
1351
|
-
manifest.settings.world.skybox.source = bundledSkyboxPath;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
return {
|
|
1355
|
-
files,
|
|
1356
|
-
manifest
|
|
1357
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
async function buildRuntimeBundle(scene, options = {}) {
|
|
1360
|
-
return externalizeRuntimeAssets(scene, options);
|
|
1361
|
-
}
|
|
1362
|
-
function normalizeRuntimeScene(scene) {
|
|
1363
|
-
return typeof scene === "string" ? parseRuntimeScene(scene) : parseRuntimeScene(JSON.stringify(scene));
|
|
1364
|
-
}
|
|
1365
|
-
function packRuntimeBundle(bundle, options = {}) {
|
|
1366
|
-
const manifestPath = options.manifestPath ?? "scene.runtime.json";
|
|
1367
|
-
const encoder = new TextEncoder();
|
|
1368
|
-
const entries = {
|
|
1369
|
-
[manifestPath]: encoder.encode(JSON.stringify(bundle.manifest))
|
|
1370
|
-
};
|
|
1371
|
-
bundle.files.forEach((file) => {
|
|
1372
|
-
entries[file.path] = file.bytes;
|
|
1373
|
-
});
|
|
1374
|
-
return zipSync(entries, {
|
|
1375
|
-
level: options.compressionLevel ?? 6
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
function unpackRuntimeBundle(bytes, options = {}) {
|
|
1379
|
-
const manifestPath = options.manifestPath ?? "scene.runtime.json";
|
|
1380
|
-
const archive = unzipSync(bytes);
|
|
1381
|
-
const manifestBytes = archive[manifestPath];
|
|
1382
|
-
if (!manifestBytes) {
|
|
1383
|
-
throw new Error(`Bundle is missing ${manifestPath}.`);
|
|
1384
|
-
}
|
|
1385
|
-
const manifest = parseRuntimeScene(new TextDecoder().decode(manifestBytes));
|
|
1386
|
-
const files = Object.entries(archive).filter(([path]) => path !== manifestPath).map(([path, fileBytes]) => ({
|
|
1387
|
-
bytes: fileBytes,
|
|
1388
|
-
mimeType: inferMimeTypeFromPath(path),
|
|
1389
|
-
path
|
|
1390
|
-
}));
|
|
1391
|
-
return {
|
|
1392
|
-
files,
|
|
1393
|
-
manifest
|
|
1394
|
-
};
|
|
1395
|
-
}
|
|
1396
|
-
function buildRuntimeWorldIndex(chunks, options = {}) {
|
|
1397
|
-
return {
|
|
1398
|
-
chunks,
|
|
1399
|
-
sharedAssets: options.sharedAssets,
|
|
1400
|
-
version: options.version ?? 1
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
async function externalizeWebHammerEngineScene(scene, options = {}) {
|
|
1404
|
-
return externalizeRuntimeAssets(scene, options);
|
|
1405
|
-
}
|
|
1406
|
-
function createWebHammerEngineBundleZip(bundle, options = {}) {
|
|
1407
|
-
return packRuntimeBundle(bundle, options);
|
|
1408
|
-
}
|
|
1409
|
-
function parseWebHammerEngineBundleZip(bytes, options = {}) {
|
|
1410
|
-
return unpackRuntimeBundle(bytes, options);
|
|
1411
|
-
}
|
|
1412
|
-
async function materializeSource(source, context) {
|
|
1413
|
-
const existing = context.pathBySource.get(source);
|
|
1414
|
-
if (existing) {
|
|
1415
|
-
return existing;
|
|
1416
|
-
}
|
|
1417
|
-
if (isDataUrl(source)) {
|
|
1418
|
-
const parsed = parseDataUrl(source);
|
|
1419
|
-
const path2 = ensureUniquePath(
|
|
1420
|
-
`${context.preferredStem}.${inferExtension(parsed.mimeType, context.preferredExtension)}`,
|
|
1421
|
-
context.usedPaths
|
|
1422
|
-
);
|
|
1423
|
-
context.files.push({
|
|
1424
|
-
bytes: parsed.bytes,
|
|
1425
|
-
mimeType: parsed.mimeType,
|
|
1426
|
-
path: path2
|
|
1427
|
-
});
|
|
1428
|
-
context.pathBySource.set(source, path2);
|
|
1429
|
-
return path2;
|
|
1430
|
-
}
|
|
1431
|
-
if (!context.copyExternalAssets) {
|
|
1432
|
-
return void 0;
|
|
1433
|
-
}
|
|
1434
|
-
const response = await fetch(source);
|
|
1435
|
-
if (!response.ok) {
|
|
1436
|
-
throw new Error(`Failed to bundle asset: ${source}`);
|
|
1437
|
-
}
|
|
1438
|
-
const blob = await response.blob();
|
|
1439
|
-
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
1440
|
-
const path = ensureUniquePath(
|
|
1441
|
-
`${context.preferredStem}.${inferExtension(blob.type, context.preferredExtension ?? inferExtensionFromPath(source))}`,
|
|
1442
|
-
context.usedPaths
|
|
1443
|
-
);
|
|
1444
|
-
context.files.push({
|
|
1445
|
-
bytes,
|
|
1446
|
-
mimeType: blob.type || "application/octet-stream",
|
|
1447
|
-
path
|
|
1448
|
-
});
|
|
1449
|
-
context.pathBySource.set(source, path);
|
|
1450
|
-
return path;
|
|
1451
|
-
}
|
|
1452
|
-
function parseDataUrl(source) {
|
|
1453
|
-
const match = /^data:([^;,]+)?(?:;charset=[^;,]+)?(;base64)?,(.*)$/i.exec(source);
|
|
1454
|
-
if (!match) {
|
|
1455
|
-
throw new Error("Invalid data URL.");
|
|
1456
|
-
}
|
|
1457
|
-
const mimeType = match[1] || "application/octet-stream";
|
|
1458
|
-
const payload = match[3] || "";
|
|
1459
|
-
if (match[2]) {
|
|
1460
|
-
const binary = atob(payload);
|
|
1461
|
-
const bytes = new Uint8Array(binary.length);
|
|
1462
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
1463
|
-
bytes[index] = binary.charCodeAt(index);
|
|
1464
|
-
}
|
|
1465
|
-
return { bytes, mimeType };
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
bytes: new TextEncoder().encode(decodeURIComponent(payload)),
|
|
1469
|
-
mimeType
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
function textureFieldSuffix(field) {
|
|
1473
|
-
switch (field) {
|
|
1474
|
-
case "baseColorTexture":
|
|
1475
|
-
return "color";
|
|
1476
|
-
case "metallicRoughnessTexture":
|
|
1477
|
-
return "orm";
|
|
1478
|
-
default:
|
|
1479
|
-
return "normal";
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
function inferModelExtension(path, modelFormat) {
|
|
1483
|
-
if (typeof modelFormat === "string" && modelFormat.length > 0) {
|
|
1484
|
-
return modelFormat.toLowerCase();
|
|
1485
|
-
}
|
|
1486
|
-
return inferExtensionFromPath(path) ?? "bin";
|
|
1487
|
-
}
|
|
1488
|
-
function inferExtension(mimeType, fallback) {
|
|
1489
|
-
const normalized = mimeType?.toLowerCase();
|
|
1490
|
-
if (normalized === "image/png") {
|
|
1491
|
-
return "png";
|
|
1492
|
-
}
|
|
1493
|
-
if (normalized === "image/jpeg") {
|
|
1494
|
-
return "jpg";
|
|
1495
|
-
}
|
|
1496
|
-
if (normalized === "image/svg+xml") {
|
|
1497
|
-
return "svg";
|
|
1498
|
-
}
|
|
1499
|
-
if (normalized === "image/vnd.radiance") {
|
|
1500
|
-
return "hdr";
|
|
1501
|
-
}
|
|
1502
|
-
if (normalized === "model/gltf+json") {
|
|
1503
|
-
return "gltf";
|
|
1504
|
-
}
|
|
1505
|
-
if (normalized === "model/gltf-binary" || normalized === "application/octet-stream") {
|
|
1506
|
-
return fallback ?? "bin";
|
|
1507
|
-
}
|
|
1508
|
-
return fallback ?? "bin";
|
|
1509
|
-
}
|
|
1510
|
-
function inferExtensionFromPath(path) {
|
|
1511
|
-
const cleanPath = path.split("?")[0]?.split("#")[0] ?? path;
|
|
1512
|
-
const parts = cleanPath.split(".");
|
|
1513
|
-
return parts.length > 1 ? parts.at(-1)?.toLowerCase() : void 0;
|
|
1514
|
-
}
|
|
1515
|
-
function inferMimeTypeFromPath(path) {
|
|
1516
|
-
switch (inferExtensionFromPath(path)) {
|
|
1517
|
-
case "png":
|
|
1518
|
-
return "image/png";
|
|
1519
|
-
case "jpg":
|
|
1520
|
-
case "jpeg":
|
|
1521
|
-
return "image/jpeg";
|
|
1522
|
-
case "svg":
|
|
1523
|
-
return "image/svg+xml";
|
|
1524
|
-
case "hdr":
|
|
1525
|
-
return "image/vnd.radiance";
|
|
1526
|
-
case "glb":
|
|
1527
|
-
return "model/gltf-binary";
|
|
1528
|
-
case "gltf":
|
|
1529
|
-
return "model/gltf+json";
|
|
1530
|
-
case "obj":
|
|
1531
|
-
return "text/plain";
|
|
1532
|
-
case "mtl":
|
|
1533
|
-
return "text/plain";
|
|
1534
|
-
case "json":
|
|
1535
|
-
return "application/json";
|
|
1536
|
-
default:
|
|
1537
|
-
return "application/octet-stream";
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
function ensureUniquePath(path, usedPaths) {
|
|
1541
|
-
if (!usedPaths.has(path)) {
|
|
1542
|
-
usedPaths.add(path);
|
|
1543
|
-
return path;
|
|
1544
|
-
}
|
|
1545
|
-
const lastDot = path.lastIndexOf(".");
|
|
1546
|
-
const stem = lastDot >= 0 ? path.slice(0, lastDot) : path;
|
|
1547
|
-
const extension = lastDot >= 0 ? path.slice(lastDot) : "";
|
|
1548
|
-
let counter = 2;
|
|
1549
|
-
while (usedPaths.has(`${stem}-${counter}${extension}`)) {
|
|
1550
|
-
counter += 1;
|
|
1551
|
-
}
|
|
1552
|
-
const resolved = `${stem}-${counter}${extension}`;
|
|
1553
|
-
usedPaths.add(resolved);
|
|
1554
|
-
return resolved;
|
|
1555
|
-
}
|
|
1556
|
-
function isDataUrl(value) {
|
|
1557
|
-
return value.startsWith("data:");
|
|
1558
|
-
}
|
|
1559
|
-
function slugify(value) {
|
|
1560
|
-
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1561
|
-
return normalized || "asset";
|
|
1562
|
-
}
|
|
1563
|
-
function trimSlashes(value) {
|
|
1564
|
-
return value.replace(/^\/+|\/+$/g, "");
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// ../geometry-kernel/src/polygon/polygon-utils.ts
|
|
1568
|
-
import earcut from "earcut";
|
|
1569
|
-
function computePolygonNormal(vertices) {
|
|
1570
|
-
if (vertices.length < 3) {
|
|
1571
|
-
return vec3(0, 1, 0);
|
|
1572
|
-
}
|
|
1573
|
-
let normal = vec3(0, 0, 0);
|
|
1574
|
-
for (let index = 0; index < vertices.length; index += 1) {
|
|
1575
|
-
const current = vertices[index];
|
|
1576
|
-
const next = vertices[(index + 1) % vertices.length];
|
|
1577
|
-
normal = addVec3(
|
|
1578
|
-
normal,
|
|
1579
|
-
vec3(
|
|
1580
|
-
(current.y - next.y) * (current.z + next.z),
|
|
1581
|
-
(current.z - next.z) * (current.x + next.x),
|
|
1582
|
-
(current.x - next.x) * (current.y + next.y)
|
|
1583
|
-
)
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
const normalized = normalizeVec3(normal);
|
|
1587
|
-
return lengthVec3(normalized) === 0 ? vec3(0, 1, 0) : normalized;
|
|
1588
|
-
}
|
|
1589
|
-
function projectPolygonToPlane(vertices, normal = computePolygonNormal(vertices)) {
|
|
1590
|
-
const origin = averageVec3(vertices);
|
|
1591
|
-
const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
1592
|
-
let tangent = normalizeVec3(crossVec3(tangentReference, normal));
|
|
1593
|
-
if (lengthVec3(tangent) === 0) {
|
|
1594
|
-
tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
|
|
1595
|
-
}
|
|
1596
|
-
const bitangent = normalizeVec3(crossVec3(normal, tangent));
|
|
1597
|
-
return vertices.map((vertex) => {
|
|
1598
|
-
const offset = subVec3(vertex, origin);
|
|
1599
|
-
return [dotVec3(offset, tangent), dotVec3(offset, bitangent)];
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
function polygonSignedArea(points) {
|
|
1603
|
-
let area = 0;
|
|
1604
|
-
for (let index = 0; index < points.length; index += 1) {
|
|
1605
|
-
const [x1, y1] = points[index];
|
|
1606
|
-
const [x2, y2] = points[(index + 1) % points.length];
|
|
1607
|
-
area += x1 * y2 - x2 * y1;
|
|
1608
|
-
}
|
|
1609
|
-
return area * 0.5;
|
|
1610
|
-
}
|
|
1611
|
-
function sortVerticesOnPlane(vertices, normal) {
|
|
1612
|
-
const center = averageVec3(vertices);
|
|
1613
|
-
const tangentReference = Math.abs(normal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
1614
|
-
let tangent = normalizeVec3(crossVec3(tangentReference, normal));
|
|
1615
|
-
if (lengthVec3(tangent) === 0) {
|
|
1616
|
-
tangent = normalizeVec3(crossVec3(vec3(0, 0, 1), normal));
|
|
1617
|
-
}
|
|
1618
|
-
const bitangent = normalizeVec3(crossVec3(normal, tangent));
|
|
1619
|
-
const sorted = [...vertices].sort((left, right) => {
|
|
1620
|
-
const leftOffset = subVec3(left, center);
|
|
1621
|
-
const rightOffset = subVec3(right, center);
|
|
1622
|
-
const leftAngle = Math.atan2(dotVec3(leftOffset, bitangent), dotVec3(leftOffset, tangent));
|
|
1623
|
-
const rightAngle = Math.atan2(dotVec3(rightOffset, bitangent), dotVec3(rightOffset, tangent));
|
|
1624
|
-
return leftAngle - rightAngle;
|
|
1625
|
-
});
|
|
1626
|
-
if (sorted.length < 3) {
|
|
1627
|
-
return sorted;
|
|
1628
|
-
}
|
|
1629
|
-
const windingNormal = normalizeVec3(
|
|
1630
|
-
crossVec3(subVec3(sorted[1], sorted[0]), subVec3(sorted[2], sorted[0]))
|
|
1631
|
-
);
|
|
1632
|
-
return dotVec3(windingNormal, normal) < 0 ? sorted.reverse() : sorted;
|
|
1633
|
-
}
|
|
1634
|
-
function triangulatePolygon(points) {
|
|
1635
|
-
const flattened = points.flatMap(([x, y]) => [x, y]);
|
|
1636
|
-
return earcut(flattened);
|
|
1637
|
-
}
|
|
1638
|
-
function triangulatePolygon3D(vertices, normal = computePolygonNormal(vertices)) {
|
|
1639
|
-
if (vertices.length < 3) {
|
|
1640
|
-
return [];
|
|
1641
|
-
}
|
|
1642
|
-
const projected = projectPolygonToPlane(vertices, normal);
|
|
1643
|
-
if (Math.abs(polygonSignedArea(projected)) <= 1e-6) {
|
|
1644
|
-
return [];
|
|
1645
|
-
}
|
|
1646
|
-
return triangulatePolygon(projected);
|
|
1647
|
-
}
|
|
1648
|
-
function computeFaceCenter(vertices) {
|
|
1649
|
-
return averageVec3(vertices);
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// ../geometry-kernel/src/brush/brush-kernel.ts
|
|
1653
|
-
function reconstructBrushFaces(brush, epsilon = 1e-4) {
|
|
1654
|
-
if (brush.planes.length < 4) {
|
|
1655
|
-
return {
|
|
1656
|
-
faces: [],
|
|
1657
|
-
vertices: [],
|
|
1658
|
-
valid: false,
|
|
1659
|
-
errors: ["Brush reconstruction requires at least four planes."]
|
|
1660
|
-
};
|
|
1661
|
-
}
|
|
1662
|
-
const vertexRegistry = /* @__PURE__ */ new Map();
|
|
1663
|
-
const faces = [];
|
|
1664
|
-
for (let planeIndex = 0; planeIndex < brush.planes.length; planeIndex += 1) {
|
|
1665
|
-
const plane = brush.planes[planeIndex];
|
|
1666
|
-
const faceVertices = collectFaceVertices(brush.planes, planeIndex, epsilon);
|
|
1667
|
-
if (faceVertices.length < 3) {
|
|
1668
|
-
continue;
|
|
1669
|
-
}
|
|
1670
|
-
const orderedVertices = sortVerticesOnPlane(faceVertices, normalizeVec3(plane.normal));
|
|
1671
|
-
const triangleIndices = triangulatePolygon3D(orderedVertices, plane.normal);
|
|
1672
|
-
if (triangleIndices.length < 3) {
|
|
1673
|
-
continue;
|
|
1674
|
-
}
|
|
1675
|
-
const vertices = orderedVertices.map((position) => registerBrushVertex(vertexRegistry, position, epsilon));
|
|
1676
|
-
const seedFace = brush.faces[planeIndex];
|
|
1677
|
-
faces.push({
|
|
1678
|
-
id: seedFace?.id ?? `face:brush:${planeIndex}`,
|
|
1679
|
-
plane,
|
|
1680
|
-
materialId: seedFace?.materialId,
|
|
1681
|
-
uvOffset: seedFace?.uvOffset,
|
|
1682
|
-
uvScale: seedFace?.uvScale,
|
|
1683
|
-
vertexIds: vertices.map((vertex) => vertex.id),
|
|
1684
|
-
vertices,
|
|
1685
|
-
center: computeFaceCenter(orderedVertices),
|
|
1686
|
-
normal: normalizeVec3(plane.normal),
|
|
1687
|
-
triangleIndices
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
return {
|
|
1691
|
-
faces,
|
|
1692
|
-
vertices: Array.from(vertexRegistry.values()),
|
|
1693
|
-
valid: faces.length >= 4,
|
|
1694
|
-
errors: faces.length >= 4 ? [] : ["Brush reconstruction did not produce a closed convex solid."]
|
|
1695
|
-
};
|
|
1696
|
-
}
|
|
1697
|
-
function classifyPointAgainstPlane(point, plane, epsilon = 1e-4) {
|
|
1698
|
-
const signedDistance = signedDistanceToPlane(point, plane);
|
|
1699
|
-
return signedDistance > epsilon ? "outside" : "inside";
|
|
1700
|
-
}
|
|
1701
|
-
function signedDistanceToPlane(point, plane) {
|
|
1702
|
-
return dotVec3(plane.normal, point) - plane.distance;
|
|
1703
|
-
}
|
|
1704
|
-
function intersectPlanes(first, second, third, epsilon = 1e-6) {
|
|
1705
|
-
const denominator = dotVec3(first.normal, crossVec3(second.normal, third.normal));
|
|
1706
|
-
if (Math.abs(denominator) <= epsilon) {
|
|
1707
|
-
return void 0;
|
|
1708
|
-
}
|
|
1709
|
-
const firstTerm = scaleVec3(crossVec3(second.normal, third.normal), first.distance);
|
|
1710
|
-
const secondTerm = scaleVec3(crossVec3(third.normal, first.normal), second.distance);
|
|
1711
|
-
const thirdTerm = scaleVec3(crossVec3(first.normal, second.normal), third.distance);
|
|
1712
|
-
return scaleVec3(addVec3(addVec3(firstTerm, secondTerm), thirdTerm), 1 / denominator);
|
|
1713
|
-
}
|
|
1714
|
-
function collectFaceVertices(planes, planeIndex, epsilon) {
|
|
1715
|
-
const plane = planes[planeIndex];
|
|
1716
|
-
const vertices = /* @__PURE__ */ new Map();
|
|
1717
|
-
for (let firstIndex = 0; firstIndex < planes.length; firstIndex += 1) {
|
|
1718
|
-
if (firstIndex === planeIndex) {
|
|
1719
|
-
continue;
|
|
1720
|
-
}
|
|
1721
|
-
for (let secondIndex = firstIndex + 1; secondIndex < planes.length; secondIndex += 1) {
|
|
1722
|
-
if (secondIndex === planeIndex) {
|
|
1723
|
-
continue;
|
|
1724
|
-
}
|
|
1725
|
-
const intersection = intersectPlanes(plane, planes[firstIndex], planes[secondIndex], epsilon);
|
|
1726
|
-
if (!intersection) {
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
const liesOnPlane = Math.abs(signedDistanceToPlane(intersection, plane)) <= epsilon * 4;
|
|
1730
|
-
const insideAllPlanes = planes.every(
|
|
1731
|
-
(candidatePlane) => classifyPointAgainstPlane(intersection, candidatePlane, epsilon * 4) === "inside"
|
|
1732
|
-
);
|
|
1733
|
-
if (!liesOnPlane || !insideAllPlanes) {
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
vertices.set(makeVertexKey(intersection, epsilon), intersection);
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
return Array.from(vertices.values());
|
|
1740
|
-
}
|
|
1741
|
-
function registerBrushVertex(registry, position, epsilon) {
|
|
1742
|
-
const key = makeVertexKey(position, epsilon);
|
|
1743
|
-
const existing = registry.get(key);
|
|
1744
|
-
if (existing) {
|
|
1745
|
-
return existing;
|
|
1746
|
-
}
|
|
1747
|
-
const vertex = {
|
|
1748
|
-
id: `vertex:brush:${registry.size}`,
|
|
1749
|
-
position: vec3(position.x, position.y, position.z)
|
|
1750
|
-
};
|
|
1751
|
-
registry.set(key, vertex);
|
|
1752
|
-
return vertex;
|
|
1753
|
-
}
|
|
1754
|
-
function makeVertexKey(position, epsilon) {
|
|
1755
|
-
return [
|
|
1756
|
-
Math.round(position.x / epsilon),
|
|
1757
|
-
Math.round(position.y / epsilon),
|
|
1758
|
-
Math.round(position.z / epsilon)
|
|
1759
|
-
].join(":");
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
// ../geometry-kernel/src/mesh/editable-mesh.ts
|
|
1763
|
-
var editableMeshIndexCache = /* @__PURE__ */ new WeakMap();
|
|
1764
|
-
function getFaceVertexIds(mesh, faceId) {
|
|
1765
|
-
const index = getEditableMeshIndex(mesh);
|
|
1766
|
-
const cachedIds = index.faceVertexIds.get(faceId);
|
|
1767
|
-
if (cachedIds) {
|
|
1768
|
-
return cachedIds;
|
|
1769
|
-
}
|
|
1770
|
-
const face = index.faceById.get(faceId);
|
|
1771
|
-
if (!face) {
|
|
1772
|
-
return [];
|
|
1773
|
-
}
|
|
1774
|
-
const ids = [];
|
|
1775
|
-
let currentEdgeId = face.halfEdge;
|
|
1776
|
-
let guard = 0;
|
|
1777
|
-
while (currentEdgeId && guard < mesh.halfEdges.length + 1) {
|
|
1778
|
-
const halfEdge = index.halfEdgeById.get(currentEdgeId);
|
|
1779
|
-
if (!halfEdge) {
|
|
1780
|
-
return [];
|
|
1781
|
-
}
|
|
1782
|
-
ids.push(halfEdge.vertex);
|
|
1783
|
-
currentEdgeId = halfEdge.next;
|
|
1784
|
-
guard += 1;
|
|
1785
|
-
if (currentEdgeId === face.halfEdge) {
|
|
1786
|
-
break;
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
index.faceVertexIds.set(faceId, ids);
|
|
1790
|
-
return ids;
|
|
1791
|
-
}
|
|
1792
|
-
function getFaceVertices(mesh, faceId) {
|
|
1793
|
-
const index = getEditableMeshIndex(mesh);
|
|
1794
|
-
return getFaceVertexIds(mesh, faceId).map((vertexId) => index.vertexById.get(vertexId)).filter((vertex) => Boolean(vertex));
|
|
1795
|
-
}
|
|
1796
|
-
function triangulateMeshFace(mesh, faceId) {
|
|
1797
|
-
const faceVertices = getFaceVertices(mesh, faceId);
|
|
1798
|
-
if (faceVertices.length < 3) {
|
|
1799
|
-
return void 0;
|
|
1800
|
-
}
|
|
1801
|
-
const normal = computePolygonNormal(faceVertices.map((vertex) => vertex.position));
|
|
1802
|
-
const indices = triangulatePolygon3D(
|
|
1803
|
-
faceVertices.map((vertex) => vertex.position),
|
|
1804
|
-
normal
|
|
1805
|
-
);
|
|
1806
|
-
if (indices.length < 3) {
|
|
1807
|
-
return void 0;
|
|
1808
|
-
}
|
|
1809
|
-
return {
|
|
1810
|
-
faceId,
|
|
1811
|
-
vertexIds: faceVertices.map((vertex) => vertex.id),
|
|
1812
|
-
normal,
|
|
1813
|
-
indices
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
function getEditableMeshIndex(mesh) {
|
|
1817
|
-
const cached = editableMeshIndexCache.get(mesh);
|
|
1818
|
-
if (cached && cached.faces === mesh.faces && cached.halfEdges === mesh.halfEdges && cached.vertices === mesh.vertices) {
|
|
1819
|
-
return cached;
|
|
1820
|
-
}
|
|
1821
|
-
const nextIndex = {
|
|
1822
|
-
faceById: new Map(mesh.faces.map((face) => [face.id, face])),
|
|
1823
|
-
faceVertexIds: /* @__PURE__ */ new Map(),
|
|
1824
|
-
faces: mesh.faces,
|
|
1825
|
-
halfEdgeById: new Map(mesh.halfEdges.map((halfEdge) => [halfEdge.id, halfEdge])),
|
|
1826
|
-
halfEdges: mesh.halfEdges,
|
|
1827
|
-
vertexById: new Map(mesh.vertices.map((vertex) => [vertex.id, vertex])),
|
|
1828
|
-
vertices: mesh.vertices
|
|
1829
|
-
};
|
|
1830
|
-
editableMeshIndexCache.set(mesh, nextIndex);
|
|
1831
|
-
return nextIndex;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// ../runtime-build/src/snapshot-build.ts
|
|
1835
|
-
import { MeshBVH } from "three-mesh-bvh";
|
|
1836
|
-
import {
|
|
1837
|
-
Box3 as Box33,
|
|
1838
|
-
BoxGeometry as BoxGeometry3,
|
|
1839
|
-
BufferGeometry as BufferGeometry3,
|
|
1840
|
-
ConeGeometry,
|
|
1841
|
-
Float32BufferAttribute as Float32BufferAttribute3,
|
|
1842
|
-
Group as Group3,
|
|
1843
|
-
Mesh as Mesh3,
|
|
1844
|
-
MeshStandardMaterial as MeshStandardMaterial3,
|
|
1845
|
-
RepeatWrapping as RepeatWrapping3,
|
|
1846
|
-
Scene as Scene2,
|
|
1847
|
-
SphereGeometry,
|
|
1848
|
-
SRGBColorSpace as SRGBColorSpace3,
|
|
1849
|
-
TextureLoader as TextureLoader3,
|
|
1850
|
-
Vector3 as Vector34,
|
|
1851
|
-
CylinderGeometry
|
|
1852
|
-
} from "three";
|
|
1853
|
-
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
1854
|
-
import { GLTFLoader as GLTFLoader3 } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
1855
|
-
import { MTLLoader as MTLLoader3 } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
1856
|
-
import { OBJLoader as OBJLoader3 } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
1857
|
-
var gltfLoader3 = new GLTFLoader3();
|
|
1858
|
-
var gltfExporter = new GLTFExporter();
|
|
1859
|
-
var mtlLoader3 = new MTLLoader3();
|
|
1860
|
-
var modelTextureLoader2 = new TextureLoader3();
|
|
1861
|
-
async function buildRuntimeScene(input) {
|
|
1862
|
-
if (typeof input === "string") {
|
|
1863
|
-
return parseRuntimeScene(input);
|
|
1864
|
-
}
|
|
1865
|
-
if (isSceneDocumentSnapshotLike(input)) {
|
|
1866
|
-
return buildRuntimeSceneFromSnapshot(input);
|
|
1867
|
-
}
|
|
1868
|
-
return normalizeRuntimeScene(input);
|
|
1869
|
-
}
|
|
1870
|
-
async function buildRuntimeSceneFromSnapshot(snapshot) {
|
|
1871
|
-
const assetsById = new Map(snapshot.assets.map((asset) => [asset.id, asset]));
|
|
1872
|
-
const materialsById = new Map(snapshot.materials.map((material) => [material.id, material]));
|
|
1873
|
-
const exportedMaterials = await Promise.all(snapshot.materials.map((material) => resolveRuntimeMaterial(material)));
|
|
1874
|
-
const shouldBakeLods = snapshot.settings.world.lod.enabled;
|
|
1875
|
-
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1876
|
-
const exportedSettings = shouldBakeLods ? {
|
|
1877
|
-
...snapshot.settings,
|
|
1878
|
-
world: {
|
|
1879
|
-
...snapshot.settings.world,
|
|
1880
|
-
lod: {
|
|
1881
|
-
...snapshot.settings.world.lod,
|
|
1882
|
-
bakedAt: exportedAt
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
} : snapshot.settings;
|
|
1886
|
-
const generatedAssets = [];
|
|
1887
|
-
const exportedNodes = [];
|
|
1888
|
-
for (const node of snapshot.nodes) {
|
|
1889
|
-
if (isGroupNode(node)) {
|
|
1890
|
-
exportedNodes.push({
|
|
1891
|
-
data: node.data,
|
|
1892
|
-
hooks: node.hooks,
|
|
1893
|
-
id: node.id,
|
|
1894
|
-
kind: "group",
|
|
1895
|
-
metadata: node.metadata,
|
|
1896
|
-
name: node.name,
|
|
1897
|
-
parentId: node.parentId,
|
|
1898
|
-
tags: node.tags,
|
|
1899
|
-
transform: node.transform
|
|
1900
|
-
});
|
|
1901
|
-
continue;
|
|
1902
|
-
}
|
|
1903
|
-
if (isBrushNode(node)) {
|
|
1904
|
-
const geometry = await buildExportGeometry(node, materialsById);
|
|
1905
|
-
exportedNodes.push({
|
|
1906
|
-
data: node.data,
|
|
1907
|
-
geometry,
|
|
1908
|
-
hooks: node.hooks,
|
|
1909
|
-
id: node.id,
|
|
1910
|
-
kind: "brush",
|
|
1911
|
-
lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
|
|
1912
|
-
metadata: node.metadata,
|
|
1913
|
-
name: node.name,
|
|
1914
|
-
parentId: node.parentId,
|
|
1915
|
-
tags: node.tags,
|
|
1916
|
-
transform: node.transform
|
|
1917
|
-
});
|
|
1918
|
-
continue;
|
|
1919
|
-
}
|
|
1920
|
-
if (isMeshNode(node)) {
|
|
1921
|
-
const geometry = await buildExportGeometry(node, materialsById);
|
|
1922
|
-
exportedNodes.push({
|
|
1923
|
-
data: node.data,
|
|
1924
|
-
geometry,
|
|
1925
|
-
hooks: node.hooks,
|
|
1926
|
-
id: node.id,
|
|
1927
|
-
kind: "mesh",
|
|
1928
|
-
lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
|
|
1929
|
-
metadata: node.metadata,
|
|
1930
|
-
name: node.name,
|
|
1931
|
-
parentId: node.parentId,
|
|
1932
|
-
tags: node.tags,
|
|
1933
|
-
transform: node.transform
|
|
1934
|
-
});
|
|
1935
|
-
continue;
|
|
1936
|
-
}
|
|
1937
|
-
if (isPrimitiveNode(node)) {
|
|
1938
|
-
const geometry = await buildExportGeometry(node, materialsById);
|
|
1939
|
-
exportedNodes.push({
|
|
1940
|
-
data: node.data,
|
|
1941
|
-
geometry,
|
|
1942
|
-
hooks: node.hooks,
|
|
1943
|
-
id: node.id,
|
|
1944
|
-
kind: "primitive",
|
|
1945
|
-
lods: shouldBakeLods ? await buildGeometryLods(geometry, snapshot.settings.world.lod) : void 0,
|
|
1946
|
-
metadata: node.metadata,
|
|
1947
|
-
name: node.name,
|
|
1948
|
-
parentId: node.parentId,
|
|
1949
|
-
tags: node.tags,
|
|
1950
|
-
transform: node.transform
|
|
1951
|
-
});
|
|
1952
|
-
continue;
|
|
1953
|
-
}
|
|
1954
|
-
if (isModelNode(node)) {
|
|
1955
|
-
const modelLodBake = shouldBakeLods ? await buildModelLods(node.name, assetsById.get(node.data.assetId), node.id, snapshot.settings.world.lod) : void 0;
|
|
1956
|
-
generatedAssets.push(...modelLodBake?.assets ?? []);
|
|
1957
|
-
exportedNodes.push({
|
|
1958
|
-
data: node.data,
|
|
1959
|
-
hooks: node.hooks,
|
|
1960
|
-
id: node.id,
|
|
1961
|
-
kind: "model",
|
|
1962
|
-
lods: modelLodBake?.lods,
|
|
1963
|
-
metadata: node.metadata,
|
|
1964
|
-
name: node.name,
|
|
1965
|
-
parentId: node.parentId,
|
|
1966
|
-
tags: node.tags,
|
|
1967
|
-
transform: node.transform
|
|
1968
|
-
});
|
|
1969
|
-
continue;
|
|
1970
|
-
}
|
|
1971
|
-
if (isInstancingNode(node)) {
|
|
1972
|
-
const sourceNode = resolveInstancingSourceNode(snapshot.nodes, node);
|
|
1973
|
-
if (!sourceNode || !(isBrushNode(sourceNode) || isMeshNode(sourceNode) || isPrimitiveNode(sourceNode) || isModelNode(sourceNode))) {
|
|
1974
|
-
continue;
|
|
1975
|
-
}
|
|
1976
|
-
exportedNodes.push({
|
|
1977
|
-
data: {
|
|
1978
|
-
sourceNodeId: sourceNode.id
|
|
1979
|
-
},
|
|
1980
|
-
hooks: node.hooks,
|
|
1981
|
-
id: node.id,
|
|
1982
|
-
kind: "instancing",
|
|
1983
|
-
metadata: node.metadata,
|
|
1984
|
-
name: node.name,
|
|
1985
|
-
parentId: node.parentId,
|
|
1986
|
-
tags: node.tags,
|
|
1987
|
-
transform: sanitizeInstanceTransform(node.transform)
|
|
1988
|
-
});
|
|
1989
|
-
continue;
|
|
1990
|
-
}
|
|
1991
|
-
exportedNodes.push({
|
|
1992
|
-
data: node.data,
|
|
1993
|
-
id: node.id,
|
|
1994
|
-
kind: "light",
|
|
1995
|
-
metadata: node.metadata,
|
|
1996
|
-
name: node.name,
|
|
1997
|
-
parentId: node.parentId,
|
|
1998
|
-
tags: node.tags,
|
|
1999
|
-
transform: node.transform
|
|
2000
|
-
});
|
|
2001
|
-
}
|
|
2002
|
-
return {
|
|
2003
|
-
assets: [...snapshot.assets, ...generatedAssets],
|
|
2004
|
-
entities: snapshot.entities,
|
|
2005
|
-
layers: snapshot.layers,
|
|
2006
|
-
materials: exportedMaterials,
|
|
2007
|
-
metadata: {
|
|
2008
|
-
exportedAt,
|
|
2009
|
-
format: "web-hammer-engine",
|
|
2010
|
-
version: CURRENT_RUNTIME_SCENE_VERSION
|
|
2011
|
-
},
|
|
2012
|
-
nodes: exportedNodes,
|
|
2013
|
-
settings: exportedSettings
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
function isSceneDocumentSnapshotLike(value) {
|
|
2017
|
-
return Boolean(
|
|
2018
|
-
value && typeof value === "object" && Array.isArray(value.nodes) && Array.isArray(value.materials) && Array.isArray(value.textures)
|
|
2019
|
-
);
|
|
2020
|
-
}
|
|
2021
|
-
async function buildExportGeometry(node, materialsById) {
|
|
2022
|
-
const fallbackMaterial = await resolveRuntimeMaterial({
|
|
2023
|
-
color: node.kind === "brush" ? "#f69036" : node.kind === "primitive" && node.data.role === "prop" ? "#7f8ea3" : "#6ed5c0",
|
|
2024
|
-
id: `material:fallback:${node.id}`,
|
|
2025
|
-
metalness: node.kind === "brush" ? 0 : node.kind === "primitive" && node.data.role === "prop" ? 0.12 : 0.05,
|
|
2026
|
-
name: `${node.name} Default`,
|
|
2027
|
-
roughness: node.kind === "brush" ? 0.95 : node.kind === "primitive" && node.data.role === "prop" ? 0.64 : 0.82
|
|
2028
|
-
});
|
|
2029
|
-
const primitiveByMaterial = /* @__PURE__ */ new Map();
|
|
2030
|
-
const appendFace = async (params) => {
|
|
2031
|
-
const material = params.faceMaterialId ? await resolveRuntimeMaterial(materialsById.get(params.faceMaterialId)) : fallbackMaterial;
|
|
2032
|
-
const primitive = primitiveByMaterial.get(material.id) ?? {
|
|
2033
|
-
indices: [],
|
|
2034
|
-
material,
|
|
2035
|
-
normals: [],
|
|
2036
|
-
positions: [],
|
|
2037
|
-
uvs: []
|
|
2038
|
-
};
|
|
2039
|
-
const vertexOffset = primitive.positions.length / 3;
|
|
2040
|
-
const uvs = params.uvs && params.uvs.length === params.vertices.length ? params.uvs.flatMap((uv) => [uv.x, uv.y]) : projectPlanarUvs(params.vertices, params.normal, params.uvScale, params.uvOffset);
|
|
2041
|
-
params.vertices.forEach((vertex) => {
|
|
2042
|
-
primitive.positions.push(vertex.x, vertex.y, vertex.z);
|
|
2043
|
-
primitive.normals.push(params.normal.x, params.normal.y, params.normal.z);
|
|
2044
|
-
});
|
|
2045
|
-
primitive.uvs.push(...uvs);
|
|
2046
|
-
params.triangleIndices.forEach((index) => {
|
|
2047
|
-
primitive.indices.push(vertexOffset + index);
|
|
2048
|
-
});
|
|
2049
|
-
primitiveByMaterial.set(material.id, primitive);
|
|
2050
|
-
};
|
|
2051
|
-
if (isBrushNode(node)) {
|
|
2052
|
-
const rebuilt = reconstructBrushFaces(node.data);
|
|
2053
|
-
if (!rebuilt.valid) {
|
|
2054
|
-
return { primitives: [] };
|
|
2055
|
-
}
|
|
2056
|
-
for (const face of rebuilt.faces) {
|
|
2057
|
-
await appendFace({
|
|
2058
|
-
faceMaterialId: face.materialId,
|
|
2059
|
-
normal: face.normal,
|
|
2060
|
-
triangleIndices: face.triangleIndices,
|
|
2061
|
-
uvOffset: face.uvOffset,
|
|
2062
|
-
uvScale: face.uvScale,
|
|
2063
|
-
vertices: face.vertices.map((vertex) => vertex.position)
|
|
2064
|
-
});
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
if (isMeshNode(node)) {
|
|
2068
|
-
for (const face of node.data.faces) {
|
|
2069
|
-
const triangulated = triangulateMeshFace(node.data, face.id);
|
|
2070
|
-
if (!triangulated) {
|
|
2071
|
-
continue;
|
|
2072
|
-
}
|
|
2073
|
-
await appendFace({
|
|
2074
|
-
faceMaterialId: face.materialId,
|
|
2075
|
-
normal: triangulated.normal,
|
|
2076
|
-
triangleIndices: triangulated.indices,
|
|
2077
|
-
uvOffset: face.uvOffset,
|
|
2078
|
-
uvScale: face.uvScale,
|
|
2079
|
-
uvs: face.uvs,
|
|
2080
|
-
vertices: getFaceVertices(node.data, face.id).map((vertex) => vertex.position)
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2084
|
-
if (isPrimitiveNode(node)) {
|
|
2085
|
-
const material = node.data.materialId ? await resolveRuntimeMaterial(materialsById.get(node.data.materialId)) : fallbackMaterial;
|
|
2086
|
-
const primitive = buildPrimitiveGeometry(node.data.shape, node.data.size, node.data.radialSegments ?? 24);
|
|
2087
|
-
if (primitive) {
|
|
2088
|
-
primitiveByMaterial.set(material.id, {
|
|
2089
|
-
indices: primitive.indices,
|
|
2090
|
-
material,
|
|
2091
|
-
normals: primitive.normals,
|
|
2092
|
-
positions: primitive.positions,
|
|
2093
|
-
uvs: primitive.uvs
|
|
2094
|
-
});
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
return {
|
|
2098
|
-
primitives: Array.from(primitiveByMaterial.values())
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
async function buildGeometryLods(geometry, settings) {
|
|
2102
|
-
if (!geometry.primitives.length) {
|
|
2103
|
-
return void 0;
|
|
2104
|
-
}
|
|
2105
|
-
const midGeometry = simplifyExportGeometry(geometry, settings.midDetailRatio);
|
|
2106
|
-
const lowGeometry = simplifyExportGeometry(geometry, settings.lowDetailRatio);
|
|
2107
|
-
const lods = [];
|
|
2108
|
-
if (midGeometry) {
|
|
2109
|
-
lods.push({
|
|
2110
|
-
geometry: midGeometry,
|
|
2111
|
-
level: "mid"
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
if (lowGeometry) {
|
|
2115
|
-
lods.push({
|
|
2116
|
-
geometry: lowGeometry,
|
|
2117
|
-
level: "low"
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
return lods.length ? lods : void 0;
|
|
2121
|
-
}
|
|
2122
|
-
async function buildModelLods(name, asset, nodeId, settings) {
|
|
2123
|
-
if (!asset?.path) {
|
|
2124
|
-
return { assets: [], lods: void 0 };
|
|
2125
|
-
}
|
|
2126
|
-
const source = await loadModelSceneForLodBake(asset);
|
|
2127
|
-
const bakedLevels = [];
|
|
2128
|
-
for (const [level, ratio] of [
|
|
2129
|
-
["mid", settings.midDetailRatio],
|
|
2130
|
-
["low", settings.lowDetailRatio]
|
|
2131
|
-
]) {
|
|
2132
|
-
const simplified = simplifyModelSceneForRatio(source, ratio);
|
|
2133
|
-
if (!simplified) {
|
|
2134
|
-
continue;
|
|
2135
|
-
}
|
|
2136
|
-
const bytes = await exportModelSceneAsGlb(simplified);
|
|
2137
|
-
bakedLevels.push({
|
|
2138
|
-
asset: createGeneratedModelLodAsset(asset, name, nodeId, level, bytes),
|
|
2139
|
-
level
|
|
2140
|
-
});
|
|
2141
|
-
}
|
|
2142
|
-
return {
|
|
2143
|
-
assets: bakedLevels.map((entry) => entry.asset),
|
|
2144
|
-
lods: bakedLevels.length ? bakedLevels.map((entry) => ({
|
|
2145
|
-
assetId: entry.asset.id,
|
|
2146
|
-
level: entry.level
|
|
2147
|
-
})) : void 0
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
async function loadModelSceneForLodBake(asset) {
|
|
2151
|
-
const format = resolveModelAssetFormat(asset);
|
|
2152
|
-
if (format === "obj") {
|
|
2153
|
-
const objLoader = new OBJLoader3();
|
|
2154
|
-
const texturePath = readModelAssetString(asset, "texturePath");
|
|
2155
|
-
const resolvedTexturePath = typeof texturePath === "string" && texturePath.length > 0 ? texturePath : void 0;
|
|
2156
|
-
const mtlText = readModelAssetString(asset, "materialMtlText");
|
|
2157
|
-
if (mtlText) {
|
|
2158
|
-
const materialCreator = mtlLoader3.parse(patchMtlTextureReferences2(mtlText, resolvedTexturePath), "");
|
|
2159
|
-
materialCreator.preload();
|
|
2160
|
-
objLoader.setMaterials(materialCreator);
|
|
2161
|
-
} else {
|
|
2162
|
-
objLoader.setMaterials(void 0);
|
|
2163
|
-
}
|
|
2164
|
-
const object = await objLoader.loadAsync(asset.path);
|
|
2165
|
-
if (!mtlText && resolvedTexturePath) {
|
|
2166
|
-
const texture = await modelTextureLoader2.loadAsync(resolvedTexturePath);
|
|
2167
|
-
texture.wrapS = RepeatWrapping3;
|
|
2168
|
-
texture.wrapT = RepeatWrapping3;
|
|
2169
|
-
texture.colorSpace = SRGBColorSpace3;
|
|
2170
|
-
object.traverse((child) => {
|
|
2171
|
-
if (child instanceof Mesh3) {
|
|
2172
|
-
child.material = new MeshStandardMaterial3({
|
|
2173
|
-
map: texture,
|
|
2174
|
-
metalness: 0.12,
|
|
2175
|
-
roughness: 0.76
|
|
2176
|
-
});
|
|
2177
|
-
}
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
return object;
|
|
2181
|
-
}
|
|
2182
|
-
return (await gltfLoader3.loadAsync(asset.path)).scene;
|
|
2183
|
-
}
|
|
2184
|
-
function simplifyModelSceneForRatio(source, ratio) {
|
|
2185
|
-
if (ratio >= 0.98) {
|
|
2186
|
-
return void 0;
|
|
2187
|
-
}
|
|
2188
|
-
const simplifiedRoot = source.clone(true);
|
|
2189
|
-
expandGroupedModelMeshesForLodBake(simplifiedRoot);
|
|
2190
|
-
let simplifiedMeshCount = 0;
|
|
2191
|
-
simplifiedRoot.traverse((child) => {
|
|
2192
|
-
if (!(child instanceof Mesh3)) {
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
if ("isSkinnedMesh" in child && child.isSkinnedMesh) {
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
const simplifiedGeometry = simplifyModelGeometry(child.geometry, ratio);
|
|
2199
|
-
if (!simplifiedGeometry) {
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
child.geometry = simplifiedGeometry;
|
|
2203
|
-
simplifiedMeshCount += 1;
|
|
2204
|
-
});
|
|
2205
|
-
return simplifiedMeshCount > 0 ? simplifiedRoot : void 0;
|
|
2206
|
-
}
|
|
2207
|
-
function expandGroupedModelMeshesForLodBake(root) {
|
|
2208
|
-
const replacements = [];
|
|
2209
|
-
root.traverse((child) => {
|
|
2210
|
-
if (!(child instanceof Mesh3) || !Array.isArray(child.material) || child.geometry.groups.length <= 1 || !child.parent) {
|
|
2211
|
-
return;
|
|
2212
|
-
}
|
|
2213
|
-
const container = new Group3();
|
|
2214
|
-
container.name = child.name ? `${child.name}:lod-groups` : "lod-groups";
|
|
2215
|
-
container.position.copy(child.position);
|
|
2216
|
-
container.quaternion.copy(child.quaternion);
|
|
2217
|
-
container.scale.copy(child.scale);
|
|
2218
|
-
container.visible = child.visible;
|
|
2219
|
-
container.renderOrder = child.renderOrder;
|
|
2220
|
-
container.userData = structuredClone(child.userData ?? {});
|
|
2221
|
-
child.geometry.groups.forEach((group, groupIndex) => {
|
|
2222
|
-
const material = child.material[group.materialIndex] ?? child.material[0];
|
|
2223
|
-
if (!material) {
|
|
2224
|
-
return;
|
|
2225
|
-
}
|
|
2226
|
-
const partGeometry = extractGeometryGroup(child.geometry, group.start, group.count);
|
|
2227
|
-
const partMesh = new Mesh3(partGeometry, material);
|
|
2228
|
-
partMesh.name = child.name ? `${child.name}:group:${groupIndex}` : `group:${groupIndex}`;
|
|
2229
|
-
partMesh.castShadow = child.castShadow;
|
|
2230
|
-
partMesh.receiveShadow = child.receiveShadow;
|
|
2231
|
-
partMesh.userData = structuredClone(child.userData ?? {});
|
|
2232
|
-
container.add(partMesh);
|
|
2233
|
-
});
|
|
2234
|
-
replacements.push({
|
|
2235
|
-
container,
|
|
2236
|
-
mesh: child,
|
|
2237
|
-
parent: child.parent
|
|
2238
|
-
});
|
|
2239
|
-
});
|
|
2240
|
-
replacements.forEach(({ container, mesh, parent }) => {
|
|
2241
|
-
parent.add(container);
|
|
2242
|
-
parent.remove(mesh);
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
function extractGeometryGroup(geometry, start, count) {
|
|
2246
|
-
const groupGeometry = new BufferGeometry3();
|
|
2247
|
-
const index = geometry.getIndex();
|
|
2248
|
-
const attributes = geometry.attributes;
|
|
2249
|
-
Object.entries(attributes).forEach(([name, attribute]) => {
|
|
2250
|
-
groupGeometry.setAttribute(name, attribute);
|
|
2251
|
-
});
|
|
2252
|
-
if (index) {
|
|
2253
|
-
groupGeometry.setIndex(Array.from(index.array).slice(start, start + count));
|
|
2254
|
-
} else {
|
|
2255
|
-
groupGeometry.setIndex(Array.from({ length: count }, (_, offset) => start + offset));
|
|
2256
|
-
}
|
|
2257
|
-
groupGeometry.computeBoundingBox();
|
|
2258
|
-
groupGeometry.computeBoundingSphere();
|
|
2259
|
-
return groupGeometry;
|
|
2260
|
-
}
|
|
2261
|
-
function simplifyModelGeometry(geometry, ratio) {
|
|
2262
|
-
const positionAttribute = geometry.getAttribute("position");
|
|
2263
|
-
const vertexCount = positionAttribute?.count ?? 0;
|
|
2264
|
-
if (!positionAttribute || vertexCount < 12 || ratio >= 0.98) {
|
|
2265
|
-
return void 0;
|
|
2266
|
-
}
|
|
2267
|
-
const workingGeometry = geometry.getAttribute("normal") ? geometry : geometry.clone();
|
|
2268
|
-
if (!workingGeometry.getAttribute("normal")) {
|
|
2269
|
-
workingGeometry.computeVertexNormals();
|
|
2270
|
-
}
|
|
2271
|
-
workingGeometry.computeBoundingBox();
|
|
2272
|
-
const bounds = workingGeometry.boundingBox?.clone();
|
|
2273
|
-
if (!bounds) {
|
|
2274
|
-
if (workingGeometry !== geometry) {
|
|
2275
|
-
workingGeometry.dispose();
|
|
2276
|
-
}
|
|
2277
|
-
return void 0;
|
|
2278
|
-
}
|
|
2279
|
-
const normalAttribute = workingGeometry.getAttribute("normal");
|
|
2280
|
-
const uvAttribute = workingGeometry.getAttribute("uv");
|
|
2281
|
-
const index = workingGeometry.getIndex();
|
|
2282
|
-
const simplified = simplifyPrimitiveWithVertexClustering(
|
|
2283
|
-
{
|
|
2284
|
-
indices: index ? Array.from(index.array) : Array.from({ length: vertexCount }, (_, value) => value),
|
|
2285
|
-
material: {
|
|
2286
|
-
color: "#ffffff",
|
|
2287
|
-
id: "material:model-simplify",
|
|
2288
|
-
metallicFactor: 0,
|
|
2289
|
-
name: "Model Simplify",
|
|
2290
|
-
roughnessFactor: 1
|
|
2291
|
-
},
|
|
2292
|
-
normals: Array.from(normalAttribute.array),
|
|
2293
|
-
positions: Array.from(positionAttribute.array),
|
|
2294
|
-
uvs: uvAttribute ? Array.from(uvAttribute.array) : []
|
|
2295
|
-
},
|
|
2296
|
-
ratio,
|
|
2297
|
-
bounds
|
|
2298
|
-
);
|
|
2299
|
-
if (workingGeometry !== geometry) {
|
|
2300
|
-
workingGeometry.dispose();
|
|
2301
|
-
}
|
|
2302
|
-
if (!simplified) {
|
|
2303
|
-
return void 0;
|
|
2304
|
-
}
|
|
2305
|
-
const simplifiedGeometry = createBufferGeometryFromPrimitive(simplified);
|
|
2306
|
-
simplifiedGeometry.computeBoundingBox();
|
|
2307
|
-
simplifiedGeometry.computeBoundingSphere();
|
|
2308
|
-
return simplifiedGeometry;
|
|
2309
|
-
}
|
|
2310
|
-
async function exportModelSceneAsGlb(object) {
|
|
2311
|
-
try {
|
|
2312
|
-
return await exportGlbBytesFromObject(object);
|
|
2313
|
-
} catch {
|
|
2314
|
-
return await exportGlbBytesFromObject(stripTextureReferencesFromObject(object.clone(true)));
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
async function exportGlbBytesFromObject(object) {
|
|
2318
|
-
const scene = new Scene2();
|
|
2319
|
-
scene.add(object);
|
|
2320
|
-
const exported = await gltfExporter.parseAsync(scene, {
|
|
2321
|
-
binary: true,
|
|
2322
|
-
includeCustomExtensions: false
|
|
2323
|
-
});
|
|
2324
|
-
if (!(exported instanceof ArrayBuffer)) {
|
|
2325
|
-
throw new Error("Expected GLB binary output for baked model LOD.");
|
|
2326
|
-
}
|
|
2327
|
-
return new Uint8Array(exported);
|
|
2328
|
-
}
|
|
2329
|
-
function stripTextureReferencesFromObject(object) {
|
|
2330
|
-
object.traverse((child) => {
|
|
2331
|
-
if (!(child instanceof Mesh3)) {
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
const strip = (material) => {
|
|
2335
|
-
const clone = material.clone();
|
|
2336
|
-
clone.alphaMap = null;
|
|
2337
|
-
clone.aoMap = null;
|
|
2338
|
-
clone.bumpMap = null;
|
|
2339
|
-
clone.displacementMap = null;
|
|
2340
|
-
clone.emissiveMap = null;
|
|
2341
|
-
clone.lightMap = null;
|
|
2342
|
-
clone.map = null;
|
|
2343
|
-
clone.metalnessMap = null;
|
|
2344
|
-
clone.normalMap = null;
|
|
2345
|
-
clone.roughnessMap = null;
|
|
2346
|
-
return clone;
|
|
2347
|
-
};
|
|
2348
|
-
if (Array.isArray(child.material)) {
|
|
2349
|
-
child.material = child.material.map(
|
|
2350
|
-
(material) => material instanceof MeshStandardMaterial3 ? strip(material) : new MeshStandardMaterial3({
|
|
2351
|
-
color: "color" in material ? material.color : "#7f8ea3",
|
|
2352
|
-
metalness: "metalness" in material && typeof material.metalness === "number" ? material.metalness : 0.1,
|
|
2353
|
-
roughness: "roughness" in material && typeof material.roughness === "number" ? material.roughness : 0.8
|
|
2354
|
-
})
|
|
2355
|
-
);
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
const fallbackMaterial = child.material;
|
|
2359
|
-
child.material = child.material instanceof MeshStandardMaterial3 ? strip(child.material) : new MeshStandardMaterial3({
|
|
2360
|
-
color: "color" in fallbackMaterial ? fallbackMaterial.color : "#7f8ea3",
|
|
2361
|
-
metalness: "metalness" in fallbackMaterial && typeof fallbackMaterial.metalness === "number" ? fallbackMaterial.metalness : 0.1,
|
|
2362
|
-
roughness: "roughness" in fallbackMaterial && typeof fallbackMaterial.roughness === "number" ? fallbackMaterial.roughness : 0.8
|
|
2363
|
-
});
|
|
2364
|
-
});
|
|
2365
|
-
return object;
|
|
2366
|
-
}
|
|
2367
|
-
function createGeneratedModelLodAsset(asset, name, nodeId, level, bytes) {
|
|
2368
|
-
return {
|
|
2369
|
-
id: `asset:model-lod:${slugify2(`${name}-${nodeId}`)}:${level}`,
|
|
2370
|
-
metadata: {
|
|
2371
|
-
...asset.metadata,
|
|
2372
|
-
lodGenerated: true,
|
|
2373
|
-
lodLevel: level,
|
|
2374
|
-
lodSourceAssetId: asset.id,
|
|
2375
|
-
materialMtlText: "",
|
|
2376
|
-
modelFormat: "glb",
|
|
2377
|
-
texturePath: ""
|
|
2378
|
-
},
|
|
2379
|
-
path: createBinaryDataUrl(bytes, "model/gltf-binary"),
|
|
2380
|
-
type: "model"
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
function createBinaryDataUrl(bytes, mimeType) {
|
|
2384
|
-
let binary = "";
|
|
2385
|
-
const chunkSize = 32768;
|
|
2386
|
-
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
2387
|
-
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
|
|
2388
|
-
}
|
|
2389
|
-
return `data:${mimeType};base64,${btoa(binary)}`;
|
|
2390
|
-
}
|
|
2391
|
-
function sanitizeInstanceTransform(transform) {
|
|
2392
|
-
return {
|
|
2393
|
-
position: structuredClone(transform.position),
|
|
2394
|
-
rotation: structuredClone(transform.rotation),
|
|
2395
|
-
scale: structuredClone(transform.scale)
|
|
2396
|
-
};
|
|
2397
|
-
}
|
|
2398
|
-
function resolveModelAssetFormat(asset) {
|
|
2399
|
-
const format = readModelAssetString(asset, "modelFormat")?.toLowerCase();
|
|
2400
|
-
return format === "obj" || asset.path.toLowerCase().endsWith(".obj") ? "obj" : "gltf";
|
|
2401
|
-
}
|
|
2402
|
-
function readModelAssetString(asset, key) {
|
|
2403
|
-
const value = asset?.metadata[key];
|
|
2404
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2405
|
-
}
|
|
2406
|
-
function patchMtlTextureReferences2(mtlText, texturePath) {
|
|
2407
|
-
if (!texturePath) {
|
|
2408
|
-
return mtlText;
|
|
2409
|
-
}
|
|
2410
|
-
const mapPattern = /^(map_Ka|map_Kd|map_d|map_Bump|bump)\s+.+$/gm;
|
|
2411
|
-
const hasDiffuseMap = /^map_Kd\s+.+$/m.test(mtlText);
|
|
2412
|
-
const normalized = mtlText.replace(mapPattern, (line) => {
|
|
2413
|
-
if (line.startsWith("map_Kd ")) {
|
|
2414
|
-
return `map_Kd ${texturePath}`;
|
|
2415
|
-
}
|
|
2416
|
-
return line;
|
|
2417
|
-
});
|
|
2418
|
-
return hasDiffuseMap ? normalized : `${normalized.trim()}
|
|
2419
|
-
map_Kd ${texturePath}
|
|
2420
|
-
`;
|
|
2421
|
-
}
|
|
2422
|
-
function slugify2(value) {
|
|
2423
|
-
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2424
|
-
return normalized || "model";
|
|
2425
|
-
}
|
|
2426
|
-
function simplifyExportGeometry(geometry, ratio) {
|
|
2427
|
-
const primitives = geometry.primitives.map((primitive) => simplifyExportPrimitive(primitive, ratio)).filter((primitive) => primitive !== void 0);
|
|
2428
|
-
return primitives.length ? { primitives } : void 0;
|
|
2429
|
-
}
|
|
2430
|
-
function simplifyExportPrimitive(primitive, ratio) {
|
|
2431
|
-
const vertexCount = Math.floor(primitive.positions.length / 3);
|
|
2432
|
-
const triangleCount = Math.floor(primitive.indices.length / 3);
|
|
2433
|
-
if (vertexCount < 12 || triangleCount < 8 || ratio >= 0.98) {
|
|
2434
|
-
return void 0;
|
|
2435
|
-
}
|
|
2436
|
-
const geometry = createBufferGeometryFromPrimitive(primitive);
|
|
2437
|
-
const boundsTree = new MeshBVH(geometry, { maxLeafSize: 12, setBoundingBox: true });
|
|
2438
|
-
const bounds = boundsTree.getBoundingBox(new Box33());
|
|
2439
|
-
const simplified = simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds);
|
|
2440
|
-
geometry.dispose();
|
|
2441
|
-
if (!simplified) {
|
|
2442
|
-
return void 0;
|
|
2443
|
-
}
|
|
2444
|
-
return simplified;
|
|
2445
|
-
}
|
|
2446
|
-
function createBufferGeometryFromPrimitive(primitive) {
|
|
2447
|
-
const geometry = new BufferGeometry3();
|
|
2448
|
-
geometry.setAttribute("position", new Float32BufferAttribute3(primitive.positions, 3));
|
|
2449
|
-
geometry.setAttribute("normal", new Float32BufferAttribute3(primitive.normals, 3));
|
|
2450
|
-
if (primitive.uvs.length) {
|
|
2451
|
-
geometry.setAttribute("uv", new Float32BufferAttribute3(primitive.uvs, 2));
|
|
2452
|
-
}
|
|
2453
|
-
geometry.setIndex(primitive.indices);
|
|
2454
|
-
return geometry;
|
|
2455
|
-
}
|
|
2456
|
-
function simplifyPrimitiveWithVertexClustering(primitive, ratio, bounds) {
|
|
2457
|
-
const targetVertexCount = Math.max(8, Math.floor(primitive.positions.length / 3 * Math.max(0.04, ratio)));
|
|
2458
|
-
const size = bounds.getSize(new Vector34());
|
|
2459
|
-
let resolution = Math.max(1, Math.round(Math.cbrt(targetVertexCount)));
|
|
2460
|
-
let best;
|
|
2461
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
2462
|
-
const simplified = clusterPrimitiveVertices(primitive, bounds, size, Math.max(1, resolution - attempt));
|
|
2463
|
-
if (!simplified) {
|
|
2464
|
-
continue;
|
|
2465
|
-
}
|
|
2466
|
-
best = simplified;
|
|
2467
|
-
if (simplified.positions.length / 3 <= targetVertexCount) {
|
|
2468
|
-
break;
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
if (!best) {
|
|
2472
|
-
return void 0;
|
|
2473
|
-
}
|
|
2474
|
-
if (best.positions.length >= primitive.positions.length || best.indices.length >= primitive.indices.length) {
|
|
2475
|
-
return void 0;
|
|
2476
|
-
}
|
|
2477
|
-
return best;
|
|
2478
|
-
}
|
|
2479
|
-
function clusterPrimitiveVertices(primitive, bounds, size, resolution) {
|
|
2480
|
-
const min = bounds.min;
|
|
2481
|
-
const cellSizeX = Math.max(size.x / resolution, 1e-4);
|
|
2482
|
-
const cellSizeY = Math.max(size.y / resolution, 1e-4);
|
|
2483
|
-
const cellSizeZ = Math.max(size.z / resolution, 1e-4);
|
|
2484
|
-
const vertexCount = primitive.positions.length / 3;
|
|
2485
|
-
const clusters = /* @__PURE__ */ new Map();
|
|
2486
|
-
const clusterKeyByVertex = new Array(vertexCount);
|
|
2487
|
-
for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex += 1) {
|
|
2488
|
-
const positionOffset = vertexIndex * 3;
|
|
2489
|
-
const uvOffset = vertexIndex * 2;
|
|
2490
|
-
const x = primitive.positions[positionOffset];
|
|
2491
|
-
const y = primitive.positions[positionOffset + 1];
|
|
2492
|
-
const z = primitive.positions[positionOffset + 2];
|
|
2493
|
-
const normalX = primitive.normals[positionOffset];
|
|
2494
|
-
const normalY = primitive.normals[positionOffset + 1];
|
|
2495
|
-
const normalZ = primitive.normals[positionOffset + 2];
|
|
2496
|
-
const cellX = Math.floor((x - min.x) / cellSizeX);
|
|
2497
|
-
const cellY = Math.floor((y - min.y) / cellSizeY);
|
|
2498
|
-
const cellZ = Math.floor((z - min.z) / cellSizeZ);
|
|
2499
|
-
const clusterKey = `${cellX}:${cellY}:${cellZ}:${resolveNormalBucket(normalX, normalY, normalZ)}`;
|
|
2500
|
-
const cluster = clusters.get(clusterKey) ?? {
|
|
2501
|
-
count: 0,
|
|
2502
|
-
normalX: 0,
|
|
2503
|
-
normalY: 0,
|
|
2504
|
-
normalZ: 0,
|
|
2505
|
-
positionX: 0,
|
|
2506
|
-
positionY: 0,
|
|
2507
|
-
positionZ: 0,
|
|
2508
|
-
uvX: 0,
|
|
2509
|
-
uvY: 0
|
|
2510
|
-
};
|
|
2511
|
-
cluster.count += 1;
|
|
2512
|
-
cluster.positionX += x;
|
|
2513
|
-
cluster.positionY += y;
|
|
2514
|
-
cluster.positionZ += z;
|
|
2515
|
-
cluster.normalX += normalX;
|
|
2516
|
-
cluster.normalY += normalY;
|
|
2517
|
-
cluster.normalZ += normalZ;
|
|
2518
|
-
cluster.uvX += primitive.uvs[uvOffset] ?? 0;
|
|
2519
|
-
cluster.uvY += primitive.uvs[uvOffset + 1] ?? 0;
|
|
2520
|
-
clusters.set(clusterKey, cluster);
|
|
2521
|
-
clusterKeyByVertex[vertexIndex] = clusterKey;
|
|
2522
|
-
}
|
|
2523
|
-
const remappedIndices = [];
|
|
2524
|
-
const positions = [];
|
|
2525
|
-
const normals = [];
|
|
2526
|
-
const uvs = [];
|
|
2527
|
-
const clusterIndexByKey = /* @__PURE__ */ new Map();
|
|
2528
|
-
const ensureClusterIndex = (clusterKey) => {
|
|
2529
|
-
const existing = clusterIndexByKey.get(clusterKey);
|
|
2530
|
-
if (existing !== void 0) {
|
|
2531
|
-
return existing;
|
|
2532
|
-
}
|
|
2533
|
-
const cluster = clusters.get(clusterKey);
|
|
2534
|
-
if (!cluster || cluster.count === 0) {
|
|
2535
|
-
return void 0;
|
|
2536
|
-
}
|
|
2537
|
-
const averagedNormal = normalizeVec3(vec3(cluster.normalX, cluster.normalY, cluster.normalZ));
|
|
2538
|
-
const index = positions.length / 3;
|
|
2539
|
-
positions.push(cluster.positionX / cluster.count, cluster.positionY / cluster.count, cluster.positionZ / cluster.count);
|
|
2540
|
-
normals.push(averagedNormal.x, averagedNormal.y, averagedNormal.z);
|
|
2541
|
-
uvs.push(cluster.uvX / cluster.count, cluster.uvY / cluster.count);
|
|
2542
|
-
clusterIndexByKey.set(clusterKey, index);
|
|
2543
|
-
return index;
|
|
2544
|
-
};
|
|
2545
|
-
for (let index = 0; index < primitive.indices.length; index += 3) {
|
|
2546
|
-
const a = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index]]);
|
|
2547
|
-
const b = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 1]]);
|
|
2548
|
-
const c = ensureClusterIndex(clusterKeyByVertex[primitive.indices[index + 2]]);
|
|
2549
|
-
if (a === void 0 || b === void 0 || c === void 0) {
|
|
2550
|
-
continue;
|
|
2551
|
-
}
|
|
2552
|
-
if (a === b || b === c || a === c) {
|
|
2553
|
-
continue;
|
|
2554
|
-
}
|
|
2555
|
-
if (triangleArea(positions, a, b, c) <= 1e-6) {
|
|
2556
|
-
continue;
|
|
2557
|
-
}
|
|
2558
|
-
remappedIndices.push(a, b, c);
|
|
2559
|
-
}
|
|
2560
|
-
if (remappedIndices.length < 12 || positions.length >= primitive.positions.length) {
|
|
2561
|
-
return void 0;
|
|
2562
|
-
}
|
|
2563
|
-
return {
|
|
2564
|
-
indices: remappedIndices,
|
|
2565
|
-
material: primitive.material,
|
|
2566
|
-
normals,
|
|
2567
|
-
positions,
|
|
2568
|
-
uvs
|
|
2569
|
-
};
|
|
2570
|
-
}
|
|
2571
|
-
function resolveNormalBucket(x, y, z) {
|
|
2572
|
-
const ax = Math.abs(x);
|
|
2573
|
-
const ay = Math.abs(y);
|
|
2574
|
-
const az = Math.abs(z);
|
|
2575
|
-
if (ax >= ay && ax >= az) {
|
|
2576
|
-
return x >= 0 ? "xp" : "xn";
|
|
2577
|
-
}
|
|
2578
|
-
if (ay >= ax && ay >= az) {
|
|
2579
|
-
return y >= 0 ? "yp" : "yn";
|
|
2580
|
-
}
|
|
2581
|
-
return z >= 0 ? "zp" : "zn";
|
|
2582
|
-
}
|
|
2583
|
-
function triangleArea(positions, a, b, c) {
|
|
2584
|
-
const ax = positions[a * 3];
|
|
2585
|
-
const ay = positions[a * 3 + 1];
|
|
2586
|
-
const az = positions[a * 3 + 2];
|
|
2587
|
-
const bx = positions[b * 3];
|
|
2588
|
-
const by = positions[b * 3 + 1];
|
|
2589
|
-
const bz = positions[b * 3 + 2];
|
|
2590
|
-
const cx = positions[c * 3];
|
|
2591
|
-
const cy = positions[c * 3 + 1];
|
|
2592
|
-
const cz = positions[c * 3 + 2];
|
|
2593
|
-
const ab = vec3(bx - ax, by - ay, bz - az);
|
|
2594
|
-
const ac = vec3(cx - ax, cy - ay, cz - az);
|
|
2595
|
-
const cross = crossVec3(ab, ac);
|
|
2596
|
-
return Math.sqrt(cross.x * cross.x + cross.y * cross.y + cross.z * cross.z) * 0.5;
|
|
2597
|
-
}
|
|
2598
|
-
function buildPrimitiveGeometry(shape, size, radialSegments) {
|
|
2599
|
-
const geometry = shape === "cube" ? new BoxGeometry3(Math.abs(size.x), Math.abs(size.y), Math.abs(size.z)) : shape === "sphere" ? new SphereGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, radialSegments, Math.max(8, Math.floor(radialSegments * 0.75))) : shape === "cylinder" ? new CylinderGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.abs(size.y), radialSegments) : new ConeGeometry(Math.max(Math.abs(size.x), Math.abs(size.z)) * 0.5, Math.abs(size.y), radialSegments);
|
|
2600
|
-
const positionAttribute = geometry.getAttribute("position");
|
|
2601
|
-
const normalAttribute = geometry.getAttribute("normal");
|
|
2602
|
-
const uvAttribute = geometry.getAttribute("uv");
|
|
2603
|
-
const index = geometry.getIndex();
|
|
2604
|
-
const primitive = {
|
|
2605
|
-
indices: index ? Array.from(index.array) : Array.from({ length: positionAttribute.count }, (_, value) => value),
|
|
2606
|
-
normals: Array.from(normalAttribute.array),
|
|
2607
|
-
positions: Array.from(positionAttribute.array),
|
|
2608
|
-
uvs: uvAttribute ? Array.from(uvAttribute.array) : []
|
|
2609
|
-
};
|
|
2610
|
-
geometry.dispose();
|
|
2611
|
-
return primitive;
|
|
2612
|
-
}
|
|
2613
|
-
async function resolveRuntimeMaterial(material) {
|
|
2614
|
-
const resolved = material ?? {
|
|
2615
|
-
color: "#ffffff",
|
|
2616
|
-
id: "material:fallback:default",
|
|
2617
|
-
metalness: 0.05,
|
|
2618
|
-
name: "Default Material",
|
|
2619
|
-
roughness: 0.8
|
|
2620
|
-
};
|
|
2621
|
-
return {
|
|
2622
|
-
baseColorTexture: await resolveEmbeddedTextureUri(resolved.colorTexture ?? resolveGeneratedBlockoutTexture(resolved)),
|
|
2623
|
-
color: resolved.color,
|
|
2624
|
-
id: resolved.id,
|
|
2625
|
-
metallicFactor: resolved.metalness ?? 0,
|
|
2626
|
-
metallicRoughnessTexture: await createMetallicRoughnessTextureDataUri(
|
|
2627
|
-
resolved.metalnessTexture,
|
|
2628
|
-
resolved.roughnessTexture,
|
|
2629
|
-
resolved.metalness ?? 0,
|
|
2630
|
-
resolved.roughness ?? 0.8
|
|
2631
|
-
),
|
|
2632
|
-
name: resolved.name,
|
|
2633
|
-
normalTexture: await resolveEmbeddedTextureUri(resolved.normalTexture),
|
|
2634
|
-
roughnessFactor: resolved.roughness ?? 0.8,
|
|
2635
|
-
side: resolved.side
|
|
2636
|
-
};
|
|
2637
|
-
}
|
|
2638
|
-
function resolveGeneratedBlockoutTexture(material) {
|
|
2639
|
-
return material.category === "blockout" ? createBlockoutTextureDataUri(material.color, material.edgeColor ?? "#2f3540", material.edgeThickness ?? 0.035) : void 0;
|
|
2640
|
-
}
|
|
2641
|
-
function projectPlanarUvs(vertices, normal, uvScale, uvOffset) {
|
|
2642
|
-
const basis = createFacePlaneBasis(normal);
|
|
2643
|
-
const origin = vertices[0] ?? vec3(0, 0, 0);
|
|
2644
|
-
const scaleX = Math.abs(uvScale?.x ?? 1) <= 1e-4 ? 1 : uvScale?.x ?? 1;
|
|
2645
|
-
const scaleY = Math.abs(uvScale?.y ?? 1) <= 1e-4 ? 1 : uvScale?.y ?? 1;
|
|
2646
|
-
const offsetX = uvOffset?.x ?? 0;
|
|
2647
|
-
const offsetY = uvOffset?.y ?? 0;
|
|
2648
|
-
return vertices.flatMap((vertex) => {
|
|
2649
|
-
const offset = subVec3(vertex, origin);
|
|
2650
|
-
return [dotVec3(offset, basis.u) * scaleX + offsetX, dotVec3(offset, basis.v) * scaleY + offsetY];
|
|
2651
|
-
});
|
|
2652
|
-
}
|
|
2653
|
-
function createFacePlaneBasis(normal) {
|
|
2654
|
-
const normalizedNormal = normalizeVec3(normal);
|
|
2655
|
-
const reference = Math.abs(normalizedNormal.y) < 0.99 ? vec3(0, 1, 0) : vec3(1, 0, 0);
|
|
2656
|
-
const u = normalizeVec3(crossVec3(reference, normalizedNormal));
|
|
2657
|
-
const v = normalizeVec3(crossVec3(normalizedNormal, u));
|
|
2658
|
-
return { u, v };
|
|
2659
|
-
}
|
|
2660
|
-
async function resolveEmbeddedTextureUri(source) {
|
|
2661
|
-
if (!source) {
|
|
2662
|
-
return void 0;
|
|
2663
|
-
}
|
|
2664
|
-
if (source.startsWith("data:")) {
|
|
2665
|
-
return source;
|
|
2666
|
-
}
|
|
2667
|
-
const response = await fetch(source);
|
|
2668
|
-
const blob = await response.blob();
|
|
2669
|
-
const buffer = await blob.arrayBuffer();
|
|
2670
|
-
return `data:${blob.type || "application/octet-stream"};base64,${toBase64(new Uint8Array(buffer))}`;
|
|
2671
|
-
}
|
|
2672
|
-
async function createMetallicRoughnessTextureDataUri(metalnessSource, roughnessSource, metalnessFactor, roughnessFactor) {
|
|
2673
|
-
if (!metalnessSource && !roughnessSource) {
|
|
2674
|
-
return void 0;
|
|
2675
|
-
}
|
|
2676
|
-
if (typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
|
|
2677
|
-
return void 0;
|
|
2678
|
-
}
|
|
2679
|
-
const [metalness, roughness] = await Promise.all([
|
|
2680
|
-
loadImagePixels(metalnessSource),
|
|
2681
|
-
loadImagePixels(roughnessSource)
|
|
2682
|
-
]);
|
|
2683
|
-
const width = Math.max(metalness?.width ?? 1, roughness?.width ?? 1);
|
|
2684
|
-
const height = Math.max(metalness?.height ?? 1, roughness?.height ?? 1);
|
|
2685
|
-
const canvas = new OffscreenCanvas(width, height);
|
|
2686
|
-
const context = canvas.getContext("2d");
|
|
2687
|
-
if (!context) {
|
|
2688
|
-
return void 0;
|
|
2689
|
-
}
|
|
2690
|
-
const imageData = context.createImageData(width, height);
|
|
2691
|
-
const metalDefault = Math.round(clamp01(metalnessFactor) * 255);
|
|
2692
|
-
const roughDefault = Math.round(clamp01(roughnessFactor) * 255);
|
|
2693
|
-
for (let index = 0; index < imageData.data.length; index += 4) {
|
|
2694
|
-
imageData.data[index] = 0;
|
|
2695
|
-
imageData.data[index + 1] = roughness?.pixels[index] ?? roughDefault;
|
|
2696
|
-
imageData.data[index + 2] = metalness?.pixels[index] ?? metalDefault;
|
|
2697
|
-
imageData.data[index + 3] = 255;
|
|
2698
|
-
}
|
|
2699
|
-
context.putImageData(imageData, 0, 0);
|
|
2700
|
-
const blob = await canvas.convertToBlob({ type: "image/png" });
|
|
2701
|
-
const buffer = await blob.arrayBuffer();
|
|
2702
|
-
return `data:image/png;base64,${toBase64(new Uint8Array(buffer))}`;
|
|
2703
|
-
}
|
|
2704
|
-
async function loadImagePixels(source) {
|
|
2705
|
-
if (!source || typeof OffscreenCanvas === "undefined" || typeof createImageBitmap === "undefined") {
|
|
2706
|
-
return void 0;
|
|
2707
|
-
}
|
|
2708
|
-
const response = await fetch(source);
|
|
2709
|
-
const blob = await response.blob();
|
|
2710
|
-
const bitmap = await createImageBitmap(blob);
|
|
2711
|
-
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
2712
|
-
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
2713
|
-
if (!context) {
|
|
2714
|
-
bitmap.close();
|
|
2715
|
-
return void 0;
|
|
2716
|
-
}
|
|
2717
|
-
context.drawImage(bitmap, 0, 0);
|
|
2718
|
-
bitmap.close();
|
|
2719
|
-
const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
2720
|
-
return {
|
|
2721
|
-
height: imageData.height,
|
|
2722
|
-
pixels: imageData.data,
|
|
2723
|
-
width: imageData.width
|
|
2724
|
-
};
|
|
2725
|
-
}
|
|
2726
|
-
function clamp01(value) {
|
|
2727
|
-
return Math.max(0, Math.min(1, value));
|
|
2728
|
-
}
|
|
2729
|
-
function toBase64(bytes) {
|
|
2730
|
-
let binary = "";
|
|
2731
|
-
bytes.forEach((byte) => {
|
|
2732
|
-
binary += String.fromCharCode(byte);
|
|
2733
|
-
});
|
|
2734
|
-
return btoa(binary);
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
985
|
// src/bundle.ts
|
|
986
|
+
import {
|
|
987
|
+
createWebHammerEngineBundleZip as packWebHammerEngineBundle,
|
|
988
|
+
externalizeWebHammerEngineScene as externalizeWebHammerEngineSceneAssets,
|
|
989
|
+
parseWebHammerEngineBundleZip as unpackWebHammerEngineBundle
|
|
990
|
+
} from "@ggez/runtime-build";
|
|
991
|
+
import {
|
|
992
|
+
buildRuntimeBundle,
|
|
993
|
+
buildRuntimeScene,
|
|
994
|
+
buildRuntimeWorldIndex,
|
|
995
|
+
externalizeRuntimeAssets,
|
|
996
|
+
packRuntimeBundle,
|
|
997
|
+
unpackRuntimeBundle
|
|
998
|
+
} from "@ggez/runtime-build";
|
|
2738
999
|
function createThreeAssetResolver(bundle) {
|
|
2739
1000
|
const urlByPath = /* @__PURE__ */ new Map();
|
|
2740
1001
|
return {
|
|
@@ -2764,14 +1025,14 @@ function createThreeAssetResolver(bundle) {
|
|
|
2764
1025
|
function createWebHammerBundleAssetResolver(bundle) {
|
|
2765
1026
|
return createThreeAssetResolver(bundle);
|
|
2766
1027
|
}
|
|
2767
|
-
function
|
|
2768
|
-
return
|
|
1028
|
+
function createWebHammerEngineBundleZip(bundle, options) {
|
|
1029
|
+
return packWebHammerEngineBundle(bundle, options);
|
|
2769
1030
|
}
|
|
2770
|
-
function
|
|
2771
|
-
return
|
|
1031
|
+
function parseWebHammerEngineBundleZip(bundleBytes, options) {
|
|
1032
|
+
return unpackWebHammerEngineBundle(bundleBytes, options);
|
|
2772
1033
|
}
|
|
2773
|
-
function
|
|
2774
|
-
return
|
|
1034
|
+
function externalizeWebHammerEngineScene(scene, options) {
|
|
1035
|
+
return externalizeWebHammerEngineSceneAssets(scene, options);
|
|
2775
1036
|
}
|
|
2776
1037
|
export {
|
|
2777
1038
|
applyRuntimeWorldSettingsToThreeScene,
|
|
@@ -2785,10 +1046,10 @@ export {
|
|
|
2785
1046
|
createThreeRuntimeObjectFactory,
|
|
2786
1047
|
createThreeRuntimeSceneInstance,
|
|
2787
1048
|
createWebHammerBundleAssetResolver,
|
|
2788
|
-
|
|
1049
|
+
createWebHammerEngineBundleZip,
|
|
2789
1050
|
createWebHammerSceneObjectFactory,
|
|
2790
1051
|
externalizeRuntimeAssets,
|
|
2791
|
-
|
|
1052
|
+
externalizeWebHammerEngineScene,
|
|
2792
1053
|
extractPhysics,
|
|
2793
1054
|
fetchWebHammerEngineScene,
|
|
2794
1055
|
findPrimaryLight,
|
|
@@ -2798,7 +1059,7 @@ export {
|
|
|
2798
1059
|
loadWebHammerEngineScene,
|
|
2799
1060
|
loadWebHammerEngineSceneFromUrl,
|
|
2800
1061
|
packRuntimeBundle,
|
|
2801
|
-
|
|
1062
|
+
parseWebHammerEngineBundleZip,
|
|
2802
1063
|
parseWebHammerEngineScene,
|
|
2803
1064
|
unpackRuntimeBundle
|
|
2804
1065
|
};
|