@nice2dev/ui-3d 1.3.15 → 1.3.16
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/CHANGELOG.md +9 -0
- package/dist/cjs/collaborative/collaborativeScene.js +160 -206
- package/dist/cjs/core/editorAsk.js +51 -41
- package/dist/cjs/core/i18n.js +13 -14
- package/dist/cjs/core/threeCompat.js +29 -42
- package/dist/cjs/dance/DanceBridge.js +148 -177
- package/dist/cjs/dance/DanceScoreEngine.js +209 -221
- package/dist/cjs/dance/PoseDetector.js +302 -275
- package/dist/cjs/index.js +110 -113
- package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
- package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
- package/dist/cjs/material/materialEditorTypes.js +35 -49
- package/dist/cjs/material/materialEditorUtils.js +684 -747
- package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
- package/dist/cjs/model/ModelEditor.js +90 -40
- package/dist/cjs/model/ModelEditor.module.css.js +257 -5
- package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
- package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
- package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
- package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
- package/dist/cjs/model/ModelEditorTimeline.js +232 -34
- package/dist/cjs/model/ModelEditorToolbar.js +170 -14
- package/dist/cjs/model/ModelEditorViewport.js +231 -39
- package/dist/cjs/model/ModelViewer.js +388 -63
- package/dist/cjs/model/ModelViewer.module.css.js +77 -5
- package/dist/cjs/model/NiceArmatureEditor.js +473 -274
- package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
- package/dist/cjs/model/NiceOctree.js +372 -320
- package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
- package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
- package/dist/cjs/model/NiceTerrainEditor.js +595 -385
- package/dist/cjs/model/NiceWeightPainter.js +384 -261
- package/dist/cjs/model/NiceXRPreview.js +385 -308
- package/dist/cjs/model/cadModeUtils.js +103 -106
- package/dist/cjs/model/editorShortcuts.js +171 -194
- package/dist/cjs/model/modelEditorTypes.js +100 -118
- package/dist/cjs/model/modelEditorUtils.js +1061 -1146
- package/dist/cjs/model/simsModeUtils.js +1023 -1053
- package/dist/cjs/model/useModelEditor.js +1977 -1933
- package/dist/cjs/model/useModelViewer.js +633 -665
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
- package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
- package/dist/cjs/particle/particleEditorTypes.js +62 -70
- package/dist/cjs/particle/particleEditorUtils.js +998 -1013
- package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
- package/dist/cjs/rendering/NiceRenderExport.js +424 -337
- package/dist/cjs/rendering/NiceSSAO.js +454 -337
- package/dist/cjs/rendering/NiceSSR.js +387 -261
- package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
- package/dist/cjs/uv/NiceUVEditor.js +787 -550
- package/dist/cjs/uv/UVEditor.module.css.js +53 -5
- package/dist/cjs/uv/uvEditorTypes.js +59 -76
- package/dist/cjs/uv/uvEditorUtils.js +501 -575
- package/dist/esm/collaborative/collaborativeScene.js +163 -205
- package/dist/esm/core/editorAsk.js +53 -40
- package/dist/esm/core/i18n.js +15 -13
- package/dist/esm/core/threeCompat.js +18 -26
- package/dist/esm/dance/DanceBridge.js +151 -176
- package/dist/esm/dance/DanceScoreEngine.js +211 -220
- package/dist/esm/dance/PoseDetector.js +304 -274
- package/dist/esm/index.js +258 -36
- package/dist/esm/material/MaterialEditor.module.css.js +242 -3
- package/dist/esm/material/NiceMaterialEditor.js +1215 -756
- package/dist/esm/material/materialEditorTypes.js +45 -48
- package/dist/esm/material/materialEditorUtils.js +713 -746
- package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
- package/dist/esm/model/ModelEditor.js +91 -39
- package/dist/esm/model/ModelEditor.module.css.js +257 -3
- package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
- package/dist/esm/model/ModelEditorMenuBar.js +359 -30
- package/dist/esm/model/ModelEditorRightPanel.js +634 -114
- package/dist/esm/model/ModelEditorSubComponents.js +265 -94
- package/dist/esm/model/ModelEditorTimeline.js +232 -32
- package/dist/esm/model/ModelEditorToolbar.js +170 -12
- package/dist/esm/model/ModelEditorViewport.js +231 -37
- package/dist/esm/model/ModelViewer.js +389 -62
- package/dist/esm/model/ModelViewer.module.css.js +77 -3
- package/dist/esm/model/NiceArmatureEditor.js +462 -258
- package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
- package/dist/esm/model/NiceOctree.js +361 -304
- package/dist/esm/model/NicePhysicsSimulation.js +517 -282
- package/dist/esm/model/NiceProceduralGeometry.js +667 -284
- package/dist/esm/model/NiceTerrainEditor.js +584 -369
- package/dist/esm/model/NiceWeightPainter.js +373 -245
- package/dist/esm/model/NiceXRPreview.js +374 -292
- package/dist/esm/model/cadModeUtils.js +95 -88
- package/dist/esm/model/editorShortcuts.js +172 -193
- package/dist/esm/model/modelEditorTypes.js +90 -100
- package/dist/esm/model/modelEditorUtils.js +1056 -1128
- package/dist/esm/model/simsModeUtils.js +1021 -1035
- package/dist/esm/model/useModelEditor.js +1946 -1920
- package/dist/esm/model/useModelViewer.js +619 -647
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +1188 -570
- package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
- package/dist/esm/particle/particleEditorTypes.js +69 -69
- package/dist/esm/particle/particleEditorUtils.js +1027 -1012
- package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
- package/dist/esm/rendering/NiceRenderExport.js +413 -321
- package/dist/esm/rendering/NiceSSAO.js +450 -328
- package/dist/esm/rendering/NiceSSR.js +383 -252
- package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
- package/dist/esm/uv/NiceUVEditor.js +788 -549
- package/dist/esm/uv/UVEditor.module.css.js +53 -3
- package/dist/esm/uv/uvEditorTypes.js +68 -75
- package/dist/esm/uv/uvEditorUtils.js +515 -557
- package/package.json +7 -12
|
@@ -1,1221 +1,1149 @@
|
|
|
1
|
-
import * as THREE from
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* modelEditorUtils.ts — Utility functions for the ModelEditor.
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Undo/Redo stack
|
|
8
|
-
* - LOD auto-generation
|
|
9
|
-
* - PBR material presets
|
|
10
|
-
* - Batch import
|
|
11
|
-
* - Instancing helpers
|
|
12
|
-
*/
|
|
1
|
+
import * as THREE from "three";
|
|
13
2
|
class UndoRedoStack {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
constructor(maxSize = 50) {
|
|
4
|
+
this.undoStack = [];
|
|
5
|
+
this.redoStack = [];
|
|
6
|
+
this.maxSize = maxSize;
|
|
7
|
+
}
|
|
8
|
+
push(action) {
|
|
9
|
+
this.undoStack.push(action);
|
|
10
|
+
if (this.undoStack.length > this.maxSize) {
|
|
11
|
+
this.undoStack.shift();
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
this.redoStack.length = 0;
|
|
14
|
+
}
|
|
15
|
+
undo() {
|
|
16
|
+
const action = this.undoStack.pop();
|
|
17
|
+
if (!action) {
|
|
18
|
+
return null;
|
|
25
19
|
}
|
|
26
|
-
undo()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
action.undo();
|
|
21
|
+
this.redoStack.push(action);
|
|
22
|
+
return action.label;
|
|
23
|
+
}
|
|
24
|
+
redo() {
|
|
25
|
+
const action = this.redoStack.pop();
|
|
26
|
+
if (!action) {
|
|
27
|
+
return null;
|
|
34
28
|
}
|
|
35
|
-
redo()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
action.redo();
|
|
30
|
+
this.undoStack.push(action);
|
|
31
|
+
return action.label;
|
|
32
|
+
}
|
|
33
|
+
canUndo() {
|
|
34
|
+
return this.undoStack.length > 0;
|
|
35
|
+
}
|
|
36
|
+
canRedo() {
|
|
37
|
+
return this.redoStack.length > 0;
|
|
38
|
+
}
|
|
39
|
+
clear() {
|
|
40
|
+
this.undoStack.length = 0;
|
|
41
|
+
this.redoStack.length = 0;
|
|
42
|
+
}
|
|
43
|
+
get undoLabel() {
|
|
44
|
+
return this.undoStack.length > 0 ? this.undoStack[this.undoStack.length - 1].label : null;
|
|
45
|
+
}
|
|
46
|
+
get redoLabel() {
|
|
47
|
+
return this.redoStack.length > 0 ? this.redoStack[this.redoStack.length - 1].label : null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const DEFAULT_LOD_LEVELS = [
|
|
51
|
+
{ distance: 0, ratio: 1 },
|
|
52
|
+
{ distance: 10, ratio: 0.5 },
|
|
53
|
+
{ distance: 25, ratio: 0.25 },
|
|
54
|
+
{ distance: 50, ratio: 0.1 }
|
|
55
|
+
];
|
|
56
|
+
function decimateGeometry(geometry, ratio) {
|
|
57
|
+
if (ratio >= 1) {
|
|
58
|
+
return geometry.clone();
|
|
59
|
+
}
|
|
60
|
+
const posAttr = geometry.getAttribute("position");
|
|
61
|
+
if (!posAttr) {
|
|
62
|
+
return geometry.clone();
|
|
63
|
+
}
|
|
64
|
+
const positions = Array.from(posAttr.array);
|
|
65
|
+
const vertexCount = posAttr.count;
|
|
66
|
+
const targetCount = Math.max(3, Math.floor(vertexCount * ratio));
|
|
67
|
+
const gridSize = Math.cbrt(vertexCount / targetCount) * 0.5;
|
|
68
|
+
const merged = /* @__PURE__ */ new Map();
|
|
69
|
+
const vertexMap = new Int32Array(vertexCount);
|
|
70
|
+
let newIndex = 0;
|
|
71
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
72
|
+
const x = positions[i * 3];
|
|
73
|
+
const y = positions[i * 3 + 1];
|
|
74
|
+
const z = positions[i * 3 + 2];
|
|
75
|
+
const key = `${Math.round(x / gridSize)},${Math.round(y / gridSize)},${Math.round(z / gridSize)}`;
|
|
76
|
+
const existing = merged.get(key);
|
|
77
|
+
if (existing) {
|
|
78
|
+
existing.x += x;
|
|
79
|
+
existing.y += y;
|
|
80
|
+
existing.z += z;
|
|
81
|
+
existing.count++;
|
|
82
|
+
vertexMap[i] = existing.index;
|
|
83
|
+
} else {
|
|
84
|
+
merged.set(key, { x, y, z, count: 1, index: newIndex });
|
|
85
|
+
vertexMap[i] = newIndex;
|
|
86
|
+
newIndex++;
|
|
43
87
|
}
|
|
44
|
-
|
|
45
|
-
|
|
88
|
+
}
|
|
89
|
+
const newPositions = new Float32Array(merged.size * 3);
|
|
90
|
+
for (const cell of merged.values()) {
|
|
91
|
+
newPositions[cell.index * 3] = cell.x / cell.count;
|
|
92
|
+
newPositions[cell.index * 3 + 1] = cell.y / cell.count;
|
|
93
|
+
newPositions[cell.index * 3 + 2] = cell.z / cell.count;
|
|
94
|
+
}
|
|
95
|
+
const oldIndex = geometry.index;
|
|
96
|
+
const newIndices = [];
|
|
97
|
+
if (oldIndex) {
|
|
98
|
+
for (let i = 0; i < oldIndex.count; i += 3) {
|
|
99
|
+
const a = vertexMap[oldIndex.array[i]];
|
|
100
|
+
const b = vertexMap[oldIndex.array[i + 1]];
|
|
101
|
+
const c = vertexMap[oldIndex.array[i + 2]];
|
|
102
|
+
if (a !== b && b !== c && a !== c) {
|
|
103
|
+
newIndices.push(a, b, c);
|
|
104
|
+
}
|
|
46
105
|
}
|
|
47
|
-
|
|
48
|
-
|
|
106
|
+
} else {
|
|
107
|
+
for (let i = 0; i < vertexCount; i += 3) {
|
|
108
|
+
const a = vertexMap[i];
|
|
109
|
+
const b = vertexMap[i + 1];
|
|
110
|
+
const c = vertexMap[i + 2];
|
|
111
|
+
if (a !== b && b !== c && a !== c) {
|
|
112
|
+
newIndices.push(a, b, c);
|
|
113
|
+
}
|
|
49
114
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
}
|
|
116
|
+
const result = new THREE.BufferGeometry();
|
|
117
|
+
result.setAttribute("position", new THREE.BufferAttribute(newPositions, 3));
|
|
118
|
+
result.setIndex(newIndices);
|
|
119
|
+
result.computeVertexNormals();
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
function generateLOD(mesh, levels = DEFAULT_LOD_LEVELS) {
|
|
123
|
+
const lod = new THREE.LOD();
|
|
124
|
+
lod.name = mesh.name + "_LOD";
|
|
125
|
+
lod.position.copy(mesh.position);
|
|
126
|
+
lod.rotation.copy(mesh.rotation);
|
|
127
|
+
lod.scale.copy(mesh.scale);
|
|
128
|
+
for (const level of levels) {
|
|
129
|
+
const decimated = decimateGeometry(mesh.geometry, level.ratio);
|
|
130
|
+
const mat = Array.isArray(mesh.material) ? mesh.material.map((m) => m.clone()) : mesh.material.clone();
|
|
131
|
+
const lodMesh = new THREE.Mesh(decimated, mat);
|
|
132
|
+
lodMesh.castShadow = mesh.castShadow;
|
|
133
|
+
lodMesh.receiveShadow = mesh.receiveShadow;
|
|
134
|
+
lod.addLevel(lodMesh, level.distance);
|
|
135
|
+
}
|
|
136
|
+
return lod;
|
|
137
|
+
}
|
|
138
|
+
const PBR_PRESETS = [
|
|
139
|
+
// Metals
|
|
140
|
+
{ name: "Gold", category: "Metal", params: { color: 16766720, metalness: 1, roughness: 0.15 } },
|
|
141
|
+
{
|
|
142
|
+
name: "Silver",
|
|
143
|
+
category: "Metal",
|
|
144
|
+
params: { color: 12632256, metalness: 1, roughness: 0.1 }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "Copper",
|
|
148
|
+
category: "Metal",
|
|
149
|
+
params: { color: 12088115, metalness: 1, roughness: 0.25 }
|
|
150
|
+
},
|
|
151
|
+
{ name: "Iron", category: "Metal", params: { color: 8421504, metalness: 0.95, roughness: 0.6 } },
|
|
152
|
+
{
|
|
153
|
+
name: "Brushed Steel",
|
|
154
|
+
category: "Metal",
|
|
155
|
+
params: { color: 10526880, metalness: 0.95, roughness: 0.4 }
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "Chrome",
|
|
159
|
+
category: "Metal",
|
|
160
|
+
params: { color: 14540253, metalness: 1, roughness: 0.02 }
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "Bronze",
|
|
164
|
+
category: "Metal",
|
|
165
|
+
params: { color: 13467442, metalness: 0.9, roughness: 0.35 }
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "Aluminum",
|
|
169
|
+
category: "Metal",
|
|
170
|
+
params: { color: 14079702, metalness: 0.85, roughness: 0.2 }
|
|
171
|
+
},
|
|
172
|
+
// Non-metals
|
|
173
|
+
{
|
|
174
|
+
name: "Plastic (White)",
|
|
175
|
+
category: "Plastic",
|
|
176
|
+
params: { color: 15790320, metalness: 0, roughness: 0.3 }
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "Plastic (Red)",
|
|
180
|
+
category: "Plastic",
|
|
181
|
+
params: { color: 13378082, metalness: 0, roughness: 0.35 }
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "Rubber",
|
|
185
|
+
category: "Plastic",
|
|
186
|
+
params: { color: 3355443, metalness: 0, roughness: 0.9 }
|
|
187
|
+
},
|
|
188
|
+
// Glass
|
|
189
|
+
{
|
|
190
|
+
name: "Clear Glass",
|
|
191
|
+
category: "Glass",
|
|
192
|
+
params: {
|
|
193
|
+
color: 16777215,
|
|
194
|
+
metalness: 0,
|
|
195
|
+
roughness: 0,
|
|
196
|
+
opacity: 0.15,
|
|
197
|
+
transparent: true,
|
|
198
|
+
ior: 1.5,
|
|
199
|
+
transmission: 0.95
|
|
53
200
|
}
|
|
54
|
-
|
|
55
|
-
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "Frosted Glass",
|
|
204
|
+
category: "Glass",
|
|
205
|
+
params: {
|
|
206
|
+
color: 15658751,
|
|
207
|
+
metalness: 0,
|
|
208
|
+
roughness: 0.5,
|
|
209
|
+
opacity: 0.3,
|
|
210
|
+
transparent: true,
|
|
211
|
+
ior: 1.5,
|
|
212
|
+
transmission: 0.7
|
|
56
213
|
}
|
|
57
|
-
|
|
58
|
-
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "Tinted Glass",
|
|
217
|
+
category: "Glass",
|
|
218
|
+
params: {
|
|
219
|
+
color: 2245768,
|
|
220
|
+
metalness: 0,
|
|
221
|
+
roughness: 0.05,
|
|
222
|
+
opacity: 0.3,
|
|
223
|
+
transparent: true,
|
|
224
|
+
ior: 1.5,
|
|
225
|
+
transmission: 0.8
|
|
59
226
|
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
227
|
+
},
|
|
228
|
+
// Natural
|
|
229
|
+
{
|
|
230
|
+
name: "Wood (Light)",
|
|
231
|
+
category: "Natural",
|
|
232
|
+
params: { color: 12687979, metalness: 0, roughness: 0.7 }
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "Wood (Dark)",
|
|
236
|
+
category: "Natural",
|
|
237
|
+
params: { color: 6042391, metalness: 0, roughness: 0.65 }
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "Stone",
|
|
241
|
+
category: "Natural",
|
|
242
|
+
params: { color: 8947848, metalness: 0, roughness: 0.85 }
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "Marble",
|
|
246
|
+
category: "Natural",
|
|
247
|
+
params: { color: 15789280, metalness: 0, roughness: 0.15 }
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "Clay",
|
|
251
|
+
category: "Natural",
|
|
252
|
+
params: { color: 11887901, metalness: 0, roughness: 0.8 }
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "Concrete",
|
|
256
|
+
category: "Natural",
|
|
257
|
+
params: { color: 10066329, metalness: 0, roughness: 0.95 }
|
|
258
|
+
},
|
|
259
|
+
// Fabric
|
|
260
|
+
{
|
|
261
|
+
name: "Cotton (White)",
|
|
262
|
+
category: "Fabric",
|
|
263
|
+
params: {
|
|
264
|
+
color: 16119260,
|
|
265
|
+
metalness: 0,
|
|
266
|
+
roughness: 0.9,
|
|
267
|
+
sheen: 0.5,
|
|
268
|
+
sheenRoughness: 0.8,
|
|
269
|
+
sheenColor: 16777215
|
|
74
270
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "Silk",
|
|
274
|
+
category: "Fabric",
|
|
275
|
+
params: {
|
|
276
|
+
color: 14932681,
|
|
277
|
+
metalness: 0,
|
|
278
|
+
roughness: 0.3,
|
|
279
|
+
sheen: 1,
|
|
280
|
+
sheenRoughness: 0.3,
|
|
281
|
+
sheenColor: 16772829
|
|
78
282
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const z = positions[i * 3 + 2];
|
|
91
|
-
const key = `${Math.round(x / gridSize)},${Math.round(y / gridSize)},${Math.round(z / gridSize)}`;
|
|
92
|
-
const existing = merged.get(key);
|
|
93
|
-
if (existing) {
|
|
94
|
-
existing.x += x;
|
|
95
|
-
existing.y += y;
|
|
96
|
-
existing.z += z;
|
|
97
|
-
existing.count++;
|
|
98
|
-
vertexMap[i] = existing.index;
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
merged.set(key, { x, y, z, count: 1, index: newIndex });
|
|
102
|
-
vertexMap[i] = newIndex;
|
|
103
|
-
newIndex++;
|
|
104
|
-
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "Velvet",
|
|
286
|
+
category: "Fabric",
|
|
287
|
+
params: {
|
|
288
|
+
color: 8388640,
|
|
289
|
+
metalness: 0,
|
|
290
|
+
roughness: 0.95,
|
|
291
|
+
sheen: 0.9,
|
|
292
|
+
sheenRoughness: 0.9,
|
|
293
|
+
sheenColor: 16728160
|
|
105
294
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "Leather",
|
|
298
|
+
category: "Fabric",
|
|
299
|
+
params: { color: 5583650, metalness: 0, roughness: 0.6 }
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "Denim",
|
|
303
|
+
category: "Fabric",
|
|
304
|
+
params: { color: 3889560, metalness: 0, roughness: 0.85 }
|
|
305
|
+
},
|
|
306
|
+
// Skin
|
|
307
|
+
{
|
|
308
|
+
name: "Skin (Light)",
|
|
309
|
+
category: "Skin",
|
|
310
|
+
params: { color: 16767916, metalness: 0, roughness: 0.55 }
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "Skin (Medium)",
|
|
314
|
+
category: "Skin",
|
|
315
|
+
params: { color: 13010498, metalness: 0, roughness: 0.5 }
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "Skin (Dark)",
|
|
319
|
+
category: "Skin",
|
|
320
|
+
params: { color: 9262372, metalness: 0, roughness: 0.5 }
|
|
321
|
+
},
|
|
322
|
+
// Special
|
|
323
|
+
{
|
|
324
|
+
name: "Car Paint",
|
|
325
|
+
category: "Special",
|
|
326
|
+
params: {
|
|
327
|
+
color: 13369344,
|
|
328
|
+
metalness: 0.5,
|
|
329
|
+
roughness: 0.15,
|
|
330
|
+
clearcoat: 1,
|
|
331
|
+
clearcoatRoughness: 0.05
|
|
112
332
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "Emissive (Blue)",
|
|
336
|
+
category: "Special",
|
|
337
|
+
params: {
|
|
338
|
+
color: 1118481,
|
|
339
|
+
metalness: 0,
|
|
340
|
+
roughness: 0.5,
|
|
341
|
+
emissive: 35071,
|
|
342
|
+
emissiveIntensity: 2
|
|
125
343
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "Emissive (Orange)",
|
|
347
|
+
category: "Special",
|
|
348
|
+
params: {
|
|
349
|
+
color: 1118481,
|
|
350
|
+
metalness: 0,
|
|
351
|
+
roughness: 0.5,
|
|
352
|
+
emissive: 16737792,
|
|
353
|
+
emissiveIntensity: 2
|
|
135
354
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const lod = new THREE.LOD();
|
|
147
|
-
lod.name = mesh.name + '_LOD';
|
|
148
|
-
lod.position.copy(mesh.position);
|
|
149
|
-
lod.rotation.copy(mesh.rotation);
|
|
150
|
-
lod.scale.copy(mesh.scale);
|
|
151
|
-
for (const level of levels) {
|
|
152
|
-
const decimated = decimateGeometry(mesh.geometry, level.ratio);
|
|
153
|
-
const mat = Array.isArray(mesh.material)
|
|
154
|
-
? mesh.material.map((m) => m.clone())
|
|
155
|
-
: mesh.material.clone();
|
|
156
|
-
const lodMesh = new THREE.Mesh(decimated, mat);
|
|
157
|
-
lodMesh.castShadow = mesh.castShadow;
|
|
158
|
-
lodMesh.receiveShadow = mesh.receiveShadow;
|
|
159
|
-
lod.addLevel(lodMesh, level.distance);
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "Neon",
|
|
358
|
+
category: "Special",
|
|
359
|
+
params: {
|
|
360
|
+
color: 0,
|
|
361
|
+
metalness: 0,
|
|
362
|
+
roughness: 0.1,
|
|
363
|
+
emissive: 65416,
|
|
364
|
+
emissiveIntensity: 5
|
|
160
365
|
}
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
const PBR_PRESETS = [
|
|
164
|
-
// Metals
|
|
165
|
-
{ name: 'Gold', category: 'Metal', params: { color: 0xffd700, metalness: 1.0, roughness: 0.15 } },
|
|
166
|
-
{
|
|
167
|
-
name: 'Silver',
|
|
168
|
-
category: 'Metal',
|
|
169
|
-
params: { color: 0xc0c0c0, metalness: 1.0, roughness: 0.1 },
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: 'Copper',
|
|
173
|
-
category: 'Metal',
|
|
174
|
-
params: { color: 0xb87333, metalness: 1.0, roughness: 0.25 },
|
|
175
|
-
},
|
|
176
|
-
{ name: 'Iron', category: 'Metal', params: { color: 0x808080, metalness: 0.95, roughness: 0.6 } },
|
|
177
|
-
{
|
|
178
|
-
name: 'Brushed Steel',
|
|
179
|
-
category: 'Metal',
|
|
180
|
-
params: { color: 0xa0a0a0, metalness: 0.95, roughness: 0.4 },
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: 'Chrome',
|
|
184
|
-
category: 'Metal',
|
|
185
|
-
params: { color: 0xdddddd, metalness: 1.0, roughness: 0.02 },
|
|
186
|
-
},
|
|
187
|
-
{
|
|
188
|
-
name: 'Bronze',
|
|
189
|
-
category: 'Metal',
|
|
190
|
-
params: { color: 0xcd7f32, metalness: 0.9, roughness: 0.35 },
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
name: 'Aluminum',
|
|
194
|
-
category: 'Metal',
|
|
195
|
-
params: { color: 0xd6d6d6, metalness: 0.85, roughness: 0.2 },
|
|
196
|
-
},
|
|
197
|
-
// Non-metals
|
|
198
|
-
{
|
|
199
|
-
name: 'Plastic (White)',
|
|
200
|
-
category: 'Plastic',
|
|
201
|
-
params: { color: 0xf0f0f0, metalness: 0.0, roughness: 0.3 },
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
name: 'Plastic (Red)',
|
|
205
|
-
category: 'Plastic',
|
|
206
|
-
params: { color: 0xcc2222, metalness: 0.0, roughness: 0.35 },
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
name: 'Rubber',
|
|
210
|
-
category: 'Plastic',
|
|
211
|
-
params: { color: 0x333333, metalness: 0.0, roughness: 0.9 },
|
|
212
|
-
},
|
|
213
|
-
// Glass
|
|
214
|
-
{
|
|
215
|
-
name: 'Clear Glass',
|
|
216
|
-
category: 'Glass',
|
|
217
|
-
params: {
|
|
218
|
-
color: 0xffffff,
|
|
219
|
-
metalness: 0.0,
|
|
220
|
-
roughness: 0.0,
|
|
221
|
-
opacity: 0.15,
|
|
222
|
-
transparent: true,
|
|
223
|
-
ior: 1.5,
|
|
224
|
-
transmission: 0.95,
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: 'Frosted Glass',
|
|
229
|
-
category: 'Glass',
|
|
230
|
-
params: {
|
|
231
|
-
color: 0xeeeeff,
|
|
232
|
-
metalness: 0.0,
|
|
233
|
-
roughness: 0.5,
|
|
234
|
-
opacity: 0.3,
|
|
235
|
-
transparent: true,
|
|
236
|
-
ior: 1.5,
|
|
237
|
-
transmission: 0.7,
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
name: 'Tinted Glass',
|
|
242
|
-
category: 'Glass',
|
|
243
|
-
params: {
|
|
244
|
-
color: 0x224488,
|
|
245
|
-
metalness: 0.0,
|
|
246
|
-
roughness: 0.05,
|
|
247
|
-
opacity: 0.3,
|
|
248
|
-
transparent: true,
|
|
249
|
-
ior: 1.5,
|
|
250
|
-
transmission: 0.8,
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
// Natural
|
|
254
|
-
{
|
|
255
|
-
name: 'Wood (Light)',
|
|
256
|
-
category: 'Natural',
|
|
257
|
-
params: { color: 0xc19a6b, metalness: 0.0, roughness: 0.7 },
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
name: 'Wood (Dark)',
|
|
261
|
-
category: 'Natural',
|
|
262
|
-
params: { color: 0x5c3317, metalness: 0.0, roughness: 0.65 },
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
name: 'Stone',
|
|
266
|
-
category: 'Natural',
|
|
267
|
-
params: { color: 0x888888, metalness: 0.0, roughness: 0.85 },
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: 'Marble',
|
|
271
|
-
category: 'Natural',
|
|
272
|
-
params: { color: 0xf0ece0, metalness: 0.0, roughness: 0.15 },
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: 'Clay',
|
|
276
|
-
category: 'Natural',
|
|
277
|
-
params: { color: 0xb5651d, metalness: 0.0, roughness: 0.8 },
|
|
278
|
-
},
|
|
279
|
-
{
|
|
280
|
-
name: 'Concrete',
|
|
281
|
-
category: 'Natural',
|
|
282
|
-
params: { color: 0x999999, metalness: 0.0, roughness: 0.95 },
|
|
283
|
-
},
|
|
284
|
-
// Fabric
|
|
285
|
-
{
|
|
286
|
-
name: 'Cotton (White)',
|
|
287
|
-
category: 'Fabric',
|
|
288
|
-
params: {
|
|
289
|
-
color: 0xf5f5dc,
|
|
290
|
-
metalness: 0.0,
|
|
291
|
-
roughness: 0.9,
|
|
292
|
-
sheen: 0.5,
|
|
293
|
-
sheenRoughness: 0.8,
|
|
294
|
-
sheenColor: 0xffffff,
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
name: 'Silk',
|
|
299
|
-
category: 'Fabric',
|
|
300
|
-
params: {
|
|
301
|
-
color: 0xe3dac9,
|
|
302
|
-
metalness: 0.0,
|
|
303
|
-
roughness: 0.3,
|
|
304
|
-
sheen: 1.0,
|
|
305
|
-
sheenRoughness: 0.3,
|
|
306
|
-
sheenColor: 0xffeedd,
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
name: 'Velvet',
|
|
311
|
-
category: 'Fabric',
|
|
312
|
-
params: {
|
|
313
|
-
color: 0x800020,
|
|
314
|
-
metalness: 0.0,
|
|
315
|
-
roughness: 0.95,
|
|
316
|
-
sheen: 0.9,
|
|
317
|
-
sheenRoughness: 0.9,
|
|
318
|
-
sheenColor: 0xff4060,
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
name: 'Leather',
|
|
323
|
-
category: 'Fabric',
|
|
324
|
-
params: { color: 0x553322, metalness: 0.0, roughness: 0.6 },
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
name: 'Denim',
|
|
328
|
-
category: 'Fabric',
|
|
329
|
-
params: { color: 0x3b5998, metalness: 0.0, roughness: 0.85 },
|
|
330
|
-
},
|
|
331
|
-
// Skin
|
|
332
|
-
{
|
|
333
|
-
name: 'Skin (Light)',
|
|
334
|
-
category: 'Skin',
|
|
335
|
-
params: { color: 0xffdbac, metalness: 0.0, roughness: 0.55 },
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
name: 'Skin (Medium)',
|
|
339
|
-
category: 'Skin',
|
|
340
|
-
params: { color: 0xc68642, metalness: 0.0, roughness: 0.5 },
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
name: 'Skin (Dark)',
|
|
344
|
-
category: 'Skin',
|
|
345
|
-
params: { color: 0x8d5524, metalness: 0.0, roughness: 0.5 },
|
|
346
|
-
},
|
|
347
|
-
// Special
|
|
348
|
-
{
|
|
349
|
-
name: 'Car Paint',
|
|
350
|
-
category: 'Special',
|
|
351
|
-
params: {
|
|
352
|
-
color: 0xcc0000,
|
|
353
|
-
metalness: 0.5,
|
|
354
|
-
roughness: 0.15,
|
|
355
|
-
clearcoat: 1.0,
|
|
356
|
-
clearcoatRoughness: 0.05,
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
name: 'Emissive (Blue)',
|
|
361
|
-
category: 'Special',
|
|
362
|
-
params: {
|
|
363
|
-
color: 0x111111,
|
|
364
|
-
metalness: 0.0,
|
|
365
|
-
roughness: 0.5,
|
|
366
|
-
emissive: 0x0088ff,
|
|
367
|
-
emissiveIntensity: 2.0,
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: 'Emissive (Orange)',
|
|
372
|
-
category: 'Special',
|
|
373
|
-
params: {
|
|
374
|
-
color: 0x111111,
|
|
375
|
-
metalness: 0.0,
|
|
376
|
-
roughness: 0.5,
|
|
377
|
-
emissive: 0xff6600,
|
|
378
|
-
emissiveIntensity: 2.0,
|
|
379
|
-
},
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
name: 'Neon',
|
|
383
|
-
category: 'Special',
|
|
384
|
-
params: {
|
|
385
|
-
color: 0x000000,
|
|
386
|
-
metalness: 0.0,
|
|
387
|
-
roughness: 0.1,
|
|
388
|
-
emissive: 0x00ff88,
|
|
389
|
-
emissiveIntensity: 5.0,
|
|
390
|
-
},
|
|
391
|
-
},
|
|
366
|
+
}
|
|
392
367
|
];
|
|
393
|
-
/**
|
|
394
|
-
* Apply a PBR preset to a MeshStandardMaterial or MeshPhysicalMaterial.
|
|
395
|
-
*/
|
|
396
368
|
function applyPBRPreset(material, preset) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (p.emissive != null) {
|
|
406
|
-
material.emissive.setHex(p.emissive);
|
|
407
|
-
}
|
|
408
|
-
if (p.emissiveIntensity != null) {
|
|
409
|
-
material.emissiveIntensity = p.emissiveIntensity;
|
|
410
|
-
}
|
|
411
|
-
if (p.opacity != null) {
|
|
412
|
-
material.opacity = p.opacity;
|
|
413
|
-
}
|
|
414
|
-
material.transparent = (_a = p.transparent) !== null && _a !== void 0 ? _a : false;
|
|
415
|
-
if (p.clearcoat != null) {
|
|
416
|
-
material.clearcoat = p.clearcoat;
|
|
417
|
-
}
|
|
418
|
-
if (p.clearcoatRoughness != null) {
|
|
419
|
-
material.clearcoatRoughness = p.clearcoatRoughness;
|
|
420
|
-
}
|
|
421
|
-
if (p.ior != null) {
|
|
422
|
-
material.ior = p.ior;
|
|
423
|
-
}
|
|
424
|
-
if (p.transmission != null) {
|
|
425
|
-
material.transmission = p.transmission;
|
|
426
|
-
}
|
|
427
|
-
if (p.sheen != null) {
|
|
428
|
-
material.sheen = p.sheen;
|
|
429
|
-
}
|
|
430
|
-
if (p.sheenRoughness != null) {
|
|
431
|
-
material.sheenRoughness = p.sheenRoughness;
|
|
432
|
-
}
|
|
433
|
-
if (p.sheenColor != null) {
|
|
434
|
-
material.sheenColor.setHex(p.sheenColor);
|
|
435
|
-
}
|
|
436
|
-
material.needsUpdate = true;
|
|
369
|
+
const p = preset.params;
|
|
370
|
+
const needsPhysical = p.clearcoat != null || p.transmission != null || p.sheen != null || p.ior != null;
|
|
371
|
+
if (material instanceof THREE.MeshPhysicalMaterial) {
|
|
372
|
+
material.color.setHex(p.color);
|
|
373
|
+
material.metalness = p.metalness;
|
|
374
|
+
material.roughness = p.roughness;
|
|
375
|
+
if (p.emissive != null) {
|
|
376
|
+
material.emissive.setHex(p.emissive);
|
|
437
377
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
378
|
+
if (p.emissiveIntensity != null) {
|
|
379
|
+
material.emissiveIntensity = p.emissiveIntensity;
|
|
380
|
+
}
|
|
381
|
+
if (p.opacity != null) {
|
|
382
|
+
material.opacity = p.opacity;
|
|
383
|
+
}
|
|
384
|
+
material.transparent = p.transparent ?? false;
|
|
385
|
+
if (p.clearcoat != null) {
|
|
386
|
+
material.clearcoat = p.clearcoat;
|
|
387
|
+
}
|
|
388
|
+
if (p.clearcoatRoughness != null) {
|
|
389
|
+
material.clearcoatRoughness = p.clearcoatRoughness;
|
|
390
|
+
}
|
|
391
|
+
if (p.ior != null) {
|
|
392
|
+
material.ior = p.ior;
|
|
453
393
|
}
|
|
394
|
+
if (p.transmission != null) {
|
|
395
|
+
material.transmission = p.transmission;
|
|
396
|
+
}
|
|
397
|
+
if (p.sheen != null) {
|
|
398
|
+
material.sheen = p.sheen;
|
|
399
|
+
}
|
|
400
|
+
if (p.sheenRoughness != null) {
|
|
401
|
+
material.sheenRoughness = p.sheenRoughness;
|
|
402
|
+
}
|
|
403
|
+
if (p.sheenColor != null) {
|
|
404
|
+
material.sheenColor.setHex(p.sheenColor);
|
|
405
|
+
}
|
|
406
|
+
material.needsUpdate = true;
|
|
407
|
+
} else if (material instanceof THREE.MeshStandardMaterial && !needsPhysical) {
|
|
408
|
+
material.color.setHex(p.color);
|
|
409
|
+
material.metalness = p.metalness;
|
|
410
|
+
material.roughness = p.roughness;
|
|
411
|
+
if (p.emissive != null) {
|
|
412
|
+
material.emissive.setHex(p.emissive);
|
|
413
|
+
}
|
|
414
|
+
if (p.emissiveIntensity != null) {
|
|
415
|
+
material.emissiveIntensity = p.emissiveIntensity;
|
|
416
|
+
}
|
|
417
|
+
if (p.opacity != null) {
|
|
418
|
+
material.opacity = p.opacity;
|
|
419
|
+
}
|
|
420
|
+
material.transparent = p.transparent ?? false;
|
|
421
|
+
material.needsUpdate = true;
|
|
422
|
+
}
|
|
454
423
|
}
|
|
455
|
-
/** Create a new MeshPhysicalMaterial from a preset. */
|
|
456
424
|
function createMaterialFromPreset(preset) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
});
|
|
425
|
+
const p = preset.params;
|
|
426
|
+
return new THREE.MeshPhysicalMaterial({
|
|
427
|
+
color: p.color,
|
|
428
|
+
metalness: p.metalness,
|
|
429
|
+
roughness: p.roughness,
|
|
430
|
+
emissive: p.emissive ?? 0,
|
|
431
|
+
emissiveIntensity: p.emissiveIntensity ?? 0,
|
|
432
|
+
opacity: p.opacity ?? 1,
|
|
433
|
+
transparent: p.transparent ?? false,
|
|
434
|
+
clearcoat: p.clearcoat ?? 0,
|
|
435
|
+
clearcoatRoughness: p.clearcoatRoughness ?? 0,
|
|
436
|
+
ior: p.ior ?? 1.5,
|
|
437
|
+
transmission: p.transmission ?? 0,
|
|
438
|
+
sheen: p.sheen ?? 0,
|
|
439
|
+
sheenRoughness: p.sheenRoughness ?? 0,
|
|
440
|
+
sheenColor: new THREE.Color(p.sheenColor ?? 0)
|
|
441
|
+
});
|
|
475
442
|
}
|
|
476
|
-
/* ═══════════════════════════════════════════
|
|
477
|
-
4. INSTANCING
|
|
478
|
-
═══════════════════════════════════════════ */
|
|
479
|
-
/**
|
|
480
|
-
* Convert duplicate meshes to InstancedMesh for better performance.
|
|
481
|
-
* Groups meshes by geometry UUID + material UUID.
|
|
482
|
-
*/
|
|
483
443
|
function convertToInstanced(parent) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
meshes[i].updateWorldMatrix(true, false);
|
|
515
|
-
matrix.copy(meshes[i].matrixWorld);
|
|
516
|
-
instancedMesh.setMatrixAt(i, matrix);
|
|
517
|
-
}
|
|
518
|
-
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
519
|
-
parent.add(instancedMesh);
|
|
520
|
-
created++;
|
|
521
|
-
for (const mesh of meshes) {
|
|
522
|
-
(_a = mesh.parent) === null || _a === void 0 ? void 0 : _a.remove(mesh);
|
|
523
|
-
removed++;
|
|
524
|
-
}
|
|
444
|
+
const groups = /* @__PURE__ */ new Map();
|
|
445
|
+
parent.traverse((child) => {
|
|
446
|
+
if (child instanceof THREE.Mesh && !child.name.startsWith("__")) {
|
|
447
|
+
const geoId = child.geometry.uuid;
|
|
448
|
+
const matId = Array.isArray(child.material) ? child.material.map((m) => m.uuid).join(",") : child.material.uuid;
|
|
449
|
+
const key = `${geoId}|${matId}`;
|
|
450
|
+
let list = groups.get(key);
|
|
451
|
+
if (!list) {
|
|
452
|
+
list = [];
|
|
453
|
+
groups.set(key, list);
|
|
454
|
+
}
|
|
455
|
+
list.push(child);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
let created = 0;
|
|
459
|
+
let removed = 0;
|
|
460
|
+
for (const [, meshes] of groups) {
|
|
461
|
+
if (meshes.length < 2) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const source = meshes[0];
|
|
465
|
+
const instancedMesh = new THREE.InstancedMesh(source.geometry, source.material, meshes.length);
|
|
466
|
+
instancedMesh.name = source.name + "_Instanced";
|
|
467
|
+
instancedMesh.castShadow = source.castShadow;
|
|
468
|
+
instancedMesh.receiveShadow = source.receiveShadow;
|
|
469
|
+
const matrix = new THREE.Matrix4();
|
|
470
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
471
|
+
meshes[i].updateWorldMatrix(true, false);
|
|
472
|
+
matrix.copy(meshes[i].matrixWorld);
|
|
473
|
+
instancedMesh.setMatrixAt(i, matrix);
|
|
525
474
|
}
|
|
526
|
-
|
|
475
|
+
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
476
|
+
parent.add(instancedMesh);
|
|
477
|
+
created++;
|
|
478
|
+
for (const mesh of meshes) {
|
|
479
|
+
mesh.parent?.remove(mesh);
|
|
480
|
+
removed++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { created, removed };
|
|
527
484
|
}
|
|
528
|
-
/* ═══════════════════════════════════════════
|
|
529
|
-
5. GEOMETRY HELPERS
|
|
530
|
-
═══════════════════════════════════════════ */
|
|
531
|
-
/** Count total triangles in a scene. */
|
|
532
485
|
function countTriangles(object) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
count += pos.count / 3;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
486
|
+
let count = 0;
|
|
487
|
+
object.traverse((child) => {
|
|
488
|
+
if (child instanceof THREE.Mesh) {
|
|
489
|
+
const geo = child.geometry;
|
|
490
|
+
if (geo.index) {
|
|
491
|
+
count += geo.index.count / 3;
|
|
492
|
+
} else {
|
|
493
|
+
const pos = geo.getAttribute("position");
|
|
494
|
+
if (pos) {
|
|
495
|
+
count += pos.count / 3;
|
|
546
496
|
}
|
|
547
|
-
|
|
548
|
-
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return Math.round(count);
|
|
549
501
|
}
|
|
550
|
-
/** Compute bounding box for an object and all its children. */
|
|
551
502
|
function computeSceneBounds(object) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
503
|
+
const box = new THREE.Box3();
|
|
504
|
+
object.traverse((child) => {
|
|
505
|
+
if (child instanceof THREE.Mesh) {
|
|
506
|
+
child.geometry.computeBoundingBox();
|
|
507
|
+
if (child.geometry.boundingBox) {
|
|
508
|
+
const worldBox = child.geometry.boundingBox.clone();
|
|
509
|
+
worldBox.applyMatrix4(child.matrixWorld);
|
|
510
|
+
box.union(worldBox);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
return box;
|
|
564
515
|
}
|
|
565
|
-
/** Measure distance between two world-space points. */
|
|
566
516
|
function measureDistance(a, b) {
|
|
567
|
-
|
|
517
|
+
return a.distanceTo(b);
|
|
568
518
|
}
|
|
569
|
-
/** Measure angle (in degrees) between three world-space points (vertex at B). */
|
|
570
519
|
function measureAngle(a, b, c) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
520
|
+
const ba = new THREE.Vector3().subVectors(a, b).normalize();
|
|
521
|
+
const bc = new THREE.Vector3().subVectors(c, b).normalize();
|
|
522
|
+
return THREE.MathUtils.radToDeg(Math.acos(THREE.MathUtils.clamp(ba.dot(bc), -1, 1)));
|
|
574
523
|
}
|
|
575
524
|
const HDRI_PRESETS = [
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
525
|
+
// Studio
|
|
526
|
+
{
|
|
527
|
+
name: "Studio Soft",
|
|
528
|
+
category: "Studio",
|
|
529
|
+
bgColor: 1710638,
|
|
530
|
+
envIntensity: 1,
|
|
531
|
+
ambientColor: 16777215,
|
|
532
|
+
ambientIntensity: 0.6,
|
|
533
|
+
dirColor: 16777215,
|
|
534
|
+
dirIntensity: 1.2,
|
|
535
|
+
dirPosition: [5, 10, 7],
|
|
536
|
+
exposure: 1
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "Studio High Key",
|
|
540
|
+
category: "Studio",
|
|
541
|
+
bgColor: 15790320,
|
|
542
|
+
envIntensity: 1.2,
|
|
543
|
+
ambientColor: 16777215,
|
|
544
|
+
ambientIntensity: 1,
|
|
545
|
+
dirColor: 16777215,
|
|
546
|
+
dirIntensity: 1.5,
|
|
547
|
+
dirPosition: [2, 8, 3],
|
|
548
|
+
exposure: 1.2
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "Studio Low Key",
|
|
552
|
+
category: "Studio",
|
|
553
|
+
bgColor: 657930,
|
|
554
|
+
envIntensity: 0.5,
|
|
555
|
+
ambientColor: 2236996,
|
|
556
|
+
ambientIntensity: 0.2,
|
|
557
|
+
dirColor: 16772829,
|
|
558
|
+
dirIntensity: 2,
|
|
559
|
+
dirPosition: [3, 5, 2],
|
|
560
|
+
exposure: 0.8
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "Studio Rim",
|
|
564
|
+
category: "Studio",
|
|
565
|
+
bgColor: 1118481,
|
|
566
|
+
envIntensity: 0.3,
|
|
567
|
+
ambientColor: 1118498,
|
|
568
|
+
ambientIntensity: 0.15,
|
|
569
|
+
dirColor: 16777215,
|
|
570
|
+
dirIntensity: 2.5,
|
|
571
|
+
dirPosition: [-5, 3, -2],
|
|
572
|
+
exposure: 0.9
|
|
573
|
+
},
|
|
574
|
+
// Outdoor
|
|
575
|
+
{
|
|
576
|
+
name: "Sunny Day",
|
|
577
|
+
category: "Outdoor",
|
|
578
|
+
bgColor: 8900331,
|
|
579
|
+
envIntensity: 1,
|
|
580
|
+
ambientColor: 9357552,
|
|
581
|
+
ambientIntensity: 0.4,
|
|
582
|
+
dirColor: 16774368,
|
|
583
|
+
dirIntensity: 2,
|
|
584
|
+
dirPosition: [5, 10, 5],
|
|
585
|
+
exposure: 1
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "Overcast",
|
|
589
|
+
category: "Outdoor",
|
|
590
|
+
bgColor: 11579576,
|
|
591
|
+
envIntensity: 0.8,
|
|
592
|
+
ambientColor: 13421789,
|
|
593
|
+
ambientIntensity: 0.7,
|
|
594
|
+
dirColor: 14540270,
|
|
595
|
+
dirIntensity: 0.5,
|
|
596
|
+
dirPosition: [0, 10, 0],
|
|
597
|
+
fog: { color: 13421789, near: 20, far: 200 },
|
|
598
|
+
exposure: 1
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
name: "Golden Hour",
|
|
602
|
+
category: "Outdoor",
|
|
603
|
+
bgColor: 16746564,
|
|
604
|
+
envIntensity: 1,
|
|
605
|
+
ambientColor: 16755285,
|
|
606
|
+
ambientIntensity: 0.3,
|
|
607
|
+
dirColor: 16750899,
|
|
608
|
+
dirIntensity: 2.5,
|
|
609
|
+
dirPosition: [10, 2, 5],
|
|
610
|
+
exposure: 1.1
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "Blue Hour",
|
|
614
|
+
category: "Outdoor",
|
|
615
|
+
bgColor: 1714762,
|
|
616
|
+
envIntensity: 0.6,
|
|
617
|
+
ambientColor: 3359846,
|
|
618
|
+
ambientIntensity: 0.3,
|
|
619
|
+
dirColor: 6719692,
|
|
620
|
+
dirIntensity: 0.8,
|
|
621
|
+
dirPosition: [-5, 3, 5],
|
|
622
|
+
exposure: 0.8
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "Night",
|
|
626
|
+
category: "Outdoor",
|
|
627
|
+
bgColor: 328976,
|
|
628
|
+
envIntensity: 0.1,
|
|
629
|
+
ambientColor: 1118515,
|
|
630
|
+
ambientIntensity: 0.05,
|
|
631
|
+
dirColor: 11189213,
|
|
632
|
+
dirIntensity: 0.3,
|
|
633
|
+
dirPosition: [-3, 8, 2],
|
|
634
|
+
exposure: 0.5
|
|
635
|
+
},
|
|
636
|
+
// Interior
|
|
637
|
+
{
|
|
638
|
+
name: "Warm Interior",
|
|
639
|
+
category: "Interior",
|
|
640
|
+
bgColor: 2759184,
|
|
641
|
+
envIntensity: 0.7,
|
|
642
|
+
ambientColor: 16768426,
|
|
643
|
+
ambientIntensity: 0.4,
|
|
644
|
+
dirColor: 16772795,
|
|
645
|
+
dirIntensity: 1,
|
|
646
|
+
dirPosition: [2, 5, 3],
|
|
647
|
+
exposure: 1
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "Cool Office",
|
|
651
|
+
category: "Interior",
|
|
652
|
+
bgColor: 1712688,
|
|
653
|
+
envIntensity: 0.8,
|
|
654
|
+
ambientColor: 14544639,
|
|
655
|
+
ambientIntensity: 0.6,
|
|
656
|
+
dirColor: 16777215,
|
|
657
|
+
dirIntensity: 0.8,
|
|
658
|
+
dirPosition: [0, 8, 0],
|
|
659
|
+
exposure: 1
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: "Gallery",
|
|
663
|
+
category: "Interior",
|
|
664
|
+
bgColor: 16448250,
|
|
665
|
+
envIntensity: 1,
|
|
666
|
+
ambientColor: 16777215,
|
|
667
|
+
ambientIntensity: 0.8,
|
|
668
|
+
dirColor: 16777215,
|
|
669
|
+
dirIntensity: 0.6,
|
|
670
|
+
dirPosition: [0, 10, 2],
|
|
671
|
+
exposure: 1.1
|
|
672
|
+
},
|
|
673
|
+
// Dramatic
|
|
674
|
+
{
|
|
675
|
+
name: "Neon City",
|
|
676
|
+
category: "Dramatic",
|
|
677
|
+
bgColor: 655381,
|
|
678
|
+
envIntensity: 0.4,
|
|
679
|
+
ambientColor: 16711935,
|
|
680
|
+
ambientIntensity: 0.2,
|
|
681
|
+
dirColor: 65535,
|
|
682
|
+
dirIntensity: 1.5,
|
|
683
|
+
dirPosition: [-3, 5, 2],
|
|
684
|
+
exposure: 1
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: "Cinematic",
|
|
688
|
+
category: "Dramatic",
|
|
689
|
+
bgColor: 986906,
|
|
690
|
+
envIntensity: 0.5,
|
|
691
|
+
ambientColor: 2241348,
|
|
692
|
+
ambientIntensity: 0.15,
|
|
693
|
+
dirColor: 16772829,
|
|
694
|
+
dirIntensity: 2,
|
|
695
|
+
dirPosition: [8, 4, 2],
|
|
696
|
+
exposure: 0.9
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "Sunrise",
|
|
700
|
+
category: "Dramatic",
|
|
701
|
+
bgColor: 16737843,
|
|
702
|
+
envIntensity: 0.8,
|
|
703
|
+
ambientColor: 16755302,
|
|
704
|
+
ambientIntensity: 0.3,
|
|
705
|
+
dirColor: 16742212,
|
|
706
|
+
dirIntensity: 2,
|
|
707
|
+
dirPosition: [15, 1, 0],
|
|
708
|
+
exposure: 1.2
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
name: "Desert",
|
|
712
|
+
category: "Dramatic",
|
|
713
|
+
bgColor: 15255688,
|
|
714
|
+
envIntensity: 1,
|
|
715
|
+
ambientColor: 14535833,
|
|
716
|
+
ambientIntensity: 0.5,
|
|
717
|
+
dirColor: 16773324,
|
|
718
|
+
dirIntensity: 2.5,
|
|
719
|
+
dirPosition: [5, 12, 3],
|
|
720
|
+
exposure: 1.3
|
|
721
|
+
}
|
|
773
722
|
];
|
|
774
|
-
/**
|
|
775
|
-
* Apply an HDRI preset to a Three.js scene and renderer.
|
|
776
|
-
*/
|
|
777
723
|
function applyHDRIPreset(scene, renderer, preset) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
scene.traverse((child) => {
|
|
786
|
-
if (child instanceof THREE.AmbientLight && !ambient) {
|
|
787
|
-
ambient = child;
|
|
788
|
-
}
|
|
789
|
-
if (child instanceof THREE.DirectionalLight && !directional) {
|
|
790
|
-
directional = child;
|
|
791
|
-
}
|
|
792
|
-
});
|
|
793
|
-
if (ambient != null) {
|
|
794
|
-
ambient.color.setHex(preset.ambientColor);
|
|
795
|
-
ambient.intensity = preset.ambientIntensity;
|
|
796
|
-
}
|
|
797
|
-
if (directional != null) {
|
|
798
|
-
directional.color.setHex(preset.dirColor);
|
|
799
|
-
directional.intensity = preset.dirIntensity;
|
|
800
|
-
directional.position.set(...preset.dirPosition);
|
|
724
|
+
scene.background = new THREE.Color(preset.bgColor);
|
|
725
|
+
renderer.toneMappingExposure = preset.exposure;
|
|
726
|
+
let ambient;
|
|
727
|
+
let directional;
|
|
728
|
+
scene.traverse((child) => {
|
|
729
|
+
if (child instanceof THREE.AmbientLight && !ambient) {
|
|
730
|
+
ambient = child;
|
|
801
731
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
scene.fog = new THREE.Fog(preset.fog.color, preset.fog.near, preset.fog.far);
|
|
805
|
-
}
|
|
806
|
-
else {
|
|
807
|
-
scene.fog = null;
|
|
732
|
+
if (child instanceof THREE.DirectionalLight && !directional) {
|
|
733
|
+
directional = child;
|
|
808
734
|
}
|
|
735
|
+
});
|
|
736
|
+
if (ambient != null) {
|
|
737
|
+
ambient.color.setHex(preset.ambientColor);
|
|
738
|
+
ambient.intensity = preset.ambientIntensity;
|
|
739
|
+
}
|
|
740
|
+
if (directional != null) {
|
|
741
|
+
directional.color.setHex(preset.dirColor);
|
|
742
|
+
directional.intensity = preset.dirIntensity;
|
|
743
|
+
directional.position.set(...preset.dirPosition);
|
|
744
|
+
}
|
|
745
|
+
if (preset.fog) {
|
|
746
|
+
scene.fog = new THREE.Fog(preset.fog.color, preset.fog.near, preset.fog.far);
|
|
747
|
+
} else {
|
|
748
|
+
scene.fog = null;
|
|
749
|
+
}
|
|
809
750
|
}
|
|
810
|
-
/** Snap a point to the nearest grid intersection. */
|
|
811
751
|
function snapToGrid(point, gridSize) {
|
|
812
|
-
|
|
752
|
+
return new THREE.Vector3(
|
|
753
|
+
Math.round(point.x / gridSize) * gridSize,
|
|
754
|
+
Math.round(point.y / gridSize) * gridSize,
|
|
755
|
+
Math.round(point.z / gridSize) * gridSize
|
|
756
|
+
);
|
|
813
757
|
}
|
|
814
|
-
/** Find the closest vertex in a mesh to a given world-space point. */
|
|
815
758
|
function snapToVertex(point, scene, maxDistance = Infinity) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
759
|
+
let closest = null;
|
|
760
|
+
let minDist = maxDistance;
|
|
761
|
+
const tmpV = new THREE.Vector3();
|
|
762
|
+
scene.traverse((child) => {
|
|
763
|
+
if (!(child instanceof THREE.Mesh)) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const posAttr = child.geometry.getAttribute("position");
|
|
767
|
+
if (!posAttr) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
child.updateWorldMatrix(true, false);
|
|
771
|
+
for (let i = 0; i < posAttr.count; i++) {
|
|
772
|
+
tmpV.fromBufferAttribute(posAttr, i);
|
|
773
|
+
tmpV.applyMatrix4(child.matrixWorld);
|
|
774
|
+
const d = point.distanceTo(tmpV);
|
|
775
|
+
if (d < minDist) {
|
|
776
|
+
minDist = d;
|
|
777
|
+
closest = { point: tmpV.clone(), target: "vertex", object: child };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
return closest;
|
|
839
782
|
}
|
|
840
|
-
/** Find the closest point on any edge in the scene to a given world-space point. */
|
|
841
783
|
function snapToEdge(point, scene, maxDistance = Infinity) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
return closest;
|
|
784
|
+
let closest = null;
|
|
785
|
+
let minDist = maxDistance;
|
|
786
|
+
const a = new THREE.Vector3();
|
|
787
|
+
const b = new THREE.Vector3();
|
|
788
|
+
scene.traverse((child) => {
|
|
789
|
+
if (!(child instanceof THREE.Mesh)) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const posAttr = child.geometry.getAttribute("position");
|
|
793
|
+
if (!posAttr) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
const index = child.geometry.index;
|
|
797
|
+
child.updateWorldMatrix(true, false);
|
|
798
|
+
const processEdge = (i0, i1) => {
|
|
799
|
+
a.fromBufferAttribute(posAttr, i0).applyMatrix4(child.matrixWorld);
|
|
800
|
+
b.fromBufferAttribute(posAttr, i1).applyMatrix4(child.matrixWorld);
|
|
801
|
+
const closestOnEdge = closestPointOnSegment(point, a, b);
|
|
802
|
+
const d = point.distanceTo(closestOnEdge);
|
|
803
|
+
if (d < minDist) {
|
|
804
|
+
minDist = d;
|
|
805
|
+
closest = { point: closestOnEdge.clone(), target: "edge", object: child };
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
if (index) {
|
|
809
|
+
for (let i = 0; i < index.count; i += 3) {
|
|
810
|
+
const i0 = index.array[i], i1 = index.array[i + 1], i2 = index.array[i + 2];
|
|
811
|
+
processEdge(i0, i1);
|
|
812
|
+
processEdge(i1, i2);
|
|
813
|
+
processEdge(i2, i0);
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
for (let i = 0; i < posAttr.count; i += 3) {
|
|
817
|
+
processEdge(i, i + 1);
|
|
818
|
+
processEdge(i + 1, i + 2);
|
|
819
|
+
processEdge(i + 2, i);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
return closest;
|
|
883
824
|
}
|
|
884
|
-
/** Find closest point on a line segment. */
|
|
885
825
|
function closestPointOnSegment(p, a, b) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
826
|
+
const ab = new THREE.Vector3().subVectors(b, a);
|
|
827
|
+
const ap = new THREE.Vector3().subVectors(p, a);
|
|
828
|
+
const t = THREE.MathUtils.clamp(ap.dot(ab) / ab.dot(ab), 0, 1);
|
|
829
|
+
return new THREE.Vector3().addVectors(a, ab.multiplyScalar(t));
|
|
890
830
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
if (result) {
|
|
906
|
-
return result;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
else if (mode === 'grid') {
|
|
910
|
-
return { point: snapToGrid(point, gridSize), target: 'grid' };
|
|
911
|
-
}
|
|
831
|
+
function snapMulti(point, scene, gridSize, modes = ["vertex", "edge", "grid"], maxDistance = 0.5) {
|
|
832
|
+
for (const mode of modes) {
|
|
833
|
+
if (mode === "vertex") {
|
|
834
|
+
const result = snapToVertex(point, scene, maxDistance);
|
|
835
|
+
if (result) {
|
|
836
|
+
return result;
|
|
837
|
+
}
|
|
838
|
+
} else if (mode === "edge") {
|
|
839
|
+
const result = snapToEdge(point, scene, maxDistance);
|
|
840
|
+
if (result) {
|
|
841
|
+
return result;
|
|
842
|
+
}
|
|
843
|
+
} else if (mode === "grid") {
|
|
844
|
+
return { point: snapToGrid(point, gridSize), target: "grid" };
|
|
912
845
|
}
|
|
913
|
-
|
|
846
|
+
}
|
|
847
|
+
return { point: snapToGrid(point, gridSize), target: "grid" };
|
|
914
848
|
}
|
|
915
|
-
/** Evaluate a cubic bezier segment at parameter t (0-1). */
|
|
916
849
|
function cubicBezier(p0, p1, p2, p3, t) {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
850
|
+
const t2 = t * t;
|
|
851
|
+
const t3 = t2 * t;
|
|
852
|
+
const mt = 1 - t;
|
|
853
|
+
const mt2 = mt * mt;
|
|
854
|
+
const mt3 = mt2 * mt;
|
|
855
|
+
return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
|
|
923
856
|
}
|
|
924
|
-
/** Evaluate a keyframe track at a given time. */
|
|
925
857
|
function evaluateTrack(track, time) {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
858
|
+
const { keyframes } = track;
|
|
859
|
+
if (keyframes.length === 0) {
|
|
860
|
+
return 0;
|
|
861
|
+
}
|
|
862
|
+
if (keyframes.length === 1) {
|
|
863
|
+
return keyframes[0].value;
|
|
864
|
+
}
|
|
865
|
+
if (time <= keyframes[0].time) {
|
|
866
|
+
return keyframes[0].value;
|
|
867
|
+
}
|
|
868
|
+
if (time >= keyframes[keyframes.length - 1].time) {
|
|
869
|
+
return keyframes[keyframes.length - 1].value;
|
|
870
|
+
}
|
|
871
|
+
let left = 0;
|
|
872
|
+
for (let i = 1; i < keyframes.length; i++) {
|
|
873
|
+
if (keyframes[i].time >= time) {
|
|
874
|
+
left = i - 1;
|
|
875
|
+
break;
|
|
933
876
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
case 'constant':
|
|
959
|
-
return kfL.value;
|
|
960
|
-
case 'linear':
|
|
961
|
-
return kfL.value + (kfR.value - kfL.value) * t;
|
|
962
|
-
case 'ease-in':
|
|
963
|
-
return kfL.value + (kfR.value - kfL.value) * (t * t);
|
|
964
|
-
case 'ease-out':
|
|
965
|
-
return kfL.value + (kfR.value - kfL.value) * (1 - (1 - t) * (1 - t));
|
|
966
|
-
case 'ease-in-out':
|
|
967
|
-
return (kfL.value + (kfR.value - kfL.value) * (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t)));
|
|
968
|
-
case 'bezier': {
|
|
969
|
-
const handleR = (_a = kfL.handleRight) !== null && _a !== void 0 ? _a : { y: 0 };
|
|
970
|
-
const handleL = (_b = kfR.handleLeft) !== null && _b !== void 0 ? _b : { y: 0 };
|
|
971
|
-
return cubicBezier(kfL.value, kfL.value + handleR.y, kfR.value + handleL.y, kfR.value, t);
|
|
972
|
-
}
|
|
973
|
-
default:
|
|
974
|
-
return kfL.value + (kfR.value - kfL.value) * t;
|
|
877
|
+
}
|
|
878
|
+
const right = left + 1;
|
|
879
|
+
const kfL = keyframes[left];
|
|
880
|
+
const kfR = keyframes[right];
|
|
881
|
+
const dt = kfR.time - kfL.time;
|
|
882
|
+
if (dt === 0) {
|
|
883
|
+
return kfL.value;
|
|
884
|
+
}
|
|
885
|
+
const t = (time - kfL.time) / dt;
|
|
886
|
+
switch (kfL.interpolation) {
|
|
887
|
+
case "constant":
|
|
888
|
+
return kfL.value;
|
|
889
|
+
case "linear":
|
|
890
|
+
return kfL.value + (kfR.value - kfL.value) * t;
|
|
891
|
+
case "ease-in":
|
|
892
|
+
return kfL.value + (kfR.value - kfL.value) * (t * t);
|
|
893
|
+
case "ease-out":
|
|
894
|
+
return kfL.value + (kfR.value - kfL.value) * (1 - (1 - t) * (1 - t));
|
|
895
|
+
case "ease-in-out":
|
|
896
|
+
return kfL.value + (kfR.value - kfL.value) * (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) * (1 - t));
|
|
897
|
+
case "bezier": {
|
|
898
|
+
const handleR = kfL.handleRight ?? { y: 0 };
|
|
899
|
+
const handleL = kfR.handleLeft ?? { y: 0 };
|
|
900
|
+
return cubicBezier(kfL.value, kfL.value + handleR.y, kfR.value + handleL.y, kfR.value, t);
|
|
975
901
|
}
|
|
902
|
+
default:
|
|
903
|
+
return kfL.value + (kfR.value - kfL.value) * t;
|
|
904
|
+
}
|
|
976
905
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
track.keyframes.sort((a, b) => a.time - b.time);
|
|
989
|
-
}
|
|
990
|
-
return kf;
|
|
906
|
+
function setKeyframe(track, time, value, interpolation = "bezier") {
|
|
907
|
+
const epsilon = 1e-3;
|
|
908
|
+
const existingIdx = track.keyframes.findIndex((kf2) => Math.abs(kf2.time - time) < epsilon);
|
|
909
|
+
const kf = { time, value, interpolation };
|
|
910
|
+
if (existingIdx >= 0) {
|
|
911
|
+
track.keyframes[existingIdx] = kf;
|
|
912
|
+
} else {
|
|
913
|
+
track.keyframes.push(kf);
|
|
914
|
+
track.keyframes.sort((a, b) => a.time - b.time);
|
|
915
|
+
}
|
|
916
|
+
return kf;
|
|
991
917
|
}
|
|
992
|
-
/** Remove a keyframe at a given time. */
|
|
993
918
|
function removeKeyframe(track, time) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
919
|
+
const epsilon = 1e-3;
|
|
920
|
+
const idx = track.keyframes.findIndex((kf) => Math.abs(kf.time - time) < epsilon);
|
|
921
|
+
if (idx >= 0) {
|
|
922
|
+
track.keyframes.splice(idx, 1);
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
return false;
|
|
1001
926
|
}
|
|
1002
|
-
/** Convert KeyframeTrack to THREE.KeyframeTrack for use in AnimationClip. */
|
|
1003
927
|
function toThreeKeyframeTrack(track, sampleRate = 30) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
928
|
+
if (track.keyframes.length === 0) {
|
|
929
|
+
return new THREE.NumberKeyframeTrack(track.property, [0], [0]);
|
|
930
|
+
}
|
|
931
|
+
const startTime = track.keyframes[0].time;
|
|
932
|
+
const endTime = track.keyframes[track.keyframes.length - 1].time;
|
|
933
|
+
const duration = endTime - startTime;
|
|
934
|
+
const numSamples = Math.max(2, Math.ceil(duration * sampleRate));
|
|
935
|
+
const times = [];
|
|
936
|
+
const values = [];
|
|
937
|
+
for (let i = 0; i < numSamples; i++) {
|
|
938
|
+
const t = startTime + duration * i / (numSamples - 1);
|
|
939
|
+
times.push(t);
|
|
940
|
+
values.push(evaluateTrack(track, t));
|
|
941
|
+
}
|
|
942
|
+
return new THREE.NumberKeyframeTrack(track.property, times, values);
|
|
1019
943
|
}
|
|
1020
|
-
/** Create an AnimationClip from multiple KeyframeTracks. */
|
|
1021
944
|
function createAnimationClip(name, tracks, sampleRate = 30) {
|
|
1022
|
-
|
|
1023
|
-
|
|
945
|
+
const threeTracks = tracks.map((t) => toThreeKeyframeTrack(t, sampleRate));
|
|
946
|
+
return new THREE.AnimationClip(name, -1, threeTracks);
|
|
1024
947
|
}
|
|
1025
|
-
/** Trigger a file download in the browser. */
|
|
1026
948
|
function downloadBlob(blob, filename) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
949
|
+
const url = URL.createObjectURL(blob);
|
|
950
|
+
const a = document.createElement("a");
|
|
951
|
+
a.href = url;
|
|
952
|
+
a.download = filename;
|
|
953
|
+
document.body.appendChild(a);
|
|
954
|
+
a.click();
|
|
955
|
+
document.body.removeChild(a);
|
|
956
|
+
URL.revokeObjectURL(url);
|
|
1035
957
|
}
|
|
1036
|
-
/** Export scene to ArrayBuffer as GLTF/GLB with optional Draco. */
|
|
1037
958
|
async function exportSceneGLTF(scene, options = {}) {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
959
|
+
const { GLTFExporter } = await import("three/examples/jsm/exporters/GLTFExporter.js");
|
|
960
|
+
const exporter = new GLTFExporter();
|
|
961
|
+
return new Promise((resolve, reject) => {
|
|
962
|
+
exporter.parse(
|
|
963
|
+
scene,
|
|
964
|
+
(result) => resolve(result),
|
|
965
|
+
(error) => reject(error),
|
|
966
|
+
{
|
|
967
|
+
binary: options.binary ?? true,
|
|
968
|
+
animations: options.animations ?? []
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
});
|
|
1047
972
|
}
|
|
1048
|
-
/** Export scene to USDZ format (Apple AR). */
|
|
1049
973
|
async function exportSceneUSDZ(scene) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
return new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' });
|
|
974
|
+
const { USDZExporter } = await import("three/examples/jsm/exporters/USDZExporter.js");
|
|
975
|
+
const exporter = new USDZExporter();
|
|
976
|
+
const arraybuffer = await new Promise((resolve, reject) => {
|
|
977
|
+
exporter.parse(scene, resolve, reject);
|
|
978
|
+
});
|
|
979
|
+
return new Blob([arraybuffer], { type: "model/vnd.usdz+zip" });
|
|
1057
980
|
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Setup crossfade between two animation actions.
|
|
1060
|
-
* Smoothly transitions from current to target over duration.
|
|
1061
|
-
*/
|
|
1062
981
|
function crossfadeAnimations(mixer, fromAction, toAction, duration = 0.5) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
982
|
+
toAction.reset();
|
|
983
|
+
toAction.setEffectiveTimeScale(1);
|
|
984
|
+
toAction.setEffectiveWeight(1);
|
|
985
|
+
toAction.play();
|
|
986
|
+
fromAction.crossFadeTo(toAction, duration, true);
|
|
1068
987
|
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Setup additive blending for an animation action.
|
|
1071
|
-
* Additive animations can layer on top of other animations.
|
|
1072
|
-
*/
|
|
1073
988
|
function makeAdditiveAction(mixer, clip, referenceClip) {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
989
|
+
const action = mixer.clipAction(clip);
|
|
990
|
+
if (referenceClip) {
|
|
991
|
+
THREE.AnimationUtils.makeClipAdditive(clip, 0, referenceClip);
|
|
992
|
+
}
|
|
993
|
+
action.blendMode = THREE.AdditiveAnimationBlendMode;
|
|
994
|
+
return action;
|
|
1080
995
|
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Apply weighted blend to multiple animation actions.
|
|
1083
|
-
* Normalizes weights so they sum to 1.0.
|
|
1084
|
-
*/
|
|
1085
996
|
function blendAnimations(actions) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
997
|
+
const totalWeight = actions.reduce((sum, a) => sum + a.weight, 0);
|
|
998
|
+
if (totalWeight === 0) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
for (const { action, weight } of actions) {
|
|
1002
|
+
const normalizedWeight = weight / totalWeight;
|
|
1003
|
+
action.setEffectiveWeight(normalizedWeight);
|
|
1004
|
+
if (!action.isRunning()) {
|
|
1005
|
+
action.play();
|
|
1096
1006
|
}
|
|
1007
|
+
}
|
|
1097
1008
|
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Create a sub-clip from an existing animation clip (trim).
|
|
1100
|
-
*/
|
|
1101
1009
|
function subclip(clip, name, startFrame, endFrame, fps = 30) {
|
|
1102
|
-
|
|
1010
|
+
return THREE.AnimationUtils.subclip(clip, name, startFrame, endFrame, fps);
|
|
1103
1011
|
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Simple CCD (Cyclic Coordinate Descent) IK solver.
|
|
1106
|
-
* Iteratively rotates each bone in the chain to reach the target.
|
|
1107
|
-
*/
|
|
1108
1012
|
function solveIKChain(skeleton, chain) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
}
|
|
1116
|
-
bones.push(bone);
|
|
1117
|
-
}
|
|
1118
|
-
if (bones.length < 2) {
|
|
1119
|
-
return false;
|
|
1120
|
-
}
|
|
1121
|
-
const endEffector = bones[bones.length - 1];
|
|
1122
|
-
const endPos = new THREE.Vector3();
|
|
1123
|
-
const toEnd = new THREE.Vector3();
|
|
1124
|
-
const toTarget = new THREE.Vector3();
|
|
1125
|
-
const axis = new THREE.Vector3();
|
|
1126
|
-
const quat = new THREE.Quaternion();
|
|
1127
|
-
for (let iter = 0; iter < iterations; iter++) {
|
|
1128
|
-
// Get end-effector world position
|
|
1129
|
-
endEffector.updateWorldMatrix(true, false);
|
|
1130
|
-
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
1131
|
-
if (endPos.distanceTo(target) < tolerance) {
|
|
1132
|
-
return true;
|
|
1133
|
-
}
|
|
1134
|
-
// Iterate bones from end to root (skip end-effector)
|
|
1135
|
-
for (let i = bones.length - 2; i >= 0; i--) {
|
|
1136
|
-
const bone = bones[i];
|
|
1137
|
-
bone.updateWorldMatrix(true, false);
|
|
1138
|
-
const boneWorldPos = new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
|
|
1139
|
-
// Direction from bone to end-effector
|
|
1140
|
-
endEffector.updateWorldMatrix(true, false);
|
|
1141
|
-
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
1142
|
-
toEnd.subVectors(endPos, boneWorldPos).normalize();
|
|
1143
|
-
// Direction from bone to target
|
|
1144
|
-
toTarget.subVectors(target, boneWorldPos).normalize();
|
|
1145
|
-
// Calculate rotation axis and angle
|
|
1146
|
-
const dot = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);
|
|
1147
|
-
const angle = Math.acos(dot);
|
|
1148
|
-
if (angle > 0.0001) {
|
|
1149
|
-
axis.crossVectors(toEnd, toTarget).normalize();
|
|
1150
|
-
// Convert to bone local space
|
|
1151
|
-
const boneWorldQuat = new THREE.Quaternion();
|
|
1152
|
-
bone.getWorldQuaternion(boneWorldQuat);
|
|
1153
|
-
const invWorldQuat = boneWorldQuat.clone().invert();
|
|
1154
|
-
const localAxis = axis.clone().applyQuaternion(invWorldQuat);
|
|
1155
|
-
// Apply limited rotation
|
|
1156
|
-
const maxAngle = Math.PI / 6; // 30° per iteration
|
|
1157
|
-
const clampedAngle = Math.min(angle, maxAngle);
|
|
1158
|
-
quat.setFromAxisAngle(localAxis, clampedAngle);
|
|
1159
|
-
bone.quaternion.premultiply(quat);
|
|
1160
|
-
bone.updateWorldMatrix(true, true);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1013
|
+
const { bones: boneNames, target, iterations = 10, tolerance = 1e-3 } = chain;
|
|
1014
|
+
const bones = [];
|
|
1015
|
+
for (const name of boneNames) {
|
|
1016
|
+
const bone = skeleton.bones.find((b) => b.name === name);
|
|
1017
|
+
if (!bone) {
|
|
1018
|
+
return false;
|
|
1163
1019
|
}
|
|
1020
|
+
bones.push(bone);
|
|
1021
|
+
}
|
|
1022
|
+
if (bones.length < 2) {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
const endEffector = bones[bones.length - 1];
|
|
1026
|
+
const endPos = new THREE.Vector3();
|
|
1027
|
+
const toEnd = new THREE.Vector3();
|
|
1028
|
+
const toTarget = new THREE.Vector3();
|
|
1029
|
+
const axis = new THREE.Vector3();
|
|
1030
|
+
const quat = new THREE.Quaternion();
|
|
1031
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
1164
1032
|
endEffector.updateWorldMatrix(true, false);
|
|
1165
1033
|
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
1166
|
-
|
|
1034
|
+
if (endPos.distanceTo(target) < tolerance) {
|
|
1035
|
+
return true;
|
|
1036
|
+
}
|
|
1037
|
+
for (let i = bones.length - 2; i >= 0; i--) {
|
|
1038
|
+
const bone = bones[i];
|
|
1039
|
+
bone.updateWorldMatrix(true, false);
|
|
1040
|
+
const boneWorldPos = new THREE.Vector3().setFromMatrixPosition(bone.matrixWorld);
|
|
1041
|
+
endEffector.updateWorldMatrix(true, false);
|
|
1042
|
+
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
1043
|
+
toEnd.subVectors(endPos, boneWorldPos).normalize();
|
|
1044
|
+
toTarget.subVectors(target, boneWorldPos).normalize();
|
|
1045
|
+
const dot = THREE.MathUtils.clamp(toEnd.dot(toTarget), -1, 1);
|
|
1046
|
+
const angle = Math.acos(dot);
|
|
1047
|
+
if (angle > 1e-4) {
|
|
1048
|
+
axis.crossVectors(toEnd, toTarget).normalize();
|
|
1049
|
+
const boneWorldQuat = new THREE.Quaternion();
|
|
1050
|
+
bone.getWorldQuaternion(boneWorldQuat);
|
|
1051
|
+
const invWorldQuat = boneWorldQuat.clone().invert();
|
|
1052
|
+
const localAxis = axis.clone().applyQuaternion(invWorldQuat);
|
|
1053
|
+
const maxAngle = Math.PI / 6;
|
|
1054
|
+
const clampedAngle = Math.min(angle, maxAngle);
|
|
1055
|
+
quat.setFromAxisAngle(localAxis, clampedAngle);
|
|
1056
|
+
bone.quaternion.premultiply(quat);
|
|
1057
|
+
bone.updateWorldMatrix(true, true);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
endEffector.updateWorldMatrix(true, false);
|
|
1062
|
+
endPos.setFromMatrixPosition(endEffector.matrixWorld);
|
|
1063
|
+
return endPos.distanceTo(target) < tolerance;
|
|
1167
1064
|
}
|
|
1168
|
-
/** Standard quad viewport layout. */
|
|
1169
1065
|
const QUAD_VIEWPORT = [
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1066
|
+
{ preset: "perspective", x: 0.5, y: 0.5, width: 0.5, height: 0.5 },
|
|
1067
|
+
{ preset: "top", x: 0, y: 0.5, width: 0.5, height: 0.5 },
|
|
1068
|
+
{ preset: "front", x: 0, y: 0, width: 0.5, height: 0.5 },
|
|
1069
|
+
{ preset: "right", x: 0.5, y: 0, width: 0.5, height: 0.5 }
|
|
1174
1070
|
];
|
|
1175
|
-
/** Camera position/rotation for orthographic viewport presets. */
|
|
1176
1071
|
const VIEWPORT_CAMERAS = {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1072
|
+
perspective: { position: [5, 5, 5], up: [0, 1, 0], ortho: false },
|
|
1073
|
+
top: { position: [0, 10, 0], up: [0, 0, -1], ortho: true },
|
|
1074
|
+
front: { position: [0, 0, 10], up: [0, 1, 0], ortho: true },
|
|
1075
|
+
right: { position: [10, 0, 0], up: [0, 1, 0], ortho: true },
|
|
1076
|
+
left: { position: [-10, 0, 0], up: [0, 1, 0], ortho: true },
|
|
1077
|
+
back: { position: [0, 0, -10], up: [0, 1, 0], ortho: true },
|
|
1078
|
+
bottom: { position: [0, -10, 0], up: [0, 0, 1], ortho: true }
|
|
1184
1079
|
};
|
|
1185
|
-
/**
|
|
1186
|
-
* Create a camera for a viewport preset.
|
|
1187
|
-
*/
|
|
1188
1080
|
function createViewportCamera(preset, aspect = 1, frustumSize = 10) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1081
|
+
const config = VIEWPORT_CAMERAS[preset];
|
|
1082
|
+
if (config.ortho) {
|
|
1083
|
+
const camera2 = new THREE.OrthographicCamera(
|
|
1084
|
+
-frustumSize * aspect / 2,
|
|
1085
|
+
frustumSize * aspect / 2,
|
|
1086
|
+
frustumSize / 2,
|
|
1087
|
+
-frustumSize / 2,
|
|
1088
|
+
0.01,
|
|
1089
|
+
1e4
|
|
1090
|
+
);
|
|
1091
|
+
camera2.position.set(...config.position);
|
|
1092
|
+
camera2.up.set(...config.up);
|
|
1093
|
+
camera2.lookAt(0, 0, 0);
|
|
1094
|
+
return camera2;
|
|
1095
|
+
}
|
|
1096
|
+
const camera = new THREE.PerspectiveCamera(50, aspect, 0.01, 1e4);
|
|
1097
|
+
camera.position.set(...config.position);
|
|
1098
|
+
camera.up.set(...config.up);
|
|
1099
|
+
camera.lookAt(0, 0, 0);
|
|
1100
|
+
return camera;
|
|
1202
1101
|
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Render a multi-viewport layout to a single renderer.
|
|
1205
|
-
*/
|
|
1206
1102
|
function renderMultiViewport(renderer, scene, viewports) {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1103
|
+
const { width, height } = renderer.getSize(new THREE.Vector2());
|
|
1104
|
+
renderer.setScissorTest(true);
|
|
1105
|
+
for (const { camera, config } of viewports) {
|
|
1106
|
+
const left = Math.floor(config.x * width);
|
|
1107
|
+
const bottom = Math.floor(config.y * height);
|
|
1108
|
+
const vpWidth = Math.floor(config.width * width);
|
|
1109
|
+
const vpHeight = Math.floor(config.height * height);
|
|
1110
|
+
renderer.setViewport(left, bottom, vpWidth, vpHeight);
|
|
1111
|
+
renderer.setScissor(left, bottom, vpWidth, vpHeight);
|
|
1112
|
+
renderer.render(scene, camera);
|
|
1113
|
+
}
|
|
1114
|
+
renderer.setScissorTest(false);
|
|
1219
1115
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1116
|
+
export {
|
|
1117
|
+
HDRI_PRESETS,
|
|
1118
|
+
PBR_PRESETS,
|
|
1119
|
+
QUAD_VIEWPORT,
|
|
1120
|
+
UndoRedoStack,
|
|
1121
|
+
applyHDRIPreset,
|
|
1122
|
+
applyPBRPreset,
|
|
1123
|
+
blendAnimations,
|
|
1124
|
+
computeSceneBounds,
|
|
1125
|
+
convertToInstanced,
|
|
1126
|
+
countTriangles,
|
|
1127
|
+
createAnimationClip,
|
|
1128
|
+
createMaterialFromPreset,
|
|
1129
|
+
createViewportCamera,
|
|
1130
|
+
crossfadeAnimations,
|
|
1131
|
+
downloadBlob,
|
|
1132
|
+
evaluateTrack,
|
|
1133
|
+
exportSceneGLTF,
|
|
1134
|
+
exportSceneUSDZ,
|
|
1135
|
+
generateLOD,
|
|
1136
|
+
makeAdditiveAction,
|
|
1137
|
+
measureAngle,
|
|
1138
|
+
measureDistance,
|
|
1139
|
+
removeKeyframe,
|
|
1140
|
+
renderMultiViewport,
|
|
1141
|
+
setKeyframe,
|
|
1142
|
+
snapMulti,
|
|
1143
|
+
snapToEdge,
|
|
1144
|
+
snapToGrid,
|
|
1145
|
+
snapToVertex,
|
|
1146
|
+
solveIKChain,
|
|
1147
|
+
subclip,
|
|
1148
|
+
toThreeKeyframeTrack
|
|
1149
|
+
};
|