three-zoo 0.11.6 → 0.11.8
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.d.ts +5 -5
- package/dist/index.js +1069 -955
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/instancedMeshPool/InstancedMeshGroup.d.ts +1 -1
- package/dist/instancedMeshPool/InstancedMeshInstance.d.ts +16 -13
- package/dist/instancedMeshPool/InstancedMeshPool.d.ts +15 -17
- package/package.json +1 -1
- /package/dist/{SkyLight.d.ts → lighting/SkyLight.d.ts} +0 -0
- /package/dist/{Sun.d.ts → lighting/Sun.d.ts} +0 -0
- /package/dist/{DualFovCamera.d.ts → miscellaneous/DualFovCamera.d.ts} +0 -0
- /package/dist/{SceneTraversal.d.ts → miscellaneous/SceneTraversal.d.ts} +0 -0
- /package/dist/{SkinnedMeshBaker.d.ts → miscellaneous/SkinnedMeshBaker.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,246 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/** Default horizontal field of view in degrees */
|
|
4
|
-
const DEFAULT_HORIZONTAL_FOV = 90;
|
|
5
|
-
/** Default vertical field of view in degrees */
|
|
6
|
-
const DEFAULT_VERTICAL_FOV = 90;
|
|
7
|
-
/** Default aspect ratio (width/height) */
|
|
8
|
-
const DEFAULT_ASPECT = 1;
|
|
9
|
-
/** Default near clipping plane distance */
|
|
10
|
-
const DEFAULT_NEAR = 1;
|
|
11
|
-
/** Default far clipping plane distance */
|
|
12
|
-
const DEFAULT_FAR = 1000;
|
|
13
|
-
/** Minimum allowed field of view in degrees */
|
|
14
|
-
const MIN_FOV = 1;
|
|
15
|
-
/** Maximum allowed field of view in degrees */
|
|
16
|
-
const MAX_FOV = 179;
|
|
17
|
-
/**
|
|
18
|
-
* Camera with independent horizontal and vertical FOV settings.
|
|
19
|
-
*/
|
|
20
|
-
class DualFovCamera extends PerspectiveCamera {
|
|
21
|
-
/**
|
|
22
|
-
* @param horizontalFov - Horizontal FOV in degrees (clamped 1-179°)
|
|
23
|
-
* @param verticalFov - Vertical FOV in degrees (clamped 1-179°)
|
|
24
|
-
* @param aspect - Aspect ratio (width/height)
|
|
25
|
-
* @param near - Near clipping plane distance
|
|
26
|
-
* @param far - Far clipping plane distance
|
|
27
|
-
*/
|
|
28
|
-
constructor(horizontalFov = DEFAULT_HORIZONTAL_FOV, verticalFov = DEFAULT_VERTICAL_FOV, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
|
|
29
|
-
super(verticalFov, aspect, near, far);
|
|
30
|
-
this._private_horizontalFovInternal = horizontalFov;
|
|
31
|
-
this._private_verticalFovInternal = verticalFov;
|
|
32
|
-
this.updateProjectionMatrix();
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* @returns Horizontal FOV in degrees
|
|
36
|
-
*/
|
|
37
|
-
get horizontalFov() {
|
|
38
|
-
return this._private_horizontalFovInternal;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* @returns Vertical FOV in degrees
|
|
42
|
-
*/
|
|
43
|
-
get verticalFov() {
|
|
44
|
-
return this._private_verticalFovInternal;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* @param value - Horizontal FOV in degrees (clamped 1-179°)
|
|
48
|
-
*/
|
|
49
|
-
set horizontalFov(value) {
|
|
50
|
-
this._private_horizontalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
51
|
-
this.updateProjectionMatrix();
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* @param value - Vertical FOV in degrees (clamped 1-179°)
|
|
55
|
-
*/
|
|
56
|
-
set verticalFov(value) {
|
|
57
|
-
this._private_verticalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
58
|
-
this.updateProjectionMatrix();
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Sets both FOV values.
|
|
62
|
-
*
|
|
63
|
-
* @param horizontal - Horizontal FOV in degrees (clamped 1-179°)
|
|
64
|
-
* @param vertical - Vertical FOV in degrees (clamped 1-179°)
|
|
65
|
-
*/
|
|
66
|
-
setFov(horizontal, vertical) {
|
|
67
|
-
this._private_horizontalFovInternal = MathUtils.clamp(horizontal, MIN_FOV, MAX_FOV);
|
|
68
|
-
this._private_verticalFovInternal = MathUtils.clamp(vertical, MIN_FOV, MAX_FOV);
|
|
69
|
-
this.updateProjectionMatrix();
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Copies FOV settings from another DualFovCamera.
|
|
73
|
-
*
|
|
74
|
-
* @param source - Source camera to copy from
|
|
75
|
-
*/
|
|
76
|
-
copyFovSettings(source) {
|
|
77
|
-
this._private_horizontalFovInternal = source.horizontalFov;
|
|
78
|
-
this._private_verticalFovInternal = source.verticalFov;
|
|
79
|
-
this.updateProjectionMatrix();
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Updates projection matrix based on FOV and aspect ratio.
|
|
83
|
-
*
|
|
84
|
-
* Landscape (aspect > 1): preserves horizontal FOV.
|
|
85
|
-
* Portrait (aspect ≤ 1): preserves vertical FOV.
|
|
86
|
-
*
|
|
87
|
-
* @override
|
|
88
|
-
*/
|
|
89
|
-
updateProjectionMatrix() {
|
|
90
|
-
if (this.aspect > 1) {
|
|
91
|
-
// Landscape orientation: preserve horizontal FOV
|
|
92
|
-
const radians = MathUtils.degToRad(this._private_horizontalFovInternal);
|
|
93
|
-
this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
// Portrait orientation: preserve vertical FOV
|
|
97
|
-
this.fov = this._private_verticalFovInternal;
|
|
98
|
-
}
|
|
99
|
-
super.updateProjectionMatrix();
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Gets actual horizontal FOV after aspect ratio adjustments.
|
|
103
|
-
*
|
|
104
|
-
* @returns Horizontal FOV in degrees
|
|
105
|
-
*/
|
|
106
|
-
getActualHorizontalFov() {
|
|
107
|
-
if (this.aspect >= 1) {
|
|
108
|
-
return this._private_horizontalFovInternal;
|
|
109
|
-
}
|
|
110
|
-
const verticalRadians = MathUtils.degToRad(this._private_verticalFovInternal);
|
|
111
|
-
return MathUtils.radToDeg(Math.atan(Math.tan(verticalRadians / 2) * this.aspect) * 2);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Gets actual vertical FOV after aspect ratio adjustments.
|
|
115
|
-
*
|
|
116
|
-
* @returns Vertical FOV in degrees
|
|
117
|
-
*/
|
|
118
|
-
getActualVerticalFov() {
|
|
119
|
-
if (this.aspect < 1) {
|
|
120
|
-
return this._private_verticalFovInternal;
|
|
121
|
-
}
|
|
122
|
-
const horizontalRadians = MathUtils.degToRad(this._private_horizontalFovInternal);
|
|
123
|
-
return MathUtils.radToDeg(Math.atan(Math.tan(horizontalRadians / 2) / this.aspect) * 2);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Adjusts vertical FOV to fit points within camera view.
|
|
127
|
-
*
|
|
128
|
-
* @param vertices - Array of 3D points in world coordinates
|
|
129
|
-
*/
|
|
130
|
-
fitVerticalFovToPoints(vertices) {
|
|
131
|
-
const up = new Vector3(0, 1, 0).applyQuaternion(this.quaternion);
|
|
132
|
-
let maxVerticalAngle = 0;
|
|
133
|
-
for (const vertex of vertices) {
|
|
134
|
-
const vertexToCam = this.position.clone().sub(vertex);
|
|
135
|
-
const vertexDirection = vertexToCam.normalize();
|
|
136
|
-
const verticalAngle = Math.asin(Math.abs(vertexDirection.dot(up))) *
|
|
137
|
-
Math.sign(vertexDirection.dot(up));
|
|
138
|
-
if (Math.abs(verticalAngle) > maxVerticalAngle) {
|
|
139
|
-
maxVerticalAngle = Math.abs(verticalAngle);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const requiredFov = MathUtils.radToDeg(2 * maxVerticalAngle);
|
|
143
|
-
this._private_verticalFovInternal = MathUtils.clamp(requiredFov, MIN_FOV, MAX_FOV);
|
|
144
|
-
this.updateProjectionMatrix();
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Adjusts vertical FOV to fit bounding box within camera view.
|
|
148
|
-
*
|
|
149
|
-
* @param box - 3D bounding box in world coordinates
|
|
150
|
-
*/
|
|
151
|
-
fitVerticalFovToBox(box) {
|
|
152
|
-
this.fitVerticalFovToPoints([
|
|
153
|
-
new Vector3(box.min.x, box.min.y, box.min.z),
|
|
154
|
-
new Vector3(box.min.x, box.min.y, box.max.z),
|
|
155
|
-
new Vector3(box.min.x, box.max.y, box.min.z),
|
|
156
|
-
new Vector3(box.min.x, box.max.y, box.max.z),
|
|
157
|
-
new Vector3(box.max.x, box.min.y, box.min.z),
|
|
158
|
-
new Vector3(box.max.x, box.min.y, box.max.z),
|
|
159
|
-
new Vector3(box.max.x, box.max.y, box.min.z),
|
|
160
|
-
new Vector3(box.max.x, box.max.y, box.max.z),
|
|
161
|
-
]);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Adjusts vertical FOV to fit skinned mesh within camera view.
|
|
165
|
-
* Updates skeleton and applies bone transformations.
|
|
166
|
-
*
|
|
167
|
-
* @param skinnedMesh - Skinned mesh with active skeleton
|
|
168
|
-
*/
|
|
169
|
-
fitVerticalFovToMesh(skinnedMesh) {
|
|
170
|
-
skinnedMesh.updateWorldMatrix(true, true);
|
|
171
|
-
skinnedMesh.skeleton.update();
|
|
172
|
-
const bakedGeometry = skinnedMesh.geometry;
|
|
173
|
-
const position = bakedGeometry.attributes["position"];
|
|
174
|
-
const target = new Vector3();
|
|
175
|
-
const points = [];
|
|
176
|
-
for (let i = 0; i < position.count; i++) {
|
|
177
|
-
target.fromBufferAttribute(position, i);
|
|
178
|
-
skinnedMesh.applyBoneTransform(i, target);
|
|
179
|
-
points.push(target.clone());
|
|
180
|
-
}
|
|
181
|
-
this.fitVerticalFovToPoints(points);
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Points camera to look at skinned mesh center of mass.
|
|
185
|
-
* Uses iterative clustering to find main vertex concentration.
|
|
186
|
-
*
|
|
187
|
-
* @param skinnedMesh - Skinned mesh with active skeleton
|
|
188
|
-
*/
|
|
189
|
-
lookAtMeshCenterOfMass(skinnedMesh) {
|
|
190
|
-
skinnedMesh.updateWorldMatrix(true, true);
|
|
191
|
-
skinnedMesh.skeleton.update();
|
|
192
|
-
const bakedGeometry = skinnedMesh.geometry;
|
|
193
|
-
const position = bakedGeometry.attributes.position;
|
|
194
|
-
const target = new Vector3();
|
|
195
|
-
const points = [];
|
|
196
|
-
for (let i = 0; i < position.count; i++) {
|
|
197
|
-
target.fromBufferAttribute(position, i);
|
|
198
|
-
skinnedMesh.applyBoneTransform(i, target);
|
|
199
|
-
points.push(target.clone());
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Finds main cluster center using iterative refinement.
|
|
203
|
-
*
|
|
204
|
-
* @param points - Array of 3D points to cluster
|
|
205
|
-
* @param iterations - Number of refinement iterations
|
|
206
|
-
* @returns Center point of main cluster
|
|
207
|
-
*/
|
|
208
|
-
const findMainCluster = (points, iterations = 3) => {
|
|
209
|
-
if (points.length === 0) {
|
|
210
|
-
return new Vector3();
|
|
211
|
-
}
|
|
212
|
-
let center = points[Math.floor(points.length / 2)].clone();
|
|
213
|
-
for (let i = 0; i < iterations; i++) {
|
|
214
|
-
let total = new Vector3();
|
|
215
|
-
let count = 0;
|
|
216
|
-
for (const point of points) {
|
|
217
|
-
if (point.distanceTo(center) < point.distanceTo(total) ||
|
|
218
|
-
count === 0) {
|
|
219
|
-
total.add(point);
|
|
220
|
-
count++;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (count > 0) {
|
|
224
|
-
center = total.divideScalar(count);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return center;
|
|
228
|
-
};
|
|
229
|
-
const centerOfMass = findMainCluster(points);
|
|
230
|
-
this.lookAt(centerOfMass);
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Creates a copy of this camera with identical settings.
|
|
234
|
-
*
|
|
235
|
-
* @returns New DualFovCamera instance
|
|
236
|
-
* @override
|
|
237
|
-
*/
|
|
238
|
-
clone() {
|
|
239
|
-
const camera = new DualFovCamera(this._private_horizontalFovInternal, this._private_verticalFovInternal, this.aspect, this.near, this.far);
|
|
240
|
-
camera.copy(this, true);
|
|
241
|
-
return camera;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
1
|
+
import { Object3D, Vector3, Quaternion, Matrix4, InstancedMesh, HemisphereLight, RGBAFormat, DirectionalLight, Box3, Spherical, MeshPhysicalMaterial, Color, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, MeshToonMaterial, PerspectiveCamera, MathUtils, Mesh, BufferAttribute, AnimationMixer } from 'three';
|
|
244
2
|
|
|
245
3
|
class InstancedMeshGroup extends Object3D {
|
|
246
4
|
constructor(_private_instances) {
|
|
@@ -261,124 +19,200 @@ class InstancedMeshGroup extends Object3D {
|
|
|
261
19
|
}
|
|
262
20
|
|
|
263
21
|
class InstancedMeshInstance {
|
|
264
|
-
constructor(
|
|
265
|
-
this._private_pool =
|
|
266
|
-
this["entry"] = entry;
|
|
267
|
-
this.index = index;
|
|
268
|
-
this._private_matrix = new Matrix4();
|
|
22
|
+
constructor(_private_pool, geometry, material, tag = "") {
|
|
23
|
+
this._private_pool = _private_pool;
|
|
269
24
|
this._private_position = new Vector3();
|
|
270
25
|
this._private_quaternion = new Quaternion();
|
|
271
26
|
this._private_scale = new Vector3(1, 1, 1);
|
|
27
|
+
this._private_matrix = new Matrix4();
|
|
28
|
+
this._private_needsUpdateMatrixFromTransform = false;
|
|
29
|
+
this._private_needsUpdateTransformFromMatrix = false;
|
|
30
|
+
this._private_needUpdateInstancedMatrixFromLocalMatrix = false;
|
|
31
|
+
this._private_handler = this._private_pool["allocate"](geometry, material, tag);
|
|
272
32
|
}
|
|
273
|
-
|
|
274
|
-
this.
|
|
275
|
-
|
|
276
|
-
|
|
33
|
+
destroy() {
|
|
34
|
+
if (this._private_handler >= 0) {
|
|
35
|
+
this._private_pool["deallocate"](this._private_handler);
|
|
36
|
+
}
|
|
277
37
|
}
|
|
278
|
-
|
|
279
|
-
this.
|
|
280
|
-
this.
|
|
38
|
+
setQuaternion(source, flushTransform = false) {
|
|
39
|
+
this._private_updateTransformFromMatrix();
|
|
40
|
+
if (!this._private_quaternion.equals(source)) {
|
|
41
|
+
this._private_quaternion.copy(source);
|
|
42
|
+
this._private_needsUpdateMatrixFromTransform = true;
|
|
43
|
+
if (flushTransform) {
|
|
44
|
+
this.flushTransform();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
281
47
|
return this;
|
|
282
48
|
}
|
|
283
|
-
|
|
284
|
-
this.
|
|
285
|
-
this.
|
|
49
|
+
setQuaternion4f(x, y, z, w, flushTransform = false) {
|
|
50
|
+
this._private_updateTransformFromMatrix();
|
|
51
|
+
if (this._private_quaternion.x !== x ||
|
|
52
|
+
this._private_quaternion.y !== y ||
|
|
53
|
+
this._private_quaternion.z !== z ||
|
|
54
|
+
this._private_quaternion.w !== w) {
|
|
55
|
+
this._private_quaternion.set(x, y, z, w);
|
|
56
|
+
this._private_needsUpdateMatrixFromTransform = true;
|
|
57
|
+
if (flushTransform) {
|
|
58
|
+
this.flushTransform();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
286
61
|
return this;
|
|
287
62
|
}
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
63
|
+
setScale(source, flushTransform = false) {
|
|
64
|
+
this._private_updateTransformFromMatrix();
|
|
65
|
+
if (!this._private_scale.equals(source)) {
|
|
66
|
+
this._private_scale.copy(source);
|
|
67
|
+
this._private_needsUpdateMatrixFromTransform = true;
|
|
68
|
+
if (flushTransform) {
|
|
69
|
+
this.flushTransform();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
291
72
|
return this;
|
|
292
73
|
}
|
|
293
|
-
|
|
294
|
-
this.
|
|
295
|
-
this.
|
|
74
|
+
setScale3f(x, y, z, flushTransform = false) {
|
|
75
|
+
this._private_updateTransformFromMatrix();
|
|
76
|
+
if (this._private_scale.x !== x || this._private_scale.y !== y || this._private_scale.z !== z) {
|
|
77
|
+
this._private_scale.set(x, y, z);
|
|
78
|
+
this._private_needsUpdateMatrixFromTransform = true;
|
|
79
|
+
if (flushTransform) {
|
|
80
|
+
this.flushTransform();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
296
83
|
return this;
|
|
297
84
|
}
|
|
298
|
-
|
|
299
|
-
this.
|
|
300
|
-
this.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
85
|
+
setTransform(source, flushTransform = false) {
|
|
86
|
+
this._private_updateMatrixFromTransform();
|
|
87
|
+
if (!this._private_matrix.equals(source)) {
|
|
88
|
+
this._private_matrix.copy(source);
|
|
89
|
+
this._private_needsUpdateTransformFromMatrix = true;
|
|
90
|
+
this._private_needUpdateInstancedMatrixFromLocalMatrix = true;
|
|
91
|
+
if (flushTransform) {
|
|
92
|
+
this._private_updateTransformFromMatrix();
|
|
93
|
+
this.flushTransform();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
306
96
|
return this;
|
|
307
97
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
98
|
+
flushTransform() {
|
|
99
|
+
this._private_updateMatrixFromTransform();
|
|
100
|
+
if (this._private_handler >= 0 && this._private_needUpdateInstancedMatrixFromLocalMatrix) {
|
|
101
|
+
this._private_pool["setTransformMatrix"](this._private_handler, this._private_matrix);
|
|
102
|
+
}
|
|
312
103
|
}
|
|
313
|
-
|
|
314
|
-
if (this.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
104
|
+
_private_updateTransformFromMatrix() {
|
|
105
|
+
if (this._private_needsUpdateTransformFromMatrix) {
|
|
106
|
+
this._private_matrix.decompose(this._private_position, this._private_quaternion, this._private_scale);
|
|
107
|
+
this._private_needsUpdateTransformFromMatrix = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
_private_updateMatrixFromTransform() {
|
|
111
|
+
if (this._private_needsUpdateMatrixFromTransform) {
|
|
112
|
+
this._private_matrix.compose(this._private_position, this._private_quaternion, this._private_scale);
|
|
113
|
+
this._private_needsUpdateMatrixFromTransform = false;
|
|
114
|
+
this._private_needUpdateInstancedMatrixFromLocalMatrix = true;
|
|
115
|
+
}
|
|
319
116
|
}
|
|
320
117
|
}
|
|
321
118
|
|
|
322
|
-
const
|
|
119
|
+
const DEFAULT_INITIAL_CAPACITY = 16;
|
|
120
|
+
const DEFAULT_CAPACITY_STEP = 2;
|
|
121
|
+
const MATRIX_SIZE = 16;
|
|
122
|
+
const TEMP_MATRIX_A = new Matrix4();
|
|
123
|
+
const TEMP_MATRIX_B = new Matrix4();
|
|
323
124
|
class InstancedMeshPool {
|
|
324
|
-
constructor(options) {
|
|
125
|
+
constructor(_private_scene, options = {}) {
|
|
325
126
|
var _a, _b;
|
|
326
|
-
this._private_scene =
|
|
327
|
-
this.
|
|
328
|
-
this.
|
|
329
|
-
this.
|
|
127
|
+
this._private_scene = _private_scene;
|
|
128
|
+
this._private_entries = new Map();
|
|
129
|
+
this._private_descriptors = new Map();
|
|
130
|
+
this._private_nextHandler = 1;
|
|
131
|
+
this._private_initialCapacity = (_a = options.initialCapacity) !== null && _a !== void 0 ? _a : DEFAULT_INITIAL_CAPACITY;
|
|
132
|
+
this._private_capacityStep = (_b = options.capacityStep) !== null && _b !== void 0 ? _b : DEFAULT_CAPACITY_STEP;
|
|
133
|
+
}
|
|
134
|
+
isValidHandler(handler) {
|
|
135
|
+
return this._private_descriptors.has(handler);
|
|
136
|
+
}
|
|
137
|
+
sortMeshes(baseRenderOrder, compare) {
|
|
138
|
+
const sorted = [];
|
|
139
|
+
for (const entry of this._private_entries.values()) {
|
|
140
|
+
sorted.push(entry);
|
|
141
|
+
}
|
|
142
|
+
sorted.sort((a, b) => compare(a.mesh, a.tag, b.mesh, b.tag));
|
|
143
|
+
let currentRenderOrder = baseRenderOrder;
|
|
144
|
+
for (const entry of sorted) {
|
|
145
|
+
entry.mesh.renderOrder = currentRenderOrder++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
sortInstances(compare) {
|
|
149
|
+
for (const entry of this._private_entries.values()) {
|
|
150
|
+
this._private_sortEntryInstances(entry, compare);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
dispose() {
|
|
154
|
+
for (const entry of this._private_entries.values()) {
|
|
155
|
+
this._private_scene.remove(entry.mesh);
|
|
156
|
+
entry.mesh.dispose();
|
|
157
|
+
}
|
|
158
|
+
this._private_entries.clear();
|
|
159
|
+
this._private_descriptors.clear();
|
|
330
160
|
}
|
|
331
|
-
allocate(geometry, material) {
|
|
332
|
-
const entry = this._private_getOrCreateEntry(geometry, material);
|
|
161
|
+
allocate(geometry, material, tag) {
|
|
162
|
+
const entry = this._private_getOrCreateEntry(geometry, material, tag);
|
|
333
163
|
if (entry.count === entry.capacity) {
|
|
334
164
|
this._private_growEntry(entry);
|
|
335
165
|
}
|
|
336
166
|
const index = entry.count;
|
|
167
|
+
const handler = this._private_nextHandler++;
|
|
168
|
+
entry.handlers[index] = handler;
|
|
337
169
|
entry.count++;
|
|
338
|
-
const instance = new InstancedMeshInstance(this, entry, index);
|
|
339
|
-
instance.setScale3f(1, 1, 1);
|
|
340
|
-
entry.instances.set(index, instance);
|
|
341
170
|
entry.mesh.count = entry.count;
|
|
342
|
-
|
|
171
|
+
this._private_descriptors.set(handler, { entry, index });
|
|
172
|
+
return handler;
|
|
343
173
|
}
|
|
344
|
-
deallocate(
|
|
345
|
-
|
|
174
|
+
deallocate(handler) {
|
|
175
|
+
const descriptor = this._private_descriptors.get(handler);
|
|
176
|
+
if (descriptor === undefined) {
|
|
346
177
|
return;
|
|
347
|
-
|
|
348
|
-
const removedIndex =
|
|
178
|
+
}
|
|
179
|
+
const { entry, index: removedIndex } = descriptor;
|
|
349
180
|
const lastIndex = entry.count - 1;
|
|
350
|
-
|
|
181
|
+
this._private_descriptors.delete(handler);
|
|
351
182
|
if (removedIndex !== lastIndex) {
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
183
|
+
const lastHandler = entry.handlers[lastIndex];
|
|
184
|
+
const lastDescriptor = this._private_descriptors.get(lastHandler);
|
|
185
|
+
if (lastDescriptor !== undefined) {
|
|
186
|
+
entry.mesh.getMatrixAt(lastIndex, TEMP_MATRIX_A);
|
|
187
|
+
entry.mesh.setMatrixAt(removedIndex, TEMP_MATRIX_A);
|
|
188
|
+
lastDescriptor.index = removedIndex;
|
|
189
|
+
entry.handlers[removedIndex] = lastHandler;
|
|
190
|
+
}
|
|
359
191
|
}
|
|
360
192
|
entry.count--;
|
|
361
193
|
entry.mesh.count = entry.count;
|
|
362
|
-
|
|
363
|
-
this["notifyUpdate"](entry);
|
|
194
|
+
entry.mesh.instanceMatrix.needsUpdate = true;
|
|
364
195
|
}
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
meshes.sort(compare);
|
|
371
|
-
for (const mesh of meshes) {
|
|
372
|
-
this._private_scene.remove(mesh);
|
|
196
|
+
setTransformMatrix(handler, matrix) {
|
|
197
|
+
const descriptor = this._private_descriptors.get(handler);
|
|
198
|
+
if (descriptor === undefined) {
|
|
199
|
+
return;
|
|
373
200
|
}
|
|
374
|
-
|
|
375
|
-
|
|
201
|
+
descriptor.entry.mesh.setMatrixAt(descriptor.index, matrix);
|
|
202
|
+
descriptor.entry.mesh.instanceMatrix.needsUpdate = true;
|
|
203
|
+
}
|
|
204
|
+
getTransformMatrix(handler, target) {
|
|
205
|
+
const descriptor = this._private_descriptors.get(handler);
|
|
206
|
+
if (descriptor === undefined) {
|
|
207
|
+
return undefined;
|
|
376
208
|
}
|
|
209
|
+
descriptor.entry.mesh.getMatrixAt(descriptor.index, target);
|
|
210
|
+
return target;
|
|
377
211
|
}
|
|
378
|
-
_private_getOrCreateEntry(geometry, material) {
|
|
212
|
+
_private_getOrCreateEntry(geometry, material, tag) {
|
|
379
213
|
const key = `${geometry.uuid}:${material.uuid}`;
|
|
380
|
-
let entry = this.
|
|
381
|
-
if (
|
|
214
|
+
let entry = this._private_entries.get(key);
|
|
215
|
+
if (entry === undefined) {
|
|
382
216
|
const mesh = new InstancedMesh(geometry, material, this._private_initialCapacity);
|
|
383
217
|
mesh.count = 0;
|
|
384
218
|
mesh.frustumCulled = false;
|
|
@@ -387,11 +221,12 @@ class InstancedMeshPool {
|
|
|
387
221
|
mesh,
|
|
388
222
|
geometry,
|
|
389
223
|
material,
|
|
224
|
+
tag: tag,
|
|
390
225
|
capacity: this._private_initialCapacity,
|
|
391
226
|
count: 0,
|
|
392
|
-
|
|
227
|
+
handlers: [],
|
|
393
228
|
};
|
|
394
|
-
this.
|
|
229
|
+
this._private_entries.set(key, entry);
|
|
395
230
|
}
|
|
396
231
|
return entry;
|
|
397
232
|
}
|
|
@@ -410,135 +245,480 @@ class InstancedMeshPool {
|
|
|
410
245
|
entry.capacity = newCapacity;
|
|
411
246
|
entry.mesh.count = entry.count;
|
|
412
247
|
}
|
|
413
|
-
|
|
414
|
-
entry.
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
class BasicToPhysicalConverter {
|
|
419
|
-
static convert(material, options = {}) {
|
|
420
|
-
const config = Object.assign({ preserveName: true, copyUserData: true, disposeOriginal: false }, options);
|
|
421
|
-
const physicalMaterial = new MeshPhysicalMaterial();
|
|
422
|
-
this.copyBasicProperties(material, physicalMaterial, config);
|
|
423
|
-
this.convertColorAndPBRProperties(material, physicalMaterial, config);
|
|
424
|
-
this.convertTextureMaps(material, physicalMaterial);
|
|
425
|
-
this.convertTransparencyProperties(material, physicalMaterial);
|
|
426
|
-
this.applyPhysicalProperties(physicalMaterial, config);
|
|
427
|
-
if (config.disposeOriginal) {
|
|
428
|
-
material.dispose();
|
|
248
|
+
_private_sortEntryInstances(entry, compare) {
|
|
249
|
+
if (entry.count <= 1) {
|
|
250
|
+
return;
|
|
429
251
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
252
|
+
const items = [];
|
|
253
|
+
for (let i = 0; i < entry.count; i++) {
|
|
254
|
+
items.push({ originalIndex: i, handler: entry.handlers[i] });
|
|
255
|
+
}
|
|
256
|
+
let needsReorder = false;
|
|
257
|
+
items.sort((a, b) => {
|
|
258
|
+
entry.mesh.getMatrixAt(a.originalIndex, TEMP_MATRIX_A);
|
|
259
|
+
entry.mesh.getMatrixAt(b.originalIndex, TEMP_MATRIX_B);
|
|
260
|
+
const result = compare(TEMP_MATRIX_A, TEMP_MATRIX_B, entry.tag);
|
|
261
|
+
if (result > 0) {
|
|
262
|
+
needsReorder = true;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
});
|
|
266
|
+
if (!needsReorder) {
|
|
267
|
+
return;
|
|
436
268
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
269
|
+
const matricesData = new Float32Array(entry.count * MATRIX_SIZE);
|
|
270
|
+
for (let i = 0; i < items.length; i++) {
|
|
271
|
+
entry.mesh.getMatrixAt(items[i].originalIndex, TEMP_MATRIX_A);
|
|
272
|
+
matricesData.set(TEMP_MATRIX_A.elements, i * MATRIX_SIZE);
|
|
273
|
+
}
|
|
274
|
+
for (let i = 0; i < entry.count; i++) {
|
|
275
|
+
TEMP_MATRIX_A.fromArray(matricesData, i * MATRIX_SIZE);
|
|
276
|
+
entry.mesh.setMatrixAt(i, TEMP_MATRIX_A);
|
|
277
|
+
}
|
|
278
|
+
for (let i = 0; i < items.length; i++) {
|
|
279
|
+
const handler = items[i].handler;
|
|
280
|
+
entry.handlers[i] = handler;
|
|
281
|
+
const descriptor = this._private_descriptors.get(handler);
|
|
282
|
+
if (descriptor !== undefined) {
|
|
283
|
+
descriptor.index = i;
|
|
284
|
+
}
|
|
447
285
|
}
|
|
286
|
+
entry.mesh.instanceMatrix.needsUpdate = true;
|
|
448
287
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Number of color channels in RGBA format */
|
|
291
|
+
const RGBA_CHANNEL_COUNT$1 = 4;
|
|
292
|
+
/** Number of color channels in RGB format */
|
|
293
|
+
const RGB_CHANNEL_COUNT$1 = 3;
|
|
294
|
+
/** Red channel weight for luminance calculation (ITU-R BT.709) */
|
|
295
|
+
const LUMINANCE_R$1 = 0.2126;
|
|
296
|
+
/** Green channel weight for luminance calculation (ITU-R BT.709) */
|
|
297
|
+
const LUMINANCE_G$1 = 0.7152;
|
|
298
|
+
/** Blue channel weight for luminance calculation (ITU-R BT.709) */
|
|
299
|
+
const LUMINANCE_B$1 = 0.0722;
|
|
300
|
+
/** Threshold for upper hemisphere sampling (0 = equator, 1 = top) */
|
|
301
|
+
const SKY_SAMPLE_THRESHOLD = 0.25;
|
|
302
|
+
/** Threshold for lower hemisphere sampling (0 = equator, 1 = bottom) */
|
|
303
|
+
const GROUND_SAMPLE_THRESHOLD = 0.75;
|
|
304
|
+
/**
|
|
305
|
+
* Hemisphere light with HDR environment map support for automatic sky/ground color extraction.
|
|
306
|
+
*/
|
|
307
|
+
class SkyLight extends HemisphereLight {
|
|
308
|
+
/**
|
|
309
|
+
* Sets sky and ground colors from an HDR texture.
|
|
310
|
+
* Analyzes upper hemisphere for sky color and lower hemisphere for ground color.
|
|
311
|
+
*
|
|
312
|
+
* @param texture - HDR texture to analyze (must have image data)
|
|
313
|
+
* @param options - Configuration options for color extraction
|
|
314
|
+
*/
|
|
315
|
+
setColorsFromHDRTexture(texture, options = {}) {
|
|
316
|
+
const { skySampleCount = 100, groundSampleCount = 100, applyGamma = true, gamma = 2.2, } = options;
|
|
317
|
+
const data = texture.image.data;
|
|
318
|
+
const width = texture.image.width;
|
|
319
|
+
const height = texture.image.height;
|
|
320
|
+
const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT$1 : RGB_CHANNEL_COUNT$1;
|
|
321
|
+
const skyPixels = [];
|
|
322
|
+
const groundPixels = [];
|
|
323
|
+
// Sample pixels from upper and lower hemispheres
|
|
324
|
+
for (let i = 0; i < data.length; i += step) {
|
|
325
|
+
const pixelIndex = i / step;
|
|
326
|
+
const y = Math.floor(pixelIndex / width);
|
|
327
|
+
const v = y / height;
|
|
328
|
+
const r = data[i];
|
|
329
|
+
const g = data[i + 1];
|
|
330
|
+
const b = data[i + 2];
|
|
331
|
+
const luminance = LUMINANCE_R$1 * r + LUMINANCE_G$1 * g + LUMINANCE_B$1 * b;
|
|
332
|
+
const pixel = { r, g, b, lum: luminance };
|
|
333
|
+
// Upper hemisphere (sky)
|
|
334
|
+
if (v < SKY_SAMPLE_THRESHOLD) {
|
|
335
|
+
this._private_insertSorted(skyPixels, pixel, skySampleCount, true);
|
|
456
336
|
}
|
|
457
|
-
|
|
458
|
-
|
|
337
|
+
// Lower hemisphere (ground)
|
|
338
|
+
else if (v > GROUND_SAMPLE_THRESHOLD) {
|
|
339
|
+
this._private_insertSorted(groundPixels, pixel, groundSampleCount, false);
|
|
459
340
|
}
|
|
460
341
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
342
|
+
// Calculate average sky color from brightest samples
|
|
343
|
+
const skyColor = this._private_averagePixels(skyPixels);
|
|
344
|
+
// Calculate average ground color
|
|
345
|
+
const groundColor = this._private_averagePixels(groundPixels);
|
|
346
|
+
// Apply gamma correction if needed
|
|
347
|
+
if (applyGamma) {
|
|
348
|
+
const invGamma = 1 / gamma;
|
|
349
|
+
skyColor.r = Math.pow(skyColor.r, invGamma);
|
|
350
|
+
skyColor.g = Math.pow(skyColor.g, invGamma);
|
|
351
|
+
skyColor.b = Math.pow(skyColor.b, invGamma);
|
|
352
|
+
groundColor.r = Math.pow(groundColor.r, invGamma);
|
|
353
|
+
groundColor.g = Math.pow(groundColor.g, invGamma);
|
|
354
|
+
groundColor.b = Math.pow(groundColor.b, invGamma);
|
|
469
355
|
}
|
|
356
|
+
// Normalize HDR values to [0, 1] range
|
|
357
|
+
const maxSky = Math.max(skyColor.r, skyColor.g, skyColor.b, 1);
|
|
358
|
+
const maxGround = Math.max(groundColor.r, groundColor.g, groundColor.b, 1);
|
|
359
|
+
this.color.setRGB(skyColor.r / maxSky, skyColor.g / maxSky, skyColor.b / maxSky);
|
|
360
|
+
this.groundColor.setRGB(groundColor.r / maxGround, groundColor.g / maxGround, groundColor.b / maxGround);
|
|
470
361
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (source.lightMap) {
|
|
479
|
-
target.lightMap = source.lightMap;
|
|
480
|
-
target.lightMapIntensity = source.lightMapIntensity;
|
|
481
|
-
}
|
|
482
|
-
if (source.aoMap) {
|
|
483
|
-
target.aoMap = source.aoMap;
|
|
484
|
-
target.aoMapIntensity = source.aoMapIntensity;
|
|
362
|
+
/**
|
|
363
|
+
* Inserts pixel into sorted array, maintaining size limit.
|
|
364
|
+
*/
|
|
365
|
+
_private_insertSorted(array, pixel, maxSize, sortDescending) {
|
|
366
|
+
if (array.length < maxSize) {
|
|
367
|
+
array.push(pixel);
|
|
368
|
+
array.sort((a, b) => (sortDescending ? b.lum - a.lum : a.lum - b.lum));
|
|
485
369
|
}
|
|
486
|
-
|
|
487
|
-
|
|
370
|
+
else {
|
|
371
|
+
const threshold = array[array.length - 1].lum;
|
|
372
|
+
const shouldInsert = sortDescending
|
|
373
|
+
? pixel.lum > threshold
|
|
374
|
+
: pixel.lum < threshold;
|
|
375
|
+
if (shouldInsert) {
|
|
376
|
+
array.pop();
|
|
377
|
+
array.push(pixel);
|
|
378
|
+
array.sort((a, b) => (sortDescending ? b.lum - a.lum : a.lum - b.lum));
|
|
379
|
+
}
|
|
488
380
|
}
|
|
489
381
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
target.blending = source.blending;
|
|
497
|
-
target.blendSrc = source.blendSrc;
|
|
498
|
-
target.blendDst = source.blendDst;
|
|
499
|
-
target.blendEquation = source.blendEquation;
|
|
500
|
-
target.blendSrcAlpha = source.blendSrcAlpha;
|
|
501
|
-
target.blendDstAlpha = source.blendDstAlpha;
|
|
502
|
-
target.blendEquationAlpha = source.blendEquationAlpha;
|
|
503
|
-
}
|
|
504
|
-
static applyPhysicalProperties(target, config) {
|
|
505
|
-
if (config.clearcoat !== undefined) {
|
|
506
|
-
target.clearcoat = config.clearcoat;
|
|
507
|
-
}
|
|
508
|
-
if (config.clearcoatRoughness !== undefined) {
|
|
509
|
-
target.clearcoatRoughness = config.clearcoatRoughness;
|
|
510
|
-
}
|
|
511
|
-
if (config.sheen !== undefined) {
|
|
512
|
-
target.sheen = config.sheen;
|
|
513
|
-
}
|
|
514
|
-
if (config.transmission !== undefined) {
|
|
515
|
-
target.transmission = config.transmission;
|
|
382
|
+
/**
|
|
383
|
+
* Calculates average color from pixel array.
|
|
384
|
+
*/
|
|
385
|
+
_private_averagePixels(pixels) {
|
|
386
|
+
if (pixels.length === 0) {
|
|
387
|
+
return { r: 0.5, g: 0.5, b: 0.5 };
|
|
516
388
|
}
|
|
517
|
-
|
|
518
|
-
|
|
389
|
+
let totalR = 0;
|
|
390
|
+
let totalG = 0;
|
|
391
|
+
let totalB = 0;
|
|
392
|
+
for (const pixel of pixels) {
|
|
393
|
+
totalR += pixel.r;
|
|
394
|
+
totalG += pixel.g;
|
|
395
|
+
totalB += pixel.b;
|
|
519
396
|
}
|
|
397
|
+
return {
|
|
398
|
+
r: totalR / pixels.length,
|
|
399
|
+
g: totalG / pixels.length,
|
|
400
|
+
b: totalB / pixels.length,
|
|
401
|
+
};
|
|
520
402
|
}
|
|
521
403
|
}
|
|
522
404
|
|
|
523
|
-
/**
|
|
524
|
-
const
|
|
525
|
-
/**
|
|
526
|
-
const
|
|
405
|
+
/** Number of color channels in RGBA format */
|
|
406
|
+
const RGBA_CHANNEL_COUNT = 4;
|
|
407
|
+
/** Number of color channels in RGB format */
|
|
408
|
+
const RGB_CHANNEL_COUNT = 3;
|
|
409
|
+
/** Red channel weight for luminance calculation (ITU-R BT.709) */
|
|
410
|
+
const LUMINANCE_R = 0.2126;
|
|
411
|
+
/** Green channel weight for luminance calculation (ITU-R BT.709) */
|
|
412
|
+
const LUMINANCE_G = 0.7152;
|
|
413
|
+
/** Blue channel weight for luminance calculation (ITU-R BT.709) */
|
|
414
|
+
const LUMINANCE_B = 0.0722;
|
|
527
415
|
/**
|
|
528
|
-
*
|
|
416
|
+
* Directional light with spherical positioning and HDR environment support.
|
|
529
417
|
*/
|
|
530
|
-
class
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
418
|
+
class Sun extends DirectionalLight {
|
|
419
|
+
constructor() {
|
|
420
|
+
super(...arguments);
|
|
421
|
+
/** Internal vectors to avoid garbage collection during calculations */
|
|
422
|
+
this._private_tempVector3D0 = new Vector3();
|
|
423
|
+
this._private_tempVector3D1 = new Vector3();
|
|
424
|
+
this._private_tempVector3D2 = new Vector3();
|
|
425
|
+
this._private_tempVector3D3 = new Vector3();
|
|
426
|
+
this._private_tempVector3D4 = new Vector3();
|
|
427
|
+
this._private_tempVector3D5 = new Vector3();
|
|
428
|
+
this._private_tempVector3D6 = new Vector3();
|
|
429
|
+
this._private_tempVector3D7 = new Vector3();
|
|
430
|
+
this._private_tempBox3 = new Box3();
|
|
431
|
+
this._private_tempSpherical = new Spherical();
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* @returns Distance from light position to origin
|
|
435
|
+
*/
|
|
436
|
+
get distance() {
|
|
437
|
+
return this.position.length();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* @returns Elevation angle in radians (phi angle)
|
|
441
|
+
*/
|
|
442
|
+
get elevation() {
|
|
443
|
+
return this._private_tempSpherical.setFromVector3(this.position).phi;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* @returns Azimuth angle in radians (theta angle)
|
|
447
|
+
*/
|
|
448
|
+
get azimuth() {
|
|
449
|
+
return this._private_tempSpherical.setFromVector3(this.position).theta;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* @param value - New distance in world units
|
|
453
|
+
*/
|
|
454
|
+
set distance(value) {
|
|
455
|
+
this._private_tempSpherical.setFromVector3(this.position);
|
|
456
|
+
this.position.setFromSphericalCoords(value, this._private_tempSpherical.phi, this._private_tempSpherical.theta);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* @param value - New elevation angle in radians (phi angle)
|
|
460
|
+
*/
|
|
461
|
+
set elevation(value) {
|
|
462
|
+
this._private_tempSpherical.setFromVector3(this.position);
|
|
463
|
+
this.position.setFromSphericalCoords(this._private_tempSpherical.radius, value, this._private_tempSpherical.theta);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* @param value - New azimuth angle in radians (theta angle)
|
|
467
|
+
*/
|
|
468
|
+
set azimuth(value) {
|
|
469
|
+
this._private_tempSpherical.setFromVector3(this.position);
|
|
470
|
+
this.position.setFromSphericalCoords(this._private_tempSpherical.radius, this._private_tempSpherical.phi, value);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Sets spherical position of the sun.
|
|
474
|
+
*
|
|
475
|
+
* @param elevation - Elevation angle in radians (phi angle)
|
|
476
|
+
* @param azimuth - Azimuth angle in radians (theta angle)
|
|
477
|
+
* @param distance - Distance from origin in world units
|
|
478
|
+
*/
|
|
479
|
+
setPosition(elevation, azimuth, distance = 1) {
|
|
480
|
+
this.position.setFromSphericalCoords(distance, elevation, azimuth);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Configures shadow camera frustum to cover bounding box.
|
|
484
|
+
*
|
|
485
|
+
* @param box3 - 3D bounding box to cover with shadows
|
|
486
|
+
*/
|
|
487
|
+
configureShadowsForBoundingBox(box3) {
|
|
488
|
+
const camera = this.shadow.camera;
|
|
489
|
+
this.target.updateWorldMatrix(true, false);
|
|
490
|
+
this.lookAt(this.target.getWorldPosition(this._private_tempVector3D0));
|
|
491
|
+
this.updateWorldMatrix(true, false);
|
|
492
|
+
const points = [
|
|
493
|
+
this._private_tempVector3D0.set(box3.min.x, box3.min.y, box3.min.z),
|
|
494
|
+
this._private_tempVector3D1.set(box3.min.x, box3.min.y, box3.max.z),
|
|
495
|
+
this._private_tempVector3D2.set(box3.min.x, box3.max.y, box3.min.z),
|
|
496
|
+
this._private_tempVector3D3.set(box3.min.x, box3.max.y, box3.max.z),
|
|
497
|
+
this._private_tempVector3D4.set(box3.max.x, box3.min.y, box3.min.z),
|
|
498
|
+
this._private_tempVector3D5.set(box3.max.x, box3.min.y, box3.max.z),
|
|
499
|
+
this._private_tempVector3D6.set(box3.max.x, box3.max.y, box3.min.z),
|
|
500
|
+
this._private_tempVector3D7.set(box3.max.x, box3.max.y, box3.max.z),
|
|
501
|
+
];
|
|
502
|
+
const inverseMatrix = this.matrixWorld.clone().invert();
|
|
503
|
+
for (const point of points) {
|
|
504
|
+
point.applyMatrix4(inverseMatrix);
|
|
505
|
+
}
|
|
506
|
+
const newBox3 = this._private_tempBox3.setFromPoints(points);
|
|
507
|
+
camera.left = newBox3.min.x;
|
|
508
|
+
camera.bottom = newBox3.min.y;
|
|
509
|
+
camera.near = -newBox3.max.z;
|
|
510
|
+
camera.right = newBox3.max.x;
|
|
511
|
+
camera.top = newBox3.max.y;
|
|
512
|
+
camera.far = -newBox3.min.z;
|
|
513
|
+
camera.updateWorldMatrix(true, false);
|
|
514
|
+
camera.updateProjectionMatrix();
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Sets sun direction, color and intensity based on brightest point in HDR environment map.
|
|
518
|
+
*
|
|
519
|
+
* @param texture - HDR texture to analyze (must have image data)
|
|
520
|
+
* @param distance - Distance to place sun from origin
|
|
521
|
+
* @param intensityScale - Multiplier for intensity derived from luminance (default: 1)
|
|
522
|
+
*/
|
|
523
|
+
setFromHDRTexture(texture, intensityScale = 1, distance = 1) {
|
|
524
|
+
this.setColorFromHDRTexture(texture);
|
|
525
|
+
this.setIntensityFromHDRTexture(texture, intensityScale);
|
|
526
|
+
this.setDirectionFromHDRTexture(texture, distance);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Sets sun color based on brightest point in HDR environment map.
|
|
530
|
+
*
|
|
531
|
+
* @param texture - HDR texture to analyze (must have image data)
|
|
532
|
+
*/
|
|
533
|
+
setColorFromHDRTexture(texture) {
|
|
534
|
+
const { index } = this._private_findBrightestPixel(texture);
|
|
535
|
+
const data = texture.image.data;
|
|
536
|
+
const r = data[index];
|
|
537
|
+
const g = data[index + 1];
|
|
538
|
+
const b = data[index + 2];
|
|
539
|
+
const maxChannel = Math.max(r, g, b, 1);
|
|
540
|
+
this.color.setRGB(r / maxChannel, g / maxChannel, b / maxChannel);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Sets sun intensity based on luminance of brightest point in HDR environment map.
|
|
544
|
+
*
|
|
545
|
+
* @param texture - HDR texture to analyze (must have image data)
|
|
546
|
+
* @param scale - Multiplier for intensity (default: 1)
|
|
547
|
+
*/
|
|
548
|
+
setIntensityFromHDRTexture(texture, scale = 1) {
|
|
549
|
+
const { luminance } = this._private_findBrightestPixel(texture);
|
|
550
|
+
this.intensity = luminance * scale;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Sets sun direction based on brightest point in HDR environment map.
|
|
554
|
+
*
|
|
555
|
+
* @param texture - HDR texture to analyze (must have image data)
|
|
556
|
+
* @param distance - Distance to place sun from origin
|
|
557
|
+
*/
|
|
558
|
+
setDirectionFromHDRTexture(texture, distance = 1) {
|
|
559
|
+
const { index } = this._private_findBrightestPixel(texture);
|
|
560
|
+
const width = texture.image.width;
|
|
561
|
+
const height = texture.image.height;
|
|
562
|
+
const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
|
|
563
|
+
// Convert to spherical coordinates
|
|
564
|
+
const pixelIndex = index / step;
|
|
565
|
+
const x = pixelIndex % width;
|
|
566
|
+
const y = Math.floor(pixelIndex / width);
|
|
567
|
+
const u = x / width;
|
|
568
|
+
const v = y / height;
|
|
569
|
+
const elevation = v * Math.PI;
|
|
570
|
+
const azimuth = u * -Math.PI * 2 - Math.PI / 2;
|
|
571
|
+
this.position.setFromSphericalCoords(distance, elevation, azimuth);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Finds the brightest pixel in an HDR texture.
|
|
575
|
+
*
|
|
576
|
+
* @param texture - HDR texture to analyze
|
|
577
|
+
* @returns Index and luminance of brightest pixel
|
|
578
|
+
*/
|
|
579
|
+
_private_findBrightestPixel(texture) {
|
|
580
|
+
const data = texture.image.data;
|
|
581
|
+
let maxLuminance = 0;
|
|
582
|
+
let maxIndex = 0;
|
|
583
|
+
const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
|
|
584
|
+
for (let i = 0; i < data.length; i += step) {
|
|
585
|
+
const r = data[i];
|
|
586
|
+
const g = data[i + 1];
|
|
587
|
+
const b = data[i + 2];
|
|
588
|
+
const luminance = LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b;
|
|
589
|
+
if (luminance > maxLuminance) {
|
|
590
|
+
maxLuminance = luminance;
|
|
591
|
+
maxIndex = i;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return { index: maxIndex, luminance: maxLuminance };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
class BasicToPhysicalConverter {
|
|
599
|
+
static convert(material, options = {}) {
|
|
600
|
+
const config = Object.assign({ preserveName: true, copyUserData: true, disposeOriginal: false }, options);
|
|
601
|
+
const physicalMaterial = new MeshPhysicalMaterial();
|
|
602
|
+
this.copyBasicProperties(material, physicalMaterial, config);
|
|
603
|
+
this.convertColorAndPBRProperties(material, physicalMaterial, config);
|
|
604
|
+
this.convertTextureMaps(material, physicalMaterial);
|
|
605
|
+
this.convertTransparencyProperties(material, physicalMaterial);
|
|
606
|
+
this.applyPhysicalProperties(physicalMaterial, config);
|
|
607
|
+
if (config.disposeOriginal) {
|
|
608
|
+
material.dispose();
|
|
609
|
+
}
|
|
610
|
+
physicalMaterial.needsUpdate = true;
|
|
611
|
+
return physicalMaterial;
|
|
612
|
+
}
|
|
613
|
+
static copyBasicProperties(source, target, config) {
|
|
614
|
+
if (config.preserveName) {
|
|
615
|
+
target.name = source.name;
|
|
616
|
+
}
|
|
617
|
+
target.side = source.side;
|
|
618
|
+
target.visible = source.visible;
|
|
619
|
+
target.fog = source.fog;
|
|
620
|
+
target.wireframe = source.wireframe;
|
|
621
|
+
target.wireframeLinewidth = source.wireframeLinewidth;
|
|
622
|
+
target.wireframeLinecap = source.wireframeLinecap;
|
|
623
|
+
target.wireframeLinejoin = source.wireframeLinejoin;
|
|
624
|
+
target.vertexColors = source.vertexColors;
|
|
625
|
+
if (config.copyUserData && source.userData) {
|
|
626
|
+
target.userData = Object.assign({}, source.userData);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
static convertColorAndPBRProperties(source, target, config) {
|
|
630
|
+
if (source.color) {
|
|
631
|
+
target.color = source.color.clone();
|
|
632
|
+
}
|
|
633
|
+
if (config.emissive !== undefined) {
|
|
634
|
+
if (config.emissive instanceof Color) {
|
|
635
|
+
target.emissive.copy(config.emissive);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
target.emissive.set(config.emissive);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (config.emissiveIntensity !== undefined) {
|
|
642
|
+
target.emissiveIntensity = config.emissiveIntensity;
|
|
643
|
+
}
|
|
644
|
+
if (config.metalness !== undefined) {
|
|
645
|
+
target.metalness = config.metalness;
|
|
646
|
+
}
|
|
647
|
+
if (config.roughness !== undefined) {
|
|
648
|
+
target.roughness = config.roughness;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
static convertTextureMaps(source, target) {
|
|
652
|
+
if (source.map) {
|
|
653
|
+
target.map = source.map;
|
|
654
|
+
}
|
|
655
|
+
if (source.alphaMap) {
|
|
656
|
+
target.alphaMap = source.alphaMap;
|
|
657
|
+
}
|
|
658
|
+
if (source.lightMap) {
|
|
659
|
+
target.lightMap = source.lightMap;
|
|
660
|
+
target.lightMapIntensity = source.lightMapIntensity;
|
|
661
|
+
}
|
|
662
|
+
if (source.aoMap) {
|
|
663
|
+
target.aoMap = source.aoMap;
|
|
664
|
+
target.aoMapIntensity = source.aoMapIntensity;
|
|
665
|
+
}
|
|
666
|
+
if (source.envMap) {
|
|
667
|
+
target.envMap = source.envMap;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
static convertTransparencyProperties(source, target) {
|
|
671
|
+
target.transparent = source.transparent;
|
|
672
|
+
target.opacity = source.opacity;
|
|
673
|
+
target.alphaTest = source.alphaTest;
|
|
674
|
+
target.depthTest = source.depthTest;
|
|
675
|
+
target.depthWrite = source.depthWrite;
|
|
676
|
+
target.blending = source.blending;
|
|
677
|
+
target.blendSrc = source.blendSrc;
|
|
678
|
+
target.blendDst = source.blendDst;
|
|
679
|
+
target.blendEquation = source.blendEquation;
|
|
680
|
+
target.blendSrcAlpha = source.blendSrcAlpha;
|
|
681
|
+
target.blendDstAlpha = source.blendDstAlpha;
|
|
682
|
+
target.blendEquationAlpha = source.blendEquationAlpha;
|
|
683
|
+
}
|
|
684
|
+
static applyPhysicalProperties(target, config) {
|
|
685
|
+
if (config.clearcoat !== undefined) {
|
|
686
|
+
target.clearcoat = config.clearcoat;
|
|
687
|
+
}
|
|
688
|
+
if (config.clearcoatRoughness !== undefined) {
|
|
689
|
+
target.clearcoatRoughness = config.clearcoatRoughness;
|
|
690
|
+
}
|
|
691
|
+
if (config.sheen !== undefined) {
|
|
692
|
+
target.sheen = config.sheen;
|
|
693
|
+
}
|
|
694
|
+
if (config.transmission !== undefined) {
|
|
695
|
+
target.transmission = config.transmission;
|
|
696
|
+
}
|
|
697
|
+
if (config.ior !== undefined) {
|
|
698
|
+
target.ior = config.ior;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Factor for metalness brightness adjustment */
|
|
704
|
+
const METALNESS_BRIGHTNESS_FACTOR = 0.3;
|
|
705
|
+
/** Factor for emissive color contribution when combining with base color */
|
|
706
|
+
const EMISSIVE_CONTRIBUTION_FACTOR = 0.5;
|
|
707
|
+
/**
|
|
708
|
+
* Converts MeshStandardMaterial to MeshBasicMaterial with brightness compensation.
|
|
709
|
+
*/
|
|
710
|
+
class StandardToBasicConverter {
|
|
711
|
+
/**
|
|
712
|
+
* Converts MeshStandardMaterial to MeshBasicMaterial.
|
|
713
|
+
*
|
|
714
|
+
* @param standardMaterial - Source material to convert
|
|
715
|
+
* @param options - Conversion options
|
|
716
|
+
* @returns New MeshBasicMaterial with mapped properties
|
|
717
|
+
*/
|
|
718
|
+
static convert(standardMaterial, options = {}) {
|
|
719
|
+
const config = Object.assign({ preserveName: true, copyUserData: true, disposeOriginal: false, combineEmissive: true, brightnessFactor: 1.3 }, options);
|
|
720
|
+
// Create new Basic material
|
|
721
|
+
const basicMaterial = new MeshBasicMaterial();
|
|
542
722
|
// Copy basic material properties
|
|
543
723
|
this.copyBasicProperties(standardMaterial, basicMaterial, config);
|
|
544
724
|
// Handle color properties with lighting compensation
|
|
@@ -1319,622 +1499,556 @@ class StandardToToonConverter {
|
|
|
1319
1499
|
}
|
|
1320
1500
|
}
|
|
1321
1501
|
|
|
1502
|
+
/** Default horizontal field of view in degrees */
|
|
1503
|
+
const DEFAULT_HORIZONTAL_FOV = 90;
|
|
1504
|
+
/** Default vertical field of view in degrees */
|
|
1505
|
+
const DEFAULT_VERTICAL_FOV = 90;
|
|
1506
|
+
/** Default aspect ratio (width/height) */
|
|
1507
|
+
const DEFAULT_ASPECT = 1;
|
|
1508
|
+
/** Default near clipping plane distance */
|
|
1509
|
+
const DEFAULT_NEAR = 1;
|
|
1510
|
+
/** Default far clipping plane distance */
|
|
1511
|
+
const DEFAULT_FAR = 1000;
|
|
1512
|
+
/** Minimum allowed field of view in degrees */
|
|
1513
|
+
const MIN_FOV = 1;
|
|
1514
|
+
/** Maximum allowed field of view in degrees */
|
|
1515
|
+
const MAX_FOV = 179;
|
|
1322
1516
|
/**
|
|
1323
|
-
*
|
|
1324
|
-
*
|
|
1325
|
-
* All methods use depth-first traversal.
|
|
1517
|
+
* Camera with independent horizontal and vertical FOV settings.
|
|
1326
1518
|
*/
|
|
1327
|
-
class
|
|
1328
|
-
/**
|
|
1329
|
-
* Finds first object with exact name match.
|
|
1330
|
-
*
|
|
1331
|
-
* @param object - Root object to start from
|
|
1332
|
-
* @param name - Name to search for (case-sensitive)
|
|
1333
|
-
* @returns First matching object or undefined
|
|
1334
|
-
*/
|
|
1335
|
-
static getObjectByName(object, name) {
|
|
1336
|
-
if (object.name === name) {
|
|
1337
|
-
return object;
|
|
1338
|
-
}
|
|
1339
|
-
for (const child of object.children) {
|
|
1340
|
-
const result = SceneTraversal.getObjectByName(child, name);
|
|
1341
|
-
if (result) {
|
|
1342
|
-
return result;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return undefined;
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* Finds first material with exact name match from mesh objects.
|
|
1349
|
-
*
|
|
1350
|
-
* @param object - Root object to start from
|
|
1351
|
-
* @param name - Material name to search for (case-sensitive)
|
|
1352
|
-
* @returns First matching material or undefined
|
|
1353
|
-
*/
|
|
1354
|
-
static getMaterialByName(object, name) {
|
|
1355
|
-
if (object instanceof Mesh) {
|
|
1356
|
-
if (Array.isArray(object.material)) {
|
|
1357
|
-
for (const material of object.material) {
|
|
1358
|
-
if (material.name === name) {
|
|
1359
|
-
return material;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
else if (object.material.name === name) {
|
|
1364
|
-
return object.material;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
for (const child of object.children) {
|
|
1368
|
-
const material = SceneTraversal.getMaterialByName(child, name);
|
|
1369
|
-
if (material) {
|
|
1370
|
-
return material;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return undefined;
|
|
1374
|
-
}
|
|
1375
|
-
/**
|
|
1376
|
-
* Executes callback for all objects of specified type.
|
|
1377
|
-
*
|
|
1378
|
-
* @template T - Type of objects to process
|
|
1379
|
-
* @param object - Root object to start from
|
|
1380
|
-
* @param type - Constructor to filter by
|
|
1381
|
-
* @param callback - Function to execute for each matching object
|
|
1382
|
-
*/
|
|
1383
|
-
static enumerateObjectsByType(object, type, callback) {
|
|
1384
|
-
if (object instanceof type) {
|
|
1385
|
-
callback(object);
|
|
1386
|
-
}
|
|
1387
|
-
for (const child of object.children) {
|
|
1388
|
-
SceneTraversal.enumerateObjectsByType(child, type, callback);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Executes callback for all materials of specified type from mesh objects.
|
|
1393
|
-
*
|
|
1394
|
-
* @template T - Type of materials to process
|
|
1395
|
-
* @param object - Root object to start from
|
|
1396
|
-
* @param type - Constructor to filter by
|
|
1397
|
-
* @param callback - Function to execute for each matching material
|
|
1398
|
-
*/
|
|
1399
|
-
static enumerateMaterialsByType(object, type, callback) {
|
|
1400
|
-
if (object instanceof Mesh) {
|
|
1401
|
-
if (Array.isArray(object.material)) {
|
|
1402
|
-
for (const material of object.material) {
|
|
1403
|
-
if (material instanceof type) {
|
|
1404
|
-
callback(material);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
else if (object.material instanceof type) {
|
|
1409
|
-
callback(object.material);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
for (const child of object.children) {
|
|
1413
|
-
SceneTraversal.enumerateMaterialsByType(child, type, callback);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Executes callback for all objects in hierarchy.
|
|
1418
|
-
*
|
|
1419
|
-
* @param object - Root object to start from
|
|
1420
|
-
* @param callback - Function to execute for each object
|
|
1421
|
-
*/
|
|
1422
|
-
static enumerateObjects(object, callback) {
|
|
1423
|
-
callback(object);
|
|
1424
|
-
for (const child of object.children) {
|
|
1425
|
-
SceneTraversal.enumerateObjects(child, callback);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
/**
|
|
1429
|
-
* Executes callback for all materials from mesh objects.
|
|
1430
|
-
* If callback returns a material, replaces the original material.
|
|
1431
|
-
*
|
|
1432
|
-
* @param object - Root object to start from
|
|
1433
|
-
* @param callback - Function to execute for each material. Return a material to replace.
|
|
1434
|
-
*/
|
|
1435
|
-
static enumerateMaterials(object, callback) {
|
|
1436
|
-
var _a, _b;
|
|
1437
|
-
if (object instanceof Mesh) {
|
|
1438
|
-
if (Array.isArray(object.material)) {
|
|
1439
|
-
for (let i = 0; i < object.material.length; i++) {
|
|
1440
|
-
object.material[i] =
|
|
1441
|
-
(_a = callback(object.material[i], object)) !== null && _a !== void 0 ? _a : object.material[i];
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
else {
|
|
1445
|
-
object.material = (_b = callback(object.material, object)) !== null && _b !== void 0 ? _b : object.material;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
for (const child of object.children) {
|
|
1449
|
-
SceneTraversal.enumerateMaterials(child, callback);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1519
|
+
class DualFovCamera extends PerspectiveCamera {
|
|
1452
1520
|
/**
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1455
|
-
* @param
|
|
1456
|
-
* @param
|
|
1457
|
-
* @
|
|
1521
|
+
* @param horizontalFov - Horizontal FOV in degrees (clamped 1-179°)
|
|
1522
|
+
* @param verticalFov - Vertical FOV in degrees (clamped 1-179°)
|
|
1523
|
+
* @param aspect - Aspect ratio (width/height)
|
|
1524
|
+
* @param near - Near clipping plane distance
|
|
1525
|
+
* @param far - Far clipping plane distance
|
|
1458
1526
|
*/
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
else {
|
|
1467
|
-
if (object.name && filter.test(object.name)) {
|
|
1468
|
-
result.push(object);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
for (const child of object.children) {
|
|
1472
|
-
result = result.concat(SceneTraversal.filterObjects(child, filter));
|
|
1473
|
-
}
|
|
1474
|
-
return result;
|
|
1527
|
+
constructor(horizontalFov = DEFAULT_HORIZONTAL_FOV, verticalFov = DEFAULT_VERTICAL_FOV, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
|
|
1528
|
+
super(verticalFov, aspect, near, far);
|
|
1529
|
+
this._private_horizontalFovInternal = horizontalFov;
|
|
1530
|
+
this._private_verticalFovInternal = verticalFov;
|
|
1531
|
+
this.updateProjectionMatrix();
|
|
1475
1532
|
}
|
|
1476
|
-
/**
|
|
1477
|
-
*
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
* @returns Array of matching materials
|
|
1482
|
-
*/
|
|
1483
|
-
static filterMaterials(object, filter) {
|
|
1484
|
-
let result = [];
|
|
1485
|
-
if (object instanceof Mesh) {
|
|
1486
|
-
if (Array.isArray(object.material)) {
|
|
1487
|
-
for (const material of object.material) {
|
|
1488
|
-
if (typeof filter === "function") {
|
|
1489
|
-
if (filter(material)) {
|
|
1490
|
-
result.push(material);
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
else if (filter.test(material.name)) {
|
|
1494
|
-
result.push(material);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
else if (typeof filter === "function") {
|
|
1499
|
-
if (filter(object.material)) {
|
|
1500
|
-
result.push(object.material);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
else if (filter.test(object.material.name)) {
|
|
1504
|
-
result.push(object.material);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
for (const child of object.children) {
|
|
1508
|
-
result = result.concat(SceneTraversal.filterMaterials(child, filter));
|
|
1509
|
-
}
|
|
1510
|
-
return result;
|
|
1533
|
+
/**
|
|
1534
|
+
* @returns Horizontal FOV in degrees
|
|
1535
|
+
*/
|
|
1536
|
+
get horizontalFov() {
|
|
1537
|
+
return this._private_horizontalFovInternal;
|
|
1511
1538
|
}
|
|
1512
1539
|
/**
|
|
1513
|
-
*
|
|
1540
|
+
* @returns Vertical FOV in degrees
|
|
1541
|
+
*/
|
|
1542
|
+
get verticalFov() {
|
|
1543
|
+
return this._private_verticalFovInternal;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* @param value - Horizontal FOV in degrees (clamped 1-179°)
|
|
1547
|
+
*/
|
|
1548
|
+
set horizontalFov(value) {
|
|
1549
|
+
this._private_horizontalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
1550
|
+
this.updateProjectionMatrix();
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* @param value - Vertical FOV in degrees (clamped 1-179°)
|
|
1554
|
+
*/
|
|
1555
|
+
set verticalFov(value) {
|
|
1556
|
+
this._private_verticalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
1557
|
+
this.updateProjectionMatrix();
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Sets both FOV values.
|
|
1514
1561
|
*
|
|
1515
|
-
* @param
|
|
1516
|
-
* @param
|
|
1517
|
-
* @returns Array of mesh objects using the materials
|
|
1562
|
+
* @param horizontal - Horizontal FOV in degrees (clamped 1-179°)
|
|
1563
|
+
* @param vertical - Vertical FOV in degrees (clamped 1-179°)
|
|
1518
1564
|
*/
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
if (Array.isArray(object.material)) {
|
|
1524
|
-
for (const material of object.material) {
|
|
1525
|
-
if (materials.includes(material)) {
|
|
1526
|
-
hasMatchingMaterial = true;
|
|
1527
|
-
break;
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
else {
|
|
1532
|
-
if (materials.includes(object.material)) {
|
|
1533
|
-
hasMatchingMaterial = true;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
if (hasMatchingMaterial) {
|
|
1537
|
-
result.push(object);
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
for (const child of object.children) {
|
|
1541
|
-
result = result.concat(SceneTraversal.findMaterialUsers(child, materials));
|
|
1542
|
-
}
|
|
1543
|
-
return result;
|
|
1565
|
+
setFov(horizontal, vertical) {
|
|
1566
|
+
this._private_horizontalFovInternal = MathUtils.clamp(horizontal, MIN_FOV, MAX_FOV);
|
|
1567
|
+
this._private_verticalFovInternal = MathUtils.clamp(vertical, MIN_FOV, MAX_FOV);
|
|
1568
|
+
this.updateProjectionMatrix();
|
|
1544
1569
|
}
|
|
1545
1570
|
/**
|
|
1546
|
-
*
|
|
1571
|
+
* Copies FOV settings from another DualFovCamera.
|
|
1547
1572
|
*
|
|
1548
|
-
* @param
|
|
1549
|
-
* @param name - Material name to search for (case-sensitive)
|
|
1550
|
-
* @returns Cloned material or undefined if not found
|
|
1573
|
+
* @param source - Source camera to copy from
|
|
1551
1574
|
*/
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
const clonedMaterial = originalMaterial.clone();
|
|
1558
|
-
SceneTraversal.replaceMaterial(object, originalMaterial, clonedMaterial);
|
|
1559
|
-
return clonedMaterial;
|
|
1575
|
+
copyFovSettings(source) {
|
|
1576
|
+
this._private_horizontalFovInternal = source.horizontalFov;
|
|
1577
|
+
this._private_verticalFovInternal = source.verticalFov;
|
|
1578
|
+
this.updateProjectionMatrix();
|
|
1560
1579
|
}
|
|
1561
1580
|
/**
|
|
1562
|
-
*
|
|
1581
|
+
* Updates projection matrix based on FOV and aspect ratio.
|
|
1563
1582
|
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1583
|
+
* Landscape (aspect > 1): preserves horizontal FOV.
|
|
1584
|
+
* Portrait (aspect ≤ 1): preserves vertical FOV.
|
|
1585
|
+
*
|
|
1586
|
+
* @override
|
|
1567
1587
|
*/
|
|
1568
|
-
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
else if (object.material === oldMaterial) {
|
|
1574
|
-
object.material = newMaterial;
|
|
1575
|
-
}
|
|
1588
|
+
updateProjectionMatrix() {
|
|
1589
|
+
if (this.aspect > 1) {
|
|
1590
|
+
// Landscape orientation: preserve horizontal FOV
|
|
1591
|
+
const radians = MathUtils.degToRad(this._private_horizontalFovInternal);
|
|
1592
|
+
this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
|
|
1576
1593
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1594
|
+
else {
|
|
1595
|
+
// Portrait orientation: preserve vertical FOV
|
|
1596
|
+
this.fov = this._private_verticalFovInternal;
|
|
1579
1597
|
}
|
|
1598
|
+
super.updateProjectionMatrix();
|
|
1580
1599
|
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
/** Number of components per vertex */
|
|
1584
|
-
const COMPONENT_COUNT = 3;
|
|
1585
|
-
/** Converts skinned meshes to static meshes. */
|
|
1586
|
-
class SkinnedMeshBaker {
|
|
1587
1600
|
/**
|
|
1588
|
-
*
|
|
1601
|
+
* Gets actual horizontal FOV after aspect ratio adjustments.
|
|
1589
1602
|
*
|
|
1590
|
-
* @
|
|
1591
|
-
* @returns Static mesh with baked positions
|
|
1603
|
+
* @returns Horizontal FOV in degrees
|
|
1592
1604
|
*/
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const newPositions = new Float32Array(position.count * COMPONENT_COUNT);
|
|
1597
|
-
const target = new Vector3();
|
|
1598
|
-
for (let i = 0; i < position.count; i++) {
|
|
1599
|
-
target.fromBufferAttribute(position, i);
|
|
1600
|
-
skinnedMesh.applyBoneTransform(i, target);
|
|
1601
|
-
newPositions[i * COMPONENT_COUNT + 0] = target.x;
|
|
1602
|
-
newPositions[i * COMPONENT_COUNT + 1] = target.y;
|
|
1603
|
-
newPositions[i * COMPONENT_COUNT + 2] = target.z;
|
|
1605
|
+
getActualHorizontalFov() {
|
|
1606
|
+
if (this.aspect >= 1) {
|
|
1607
|
+
return this._private_horizontalFovInternal;
|
|
1604
1608
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
bakedGeometry.deleteAttribute("skinIndex");
|
|
1608
|
-
bakedGeometry.deleteAttribute("skinWeight");
|
|
1609
|
-
const mesh = new Mesh(bakedGeometry, skinnedMesh.material);
|
|
1610
|
-
mesh.name = skinnedMesh.name;
|
|
1611
|
-
return mesh;
|
|
1609
|
+
const verticalRadians = MathUtils.degToRad(this._private_verticalFovInternal);
|
|
1610
|
+
return MathUtils.radToDeg(Math.atan(Math.tan(verticalRadians / 2) * this.aspect) * 2);
|
|
1612
1611
|
}
|
|
1613
1612
|
/**
|
|
1614
|
-
*
|
|
1613
|
+
* Gets actual vertical FOV after aspect ratio adjustments.
|
|
1615
1614
|
*
|
|
1616
|
-
* @
|
|
1617
|
-
* @param skinnedMesh - Mesh to convert
|
|
1618
|
-
* @param timeOffset - Time in seconds within animation
|
|
1619
|
-
* @param clip - Animation clip for pose
|
|
1620
|
-
* @returns Static mesh with baked positions
|
|
1615
|
+
* @returns Vertical FOV in degrees
|
|
1621
1616
|
*/
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
skinnedMesh.skeleton.update();
|
|
1629
|
-
return this.bakePose(skinnedMesh);
|
|
1617
|
+
getActualVerticalFov() {
|
|
1618
|
+
if (this.aspect < 1) {
|
|
1619
|
+
return this._private_verticalFovInternal;
|
|
1620
|
+
}
|
|
1621
|
+
const horizontalRadians = MathUtils.degToRad(this._private_horizontalFovInternal);
|
|
1622
|
+
return MathUtils.radToDeg(Math.atan(Math.tan(horizontalRadians / 2) / this.aspect) * 2);
|
|
1630
1623
|
}
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
/** Number of color channels in RGBA format */
|
|
1634
|
-
const RGBA_CHANNEL_COUNT$1 = 4;
|
|
1635
|
-
/** Number of color channels in RGB format */
|
|
1636
|
-
const RGB_CHANNEL_COUNT$1 = 3;
|
|
1637
|
-
/** Red channel weight for luminance calculation (ITU-R BT.709) */
|
|
1638
|
-
const LUMINANCE_R$1 = 0.2126;
|
|
1639
|
-
/** Green channel weight for luminance calculation (ITU-R BT.709) */
|
|
1640
|
-
const LUMINANCE_G$1 = 0.7152;
|
|
1641
|
-
/** Blue channel weight for luminance calculation (ITU-R BT.709) */
|
|
1642
|
-
const LUMINANCE_B$1 = 0.0722;
|
|
1643
|
-
/** Threshold for upper hemisphere sampling (0 = equator, 1 = top) */
|
|
1644
|
-
const SKY_SAMPLE_THRESHOLD = 0.25;
|
|
1645
|
-
/** Threshold for lower hemisphere sampling (0 = equator, 1 = bottom) */
|
|
1646
|
-
const GROUND_SAMPLE_THRESHOLD = 0.75;
|
|
1647
|
-
/**
|
|
1648
|
-
* Hemisphere light with HDR environment map support for automatic sky/ground color extraction.
|
|
1649
|
-
*/
|
|
1650
|
-
class SkyLight extends HemisphereLight {
|
|
1651
1624
|
/**
|
|
1652
|
-
*
|
|
1653
|
-
* Analyzes upper hemisphere for sky color and lower hemisphere for ground color.
|
|
1625
|
+
* Adjusts vertical FOV to fit points within camera view.
|
|
1654
1626
|
*
|
|
1655
|
-
* @param
|
|
1656
|
-
* @param options - Configuration options for color extraction
|
|
1627
|
+
* @param vertices - Array of 3D points in world coordinates
|
|
1657
1628
|
*/
|
|
1658
|
-
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const pixelIndex = i / step;
|
|
1669
|
-
const y = Math.floor(pixelIndex / width);
|
|
1670
|
-
const v = y / height;
|
|
1671
|
-
const r = data[i];
|
|
1672
|
-
const g = data[i + 1];
|
|
1673
|
-
const b = data[i + 2];
|
|
1674
|
-
const luminance = LUMINANCE_R$1 * r + LUMINANCE_G$1 * g + LUMINANCE_B$1 * b;
|
|
1675
|
-
const pixel = { r, g, b, lum: luminance };
|
|
1676
|
-
// Upper hemisphere (sky)
|
|
1677
|
-
if (v < SKY_SAMPLE_THRESHOLD) {
|
|
1678
|
-
this._private_insertSorted(skyPixels, pixel, skySampleCount, true);
|
|
1679
|
-
}
|
|
1680
|
-
// Lower hemisphere (ground)
|
|
1681
|
-
else if (v > GROUND_SAMPLE_THRESHOLD) {
|
|
1682
|
-
this._private_insertSorted(groundPixels, pixel, groundSampleCount, false);
|
|
1629
|
+
fitVerticalFovToPoints(vertices) {
|
|
1630
|
+
const up = new Vector3(0, 1, 0).applyQuaternion(this.quaternion);
|
|
1631
|
+
let maxVerticalAngle = 0;
|
|
1632
|
+
for (const vertex of vertices) {
|
|
1633
|
+
const vertexToCam = this.position.clone().sub(vertex);
|
|
1634
|
+
const vertexDirection = vertexToCam.normalize();
|
|
1635
|
+
const verticalAngle = Math.asin(Math.abs(vertexDirection.dot(up))) *
|
|
1636
|
+
Math.sign(vertexDirection.dot(up));
|
|
1637
|
+
if (Math.abs(verticalAngle) > maxVerticalAngle) {
|
|
1638
|
+
maxVerticalAngle = Math.abs(verticalAngle);
|
|
1683
1639
|
}
|
|
1684
1640
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1641
|
+
const requiredFov = MathUtils.radToDeg(2 * maxVerticalAngle);
|
|
1642
|
+
this._private_verticalFovInternal = MathUtils.clamp(requiredFov, MIN_FOV, MAX_FOV);
|
|
1643
|
+
this.updateProjectionMatrix();
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Adjusts vertical FOV to fit bounding box within camera view.
|
|
1647
|
+
*
|
|
1648
|
+
* @param box - 3D bounding box in world coordinates
|
|
1649
|
+
*/
|
|
1650
|
+
fitVerticalFovToBox(box) {
|
|
1651
|
+
this.fitVerticalFovToPoints([
|
|
1652
|
+
new Vector3(box.min.x, box.min.y, box.min.z),
|
|
1653
|
+
new Vector3(box.min.x, box.min.y, box.max.z),
|
|
1654
|
+
new Vector3(box.min.x, box.max.y, box.min.z),
|
|
1655
|
+
new Vector3(box.min.x, box.max.y, box.max.z),
|
|
1656
|
+
new Vector3(box.max.x, box.min.y, box.min.z),
|
|
1657
|
+
new Vector3(box.max.x, box.min.y, box.max.z),
|
|
1658
|
+
new Vector3(box.max.x, box.max.y, box.min.z),
|
|
1659
|
+
new Vector3(box.max.x, box.max.y, box.max.z),
|
|
1660
|
+
]);
|
|
1704
1661
|
}
|
|
1705
1662
|
/**
|
|
1706
|
-
*
|
|
1663
|
+
* Adjusts vertical FOV to fit skinned mesh within camera view.
|
|
1664
|
+
* Updates skeleton and applies bone transformations.
|
|
1665
|
+
*
|
|
1666
|
+
* @param skinnedMesh - Skinned mesh with active skeleton
|
|
1707
1667
|
*/
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
array.pop();
|
|
1720
|
-
array.push(pixel);
|
|
1721
|
-
array.sort((a, b) => (sortDescending ? b.lum - a.lum : a.lum - b.lum));
|
|
1722
|
-
}
|
|
1668
|
+
fitVerticalFovToMesh(skinnedMesh) {
|
|
1669
|
+
skinnedMesh.updateWorldMatrix(true, true);
|
|
1670
|
+
skinnedMesh.skeleton.update();
|
|
1671
|
+
const bakedGeometry = skinnedMesh.geometry;
|
|
1672
|
+
const position = bakedGeometry.attributes["position"];
|
|
1673
|
+
const target = new Vector3();
|
|
1674
|
+
const points = [];
|
|
1675
|
+
for (let i = 0; i < position.count; i++) {
|
|
1676
|
+
target.fromBufferAttribute(position, i);
|
|
1677
|
+
skinnedMesh.applyBoneTransform(i, target);
|
|
1678
|
+
points.push(target.clone());
|
|
1723
1679
|
}
|
|
1680
|
+
this.fitVerticalFovToPoints(points);
|
|
1724
1681
|
}
|
|
1725
1682
|
/**
|
|
1726
|
-
*
|
|
1683
|
+
* Points camera to look at skinned mesh center of mass.
|
|
1684
|
+
* Uses iterative clustering to find main vertex concentration.
|
|
1685
|
+
*
|
|
1686
|
+
* @param skinnedMesh - Skinned mesh with active skeleton
|
|
1727
1687
|
*/
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
for (
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1688
|
+
lookAtMeshCenterOfMass(skinnedMesh) {
|
|
1689
|
+
skinnedMesh.updateWorldMatrix(true, true);
|
|
1690
|
+
skinnedMesh.skeleton.update();
|
|
1691
|
+
const bakedGeometry = skinnedMesh.geometry;
|
|
1692
|
+
const position = bakedGeometry.attributes.position;
|
|
1693
|
+
const target = new Vector3();
|
|
1694
|
+
const points = [];
|
|
1695
|
+
for (let i = 0; i < position.count; i++) {
|
|
1696
|
+
target.fromBufferAttribute(position, i);
|
|
1697
|
+
skinnedMesh.applyBoneTransform(i, target);
|
|
1698
|
+
points.push(target.clone());
|
|
1739
1699
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1700
|
+
/**
|
|
1701
|
+
* Finds main cluster center using iterative refinement.
|
|
1702
|
+
*
|
|
1703
|
+
* @param points - Array of 3D points to cluster
|
|
1704
|
+
* @param iterations - Number of refinement iterations
|
|
1705
|
+
* @returns Center point of main cluster
|
|
1706
|
+
*/
|
|
1707
|
+
const findMainCluster = (points, iterations = 3) => {
|
|
1708
|
+
if (points.length === 0) {
|
|
1709
|
+
return new Vector3();
|
|
1710
|
+
}
|
|
1711
|
+
let center = points[Math.floor(points.length / 2)].clone();
|
|
1712
|
+
for (let i = 0; i < iterations; i++) {
|
|
1713
|
+
let total = new Vector3();
|
|
1714
|
+
let count = 0;
|
|
1715
|
+
for (const point of points) {
|
|
1716
|
+
if (point.distanceTo(center) < point.distanceTo(total) ||
|
|
1717
|
+
count === 0) {
|
|
1718
|
+
total.add(point);
|
|
1719
|
+
count++;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (count > 0) {
|
|
1723
|
+
center = total.divideScalar(count);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return center;
|
|
1744
1727
|
};
|
|
1728
|
+
const centerOfMass = findMainCluster(points);
|
|
1729
|
+
this.lookAt(centerOfMass);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Creates a copy of this camera with identical settings.
|
|
1733
|
+
*
|
|
1734
|
+
* @returns New DualFovCamera instance
|
|
1735
|
+
* @override
|
|
1736
|
+
*/
|
|
1737
|
+
clone() {
|
|
1738
|
+
const camera = new DualFovCamera(this._private_horizontalFovInternal, this._private_verticalFovInternal, this.aspect, this.near, this.far);
|
|
1739
|
+
camera.copy(this, true);
|
|
1740
|
+
return camera;
|
|
1745
1741
|
}
|
|
1746
1742
|
}
|
|
1747
1743
|
|
|
1748
|
-
/** Number of color channels in RGBA format */
|
|
1749
|
-
const RGBA_CHANNEL_COUNT = 4;
|
|
1750
|
-
/** Number of color channels in RGB format */
|
|
1751
|
-
const RGB_CHANNEL_COUNT = 3;
|
|
1752
|
-
/** Red channel weight for luminance calculation (ITU-R BT.709) */
|
|
1753
|
-
const LUMINANCE_R = 0.2126;
|
|
1754
|
-
/** Green channel weight for luminance calculation (ITU-R BT.709) */
|
|
1755
|
-
const LUMINANCE_G = 0.7152;
|
|
1756
|
-
/** Blue channel weight for luminance calculation (ITU-R BT.709) */
|
|
1757
|
-
const LUMINANCE_B = 0.0722;
|
|
1758
1744
|
/**
|
|
1759
|
-
*
|
|
1745
|
+
* Static methods for traversing Three.js scene hierarchies.
|
|
1746
|
+
*
|
|
1747
|
+
* All methods use depth-first traversal.
|
|
1760
1748
|
*/
|
|
1761
|
-
class
|
|
1762
|
-
constructor() {
|
|
1763
|
-
super(...arguments);
|
|
1764
|
-
/** Internal vectors to avoid garbage collection during calculations */
|
|
1765
|
-
this._private_tempVector3D0 = new Vector3();
|
|
1766
|
-
this._private_tempVector3D1 = new Vector3();
|
|
1767
|
-
this._private_tempVector3D2 = new Vector3();
|
|
1768
|
-
this._private_tempVector3D3 = new Vector3();
|
|
1769
|
-
this._private_tempVector3D4 = new Vector3();
|
|
1770
|
-
this._private_tempVector3D5 = new Vector3();
|
|
1771
|
-
this._private_tempVector3D6 = new Vector3();
|
|
1772
|
-
this._private_tempVector3D7 = new Vector3();
|
|
1773
|
-
this._private_tempBox3 = new Box3();
|
|
1774
|
-
this._private_tempSpherical = new Spherical();
|
|
1775
|
-
}
|
|
1749
|
+
class SceneTraversal {
|
|
1776
1750
|
/**
|
|
1777
|
-
*
|
|
1751
|
+
* Finds first object with exact name match.
|
|
1752
|
+
*
|
|
1753
|
+
* @param object - Root object to start from
|
|
1754
|
+
* @param name - Name to search for (case-sensitive)
|
|
1755
|
+
* @returns First matching object or undefined
|
|
1778
1756
|
*/
|
|
1779
|
-
|
|
1780
|
-
|
|
1757
|
+
static getObjectByName(object, name) {
|
|
1758
|
+
if (object.name === name) {
|
|
1759
|
+
return object;
|
|
1760
|
+
}
|
|
1761
|
+
for (const child of object.children) {
|
|
1762
|
+
const result = SceneTraversal.getObjectByName(child, name);
|
|
1763
|
+
if (result) {
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return undefined;
|
|
1781
1768
|
}
|
|
1782
1769
|
/**
|
|
1783
|
-
*
|
|
1770
|
+
* Finds first material with exact name match from mesh objects.
|
|
1771
|
+
*
|
|
1772
|
+
* @param object - Root object to start from
|
|
1773
|
+
* @param name - Material name to search for (case-sensitive)
|
|
1774
|
+
* @returns First matching material or undefined
|
|
1784
1775
|
*/
|
|
1785
|
-
|
|
1786
|
-
|
|
1776
|
+
static getMaterialByName(object, name) {
|
|
1777
|
+
if (object instanceof Mesh) {
|
|
1778
|
+
if (Array.isArray(object.material)) {
|
|
1779
|
+
for (const material of object.material) {
|
|
1780
|
+
if (material.name === name) {
|
|
1781
|
+
return material;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
else if (object.material.name === name) {
|
|
1786
|
+
return object.material;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
for (const child of object.children) {
|
|
1790
|
+
const material = SceneTraversal.getMaterialByName(child, name);
|
|
1791
|
+
if (material) {
|
|
1792
|
+
return material;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return undefined;
|
|
1787
1796
|
}
|
|
1788
1797
|
/**
|
|
1789
|
-
*
|
|
1798
|
+
* Executes callback for all objects of specified type.
|
|
1799
|
+
*
|
|
1800
|
+
* @template T - Type of objects to process
|
|
1801
|
+
* @param object - Root object to start from
|
|
1802
|
+
* @param type - Constructor to filter by
|
|
1803
|
+
* @param callback - Function to execute for each matching object
|
|
1790
1804
|
*/
|
|
1791
|
-
|
|
1792
|
-
|
|
1805
|
+
static enumerateObjectsByType(object, type, callback) {
|
|
1806
|
+
if (object instanceof type) {
|
|
1807
|
+
callback(object);
|
|
1808
|
+
}
|
|
1809
|
+
for (const child of object.children) {
|
|
1810
|
+
SceneTraversal.enumerateObjectsByType(child, type, callback);
|
|
1811
|
+
}
|
|
1793
1812
|
}
|
|
1794
1813
|
/**
|
|
1795
|
-
*
|
|
1814
|
+
* Executes callback for all materials of specified type from mesh objects.
|
|
1815
|
+
*
|
|
1816
|
+
* @template T - Type of materials to process
|
|
1817
|
+
* @param object - Root object to start from
|
|
1818
|
+
* @param type - Constructor to filter by
|
|
1819
|
+
* @param callback - Function to execute for each matching material
|
|
1796
1820
|
*/
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1821
|
+
static enumerateMaterialsByType(object, type, callback) {
|
|
1822
|
+
if (object instanceof Mesh) {
|
|
1823
|
+
if (Array.isArray(object.material)) {
|
|
1824
|
+
for (const material of object.material) {
|
|
1825
|
+
if (material instanceof type) {
|
|
1826
|
+
callback(material);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
else if (object.material instanceof type) {
|
|
1831
|
+
callback(object.material);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
for (const child of object.children) {
|
|
1835
|
+
SceneTraversal.enumerateMaterialsByType(child, type, callback);
|
|
1836
|
+
}
|
|
1800
1837
|
}
|
|
1801
1838
|
/**
|
|
1802
|
-
*
|
|
1839
|
+
* Executes callback for all objects in hierarchy.
|
|
1840
|
+
*
|
|
1841
|
+
* @param object - Root object to start from
|
|
1842
|
+
* @param callback - Function to execute for each object
|
|
1803
1843
|
*/
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1844
|
+
static enumerateObjects(object, callback) {
|
|
1845
|
+
callback(object);
|
|
1846
|
+
for (const child of object.children) {
|
|
1847
|
+
SceneTraversal.enumerateObjects(child, callback);
|
|
1848
|
+
}
|
|
1807
1849
|
}
|
|
1808
1850
|
/**
|
|
1809
|
-
*
|
|
1851
|
+
* Executes callback for all materials from mesh objects.
|
|
1852
|
+
* If callback returns a material, replaces the original material.
|
|
1853
|
+
*
|
|
1854
|
+
* @param object - Root object to start from
|
|
1855
|
+
* @param callback - Function to execute for each material. Return a material to replace.
|
|
1810
1856
|
*/
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1857
|
+
static enumerateMaterials(object, callback) {
|
|
1858
|
+
var _a, _b;
|
|
1859
|
+
if (object instanceof Mesh) {
|
|
1860
|
+
if (Array.isArray(object.material)) {
|
|
1861
|
+
for (let i = 0; i < object.material.length; i++) {
|
|
1862
|
+
object.material[i] =
|
|
1863
|
+
(_a = callback(object.material[i], object)) !== null && _a !== void 0 ? _a : object.material[i];
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
object.material = (_b = callback(object.material, object)) !== null && _b !== void 0 ? _b : object.material;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
for (const child of object.children) {
|
|
1871
|
+
SceneTraversal.enumerateMaterials(child, callback);
|
|
1872
|
+
}
|
|
1814
1873
|
}
|
|
1815
1874
|
/**
|
|
1816
|
-
*
|
|
1875
|
+
* Returns all objects matching filter criteria.
|
|
1817
1876
|
*
|
|
1818
|
-
* @param
|
|
1819
|
-
* @param
|
|
1820
|
-
* @
|
|
1877
|
+
* @param object - Root object to start from
|
|
1878
|
+
* @param filter - RegExp for object names or predicate function
|
|
1879
|
+
* @returns Array of matching objects
|
|
1821
1880
|
*/
|
|
1822
|
-
|
|
1823
|
-
|
|
1881
|
+
static filterObjects(object, filter) {
|
|
1882
|
+
let result = [];
|
|
1883
|
+
if (typeof filter === "function") {
|
|
1884
|
+
if (filter(object)) {
|
|
1885
|
+
result.push(object);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
else {
|
|
1889
|
+
if (object.name && filter.test(object.name)) {
|
|
1890
|
+
result.push(object);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
for (const child of object.children) {
|
|
1894
|
+
result = result.concat(SceneTraversal.filterObjects(child, filter));
|
|
1895
|
+
}
|
|
1896
|
+
return result;
|
|
1824
1897
|
}
|
|
1825
1898
|
/**
|
|
1826
|
-
*
|
|
1899
|
+
* Returns all materials matching filter criteria from mesh objects.
|
|
1827
1900
|
*
|
|
1828
|
-
* @param
|
|
1901
|
+
* @param object - Root object to start from
|
|
1902
|
+
* @param filter - RegExp for material names or predicate function
|
|
1903
|
+
* @returns Array of matching materials
|
|
1829
1904
|
*/
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1905
|
+
static filterMaterials(object, filter) {
|
|
1906
|
+
let result = [];
|
|
1907
|
+
if (object instanceof Mesh) {
|
|
1908
|
+
if (Array.isArray(object.material)) {
|
|
1909
|
+
for (const material of object.material) {
|
|
1910
|
+
if (typeof filter === "function") {
|
|
1911
|
+
if (filter(material)) {
|
|
1912
|
+
result.push(material);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
else if (filter.test(material.name)) {
|
|
1916
|
+
result.push(material);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
else if (typeof filter === "function") {
|
|
1921
|
+
if (filter(object.material)) {
|
|
1922
|
+
result.push(object.material);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
else if (filter.test(object.material.name)) {
|
|
1926
|
+
result.push(object.material);
|
|
1927
|
+
}
|
|
1848
1928
|
}
|
|
1849
|
-
const
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
camera.right = newBox3.max.x;
|
|
1854
|
-
camera.top = newBox3.max.y;
|
|
1855
|
-
camera.far = -newBox3.min.z;
|
|
1856
|
-
camera.updateWorldMatrix(true, false);
|
|
1857
|
-
camera.updateProjectionMatrix();
|
|
1929
|
+
for (const child of object.children) {
|
|
1930
|
+
result = result.concat(SceneTraversal.filterMaterials(child, filter));
|
|
1931
|
+
}
|
|
1932
|
+
return result;
|
|
1858
1933
|
}
|
|
1859
1934
|
/**
|
|
1860
|
-
*
|
|
1935
|
+
* Returns all mesh objects that use any of the specified materials.
|
|
1861
1936
|
*
|
|
1862
|
-
* @param
|
|
1863
|
-
* @param
|
|
1864
|
-
* @
|
|
1937
|
+
* @param object - Root object to start from
|
|
1938
|
+
* @param materials - Array of materials to search for
|
|
1939
|
+
* @returns Array of mesh objects using the materials
|
|
1865
1940
|
*/
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1941
|
+
static findMaterialUsers(object, materials) {
|
|
1942
|
+
let result = [];
|
|
1943
|
+
if (object instanceof Mesh) {
|
|
1944
|
+
let hasMatchingMaterial = false;
|
|
1945
|
+
if (Array.isArray(object.material)) {
|
|
1946
|
+
for (const material of object.material) {
|
|
1947
|
+
if (materials.includes(material)) {
|
|
1948
|
+
hasMatchingMaterial = true;
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
else {
|
|
1954
|
+
if (materials.includes(object.material)) {
|
|
1955
|
+
hasMatchingMaterial = true;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (hasMatchingMaterial) {
|
|
1959
|
+
result.push(object);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
for (const child of object.children) {
|
|
1963
|
+
result = result.concat(SceneTraversal.findMaterialUsers(child, materials));
|
|
1964
|
+
}
|
|
1965
|
+
return result;
|
|
1870
1966
|
}
|
|
1871
1967
|
/**
|
|
1872
|
-
*
|
|
1968
|
+
* Clones material by name and replaces all instances with the clone.
|
|
1873
1969
|
*
|
|
1874
|
-
* @param
|
|
1970
|
+
* @param object - Root object to start from
|
|
1971
|
+
* @param name - Material name to search for (case-sensitive)
|
|
1972
|
+
* @returns Cloned material or undefined if not found
|
|
1875
1973
|
*/
|
|
1876
|
-
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1974
|
+
static cloneMaterialByName(object, name) {
|
|
1975
|
+
const originalMaterial = SceneTraversal.getMaterialByName(object, name);
|
|
1976
|
+
if (!originalMaterial) {
|
|
1977
|
+
return undefined;
|
|
1978
|
+
}
|
|
1979
|
+
const clonedMaterial = originalMaterial.clone();
|
|
1980
|
+
SceneTraversal.replaceMaterial(object, originalMaterial, clonedMaterial);
|
|
1981
|
+
return clonedMaterial;
|
|
1884
1982
|
}
|
|
1885
1983
|
/**
|
|
1886
|
-
*
|
|
1984
|
+
* Replaces all instances of a material with another material.
|
|
1887
1985
|
*
|
|
1888
|
-
* @param
|
|
1889
|
-
* @param
|
|
1986
|
+
* @param object - Root object to start from
|
|
1987
|
+
* @param oldMaterial - Material to replace
|
|
1988
|
+
* @param newMaterial - Material to use as replacement
|
|
1890
1989
|
*/
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1990
|
+
static replaceMaterial(object, oldMaterial, newMaterial) {
|
|
1991
|
+
if (object instanceof Mesh) {
|
|
1992
|
+
if (Array.isArray(object.material)) {
|
|
1993
|
+
object.material = object.material.map((material) => material === oldMaterial ? newMaterial : material);
|
|
1994
|
+
}
|
|
1995
|
+
else if (object.material === oldMaterial) {
|
|
1996
|
+
object.material = newMaterial;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
for (const child of object.children) {
|
|
2000
|
+
SceneTraversal.replaceMaterial(child, oldMaterial, newMaterial);
|
|
2001
|
+
}
|
|
1894
2002
|
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/** Number of components per vertex */
|
|
2006
|
+
const COMPONENT_COUNT = 3;
|
|
2007
|
+
/** Converts skinned meshes to static meshes. */
|
|
2008
|
+
class SkinnedMeshBaker {
|
|
1895
2009
|
/**
|
|
1896
|
-
*
|
|
2010
|
+
* Converts skinned mesh to static mesh in current pose.
|
|
1897
2011
|
*
|
|
1898
|
-
* @param
|
|
1899
|
-
* @
|
|
2012
|
+
* @param skinnedMesh - Mesh to convert
|
|
2013
|
+
* @returns Static mesh with baked positions
|
|
1900
2014
|
*/
|
|
1901
|
-
|
|
1902
|
-
const
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
const
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2015
|
+
static bakePose(skinnedMesh) {
|
|
2016
|
+
const bakedGeometry = skinnedMesh.geometry.clone();
|
|
2017
|
+
const position = bakedGeometry.attributes["position"];
|
|
2018
|
+
const newPositions = new Float32Array(position.count * COMPONENT_COUNT);
|
|
2019
|
+
const target = new Vector3();
|
|
2020
|
+
for (let i = 0; i < position.count; i++) {
|
|
2021
|
+
target.fromBufferAttribute(position, i);
|
|
2022
|
+
skinnedMesh.applyBoneTransform(i, target);
|
|
2023
|
+
newPositions[i * COMPONENT_COUNT + 0] = target.x;
|
|
2024
|
+
newPositions[i * COMPONENT_COUNT + 1] = target.y;
|
|
2025
|
+
newPositions[i * COMPONENT_COUNT + 2] = target.z;
|
|
2026
|
+
}
|
|
2027
|
+
bakedGeometry.setAttribute("position", new BufferAttribute(newPositions, COMPONENT_COUNT));
|
|
2028
|
+
bakedGeometry.computeVertexNormals();
|
|
2029
|
+
bakedGeometry.deleteAttribute("skinIndex");
|
|
2030
|
+
bakedGeometry.deleteAttribute("skinWeight");
|
|
2031
|
+
const mesh = new Mesh(bakedGeometry, skinnedMesh.material);
|
|
2032
|
+
mesh.name = skinnedMesh.name;
|
|
2033
|
+
return mesh;
|
|
1915
2034
|
}
|
|
1916
2035
|
/**
|
|
1917
|
-
*
|
|
2036
|
+
* Bakes animation frame to static mesh.
|
|
1918
2037
|
*
|
|
1919
|
-
* @param
|
|
1920
|
-
* @
|
|
2038
|
+
* @param armature - Root object with bones
|
|
2039
|
+
* @param skinnedMesh - Mesh to convert
|
|
2040
|
+
* @param timeOffset - Time in seconds within animation
|
|
2041
|
+
* @param clip - Animation clip for pose
|
|
2042
|
+
* @returns Static mesh with baked positions
|
|
1921
2043
|
*/
|
|
1922
|
-
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
const b = data[i + 2];
|
|
1931
|
-
const luminance = LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b;
|
|
1932
|
-
if (luminance > maxLuminance) {
|
|
1933
|
-
maxLuminance = luminance;
|
|
1934
|
-
maxIndex = i;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
return { index: maxIndex, luminance: maxLuminance };
|
|
2044
|
+
static bakeAnimationFrame(armature, skinnedMesh, timeOffset, clip) {
|
|
2045
|
+
const mixer = new AnimationMixer(armature);
|
|
2046
|
+
const action = mixer.clipAction(clip);
|
|
2047
|
+
action.play();
|
|
2048
|
+
mixer.setTime(timeOffset);
|
|
2049
|
+
armature.updateWorldMatrix(true, true);
|
|
2050
|
+
skinnedMesh.skeleton.update();
|
|
2051
|
+
return this.bakePose(skinnedMesh);
|
|
1938
2052
|
}
|
|
1939
2053
|
}
|
|
1940
2054
|
|