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.js CHANGED
@@ -1,246 +1,4 @@
1
- import { PerspectiveCamera, MathUtils, Vector3, Object3D, Matrix4, Quaternion, InstancedMesh, MeshPhysicalMaterial, Color, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, MeshToonMaterial, Mesh, BufferAttribute, AnimationMixer, HemisphereLight, RGBAFormat, DirectionalLight, Box3, Spherical } from 'three';
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(pool, entry, index) {
265
- this._private_pool = 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
- setPosition(v) {
274
- this._private_position.copy(v);
275
- this._private_apply();
276
- return this;
33
+ destroy() {
34
+ if (this._private_handler >= 0) {
35
+ this._private_pool["deallocate"](this._private_handler);
36
+ }
277
37
  }
278
- setPosition3f(x, y, z) {
279
- this._private_position.set(x, y, z);
280
- this._private_apply();
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
- setQuaternion(q) {
284
- this._private_quaternion.copy(q);
285
- this._private_apply();
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
- setQuaternion4f(x, y, z, w) {
289
- this._private_quaternion.set(x, y, z, w);
290
- this._private_apply();
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
- setScale(v) {
294
- this._private_scale.copy(v);
295
- this._private_apply();
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
- setScale3f(x, y, z) {
299
- this._private_scale.set(x, y, z);
300
- this._private_apply();
301
- return this;
302
- }
303
- setTransform(m) {
304
- m.decompose(this._private_position, this._private_quaternion, this._private_scale);
305
- this._private_apply();
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
- destroy() {
309
- if (this.index === -1)
310
- return;
311
- this._private_pool.deallocate(this);
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
- _private_apply() {
314
- if (this.index === -1)
315
- return;
316
- this._private_matrix.compose(this._private_position, this._private_quaternion, this._private_scale);
317
- this["entry"].mesh.setMatrixAt(this.index, this._private_matrix);
318
- this._private_pool["notifyUpdate"](this["entry"]);
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 TEMP_MATRIX = new Matrix4();
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 = options.scene;
327
- this._private_initialCapacity = (_a = options.initialCapacity) !== null && _a !== void 0 ? _a : 16;
328
- this._private_capacityStep = (_b = options.capacityStep) !== null && _b !== void 0 ? _b : this._private_initialCapacity;
329
- this._private_meshes = new Map();
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
- return instance;
171
+ this._private_descriptors.set(handler, { entry, index });
172
+ return handler;
343
173
  }
344
- deallocate(instance) {
345
- if (instance.index === -1)
174
+ deallocate(handler) {
175
+ const descriptor = this._private_descriptors.get(handler);
176
+ if (descriptor === undefined) {
346
177
  return;
347
- const entry = instance["entry"];
348
- const removedIndex = instance.index;
178
+ }
179
+ const { entry, index: removedIndex } = descriptor;
349
180
  const lastIndex = entry.count - 1;
350
- entry.instances.delete(removedIndex);
181
+ this._private_descriptors.delete(handler);
351
182
  if (removedIndex !== lastIndex) {
352
- // swap: move last to removed position
353
- const lastInstance = entry.instances.get(lastIndex);
354
- entry.mesh.getMatrixAt(lastIndex, TEMP_MATRIX);
355
- entry.mesh.setMatrixAt(removedIndex, TEMP_MATRIX);
356
- lastInstance.index = removedIndex;
357
- entry.instances.delete(lastIndex);
358
- entry.instances.set(removedIndex, lastInstance);
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
- instance.index = -1;
363
- this["notifyUpdate"](entry);
194
+ entry.mesh.instanceMatrix.needsUpdate = true;
364
195
  }
365
- sortMeshes(compare) {
366
- const meshes = [];
367
- for (const entry of this._private_meshes.values()) {
368
- meshes.push(entry.mesh);
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
- for (const mesh of meshes) {
375
- this._private_scene.add(mesh);
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._private_meshes.get(key);
381
- if (!entry) {
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
- instances: new Map(),
227
+ handlers: [],
393
228
  };
394
- this._private_meshes.set(key, entry);
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
- ["notifyUpdate"](entry) {
414
- entry.mesh.instanceMatrix.needsUpdate = true;
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
- physicalMaterial.needsUpdate = true;
431
- return physicalMaterial;
432
- }
433
- static copyBasicProperties(source, target, config) {
434
- if (config.preserveName) {
435
- target.name = source.name;
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
- target.side = source.side;
438
- target.visible = source.visible;
439
- target.fog = source.fog;
440
- target.wireframe = source.wireframe;
441
- target.wireframeLinewidth = source.wireframeLinewidth;
442
- target.wireframeLinecap = source.wireframeLinecap;
443
- target.wireframeLinejoin = source.wireframeLinejoin;
444
- target.vertexColors = source.vertexColors;
445
- if (config.copyUserData && source.userData) {
446
- target.userData = Object.assign({}, source.userData);
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
- static convertColorAndPBRProperties(source, target, config) {
450
- if (source.color) {
451
- target.color = source.color.clone();
452
- }
453
- if (config.emissive !== undefined) {
454
- if (config.emissive instanceof Color) {
455
- target.emissive.copy(config.emissive);
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
- else {
458
- target.emissive.set(config.emissive);
337
+ // Lower hemisphere (ground)
338
+ else if (v > GROUND_SAMPLE_THRESHOLD) {
339
+ this._private_insertSorted(groundPixels, pixel, groundSampleCount, false);
459
340
  }
460
341
  }
461
- if (config.emissiveIntensity !== undefined) {
462
- target.emissiveIntensity = config.emissiveIntensity;
463
- }
464
- if (config.metalness !== undefined) {
465
- target.metalness = config.metalness;
466
- }
467
- if (config.roughness !== undefined) {
468
- target.roughness = config.roughness;
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
- static convertTextureMaps(source, target) {
472
- if (source.map) {
473
- target.map = source.map;
474
- }
475
- if (source.alphaMap) {
476
- target.alphaMap = source.alphaMap;
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
- if (source.envMap) {
487
- target.envMap = source.envMap;
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
- static convertTransparencyProperties(source, target) {
491
- target.transparent = source.transparent;
492
- target.opacity = source.opacity;
493
- target.alphaTest = source.alphaTest;
494
- target.depthTest = source.depthTest;
495
- target.depthWrite = source.depthWrite;
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
- if (config.ior !== undefined) {
518
- target.ior = config.ior;
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
- /** Factor for metalness brightness adjustment */
524
- const METALNESS_BRIGHTNESS_FACTOR = 0.3;
525
- /** Factor for emissive color contribution when combining with base color */
526
- const EMISSIVE_CONTRIBUTION_FACTOR = 0.5;
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
- * Converts MeshStandardMaterial to MeshBasicMaterial with brightness compensation.
416
+ * Directional light with spherical positioning and HDR environment support.
529
417
  */
530
- class StandardToBasicConverter {
531
- /**
532
- * Converts MeshStandardMaterial to MeshBasicMaterial.
533
- *
534
- * @param standardMaterial - Source material to convert
535
- * @param options - Conversion options
536
- * @returns New MeshBasicMaterial with mapped properties
537
- */
538
- static convert(standardMaterial, options = {}) {
539
- const config = Object.assign({ preserveName: true, copyUserData: true, disposeOriginal: false, combineEmissive: true, brightnessFactor: 1.3 }, options);
540
- // Create new Basic material
541
- const basicMaterial = new MeshBasicMaterial();
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
- * Static methods for traversing Three.js scene hierarchies.
1324
- *
1325
- * All methods use depth-first traversal.
1517
+ * Camera with independent horizontal and vertical FOV settings.
1326
1518
  */
1327
- class SceneTraversal {
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
- * Returns all objects matching filter criteria.
1454
- *
1455
- * @param object - Root object to start from
1456
- * @param filter - RegExp for object names or predicate function
1457
- * @returns Array of matching objects
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
- static filterObjects(object, filter) {
1460
- let result = [];
1461
- if (typeof filter === "function") {
1462
- if (filter(object)) {
1463
- result.push(object);
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
- * Returns all materials matching filter criteria from mesh objects.
1478
- *
1479
- * @param object - Root object to start from
1480
- * @param filter - RegExp for material names or predicate function
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
- * Returns all mesh objects that use any of the specified materials.
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 object - Root object to start from
1516
- * @param materials - Array of materials to search for
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
- static findMaterialUsers(object, materials) {
1520
- let result = [];
1521
- if (object instanceof Mesh) {
1522
- let hasMatchingMaterial = false;
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
- * Clones material by name and replaces all instances with the clone.
1571
+ * Copies FOV settings from another DualFovCamera.
1547
1572
  *
1548
- * @param object - Root object to start from
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
- static cloneMaterialByName(object, name) {
1553
- const originalMaterial = SceneTraversal.getMaterialByName(object, name);
1554
- if (!originalMaterial) {
1555
- return undefined;
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
- * Replaces all instances of a material with another material.
1581
+ * Updates projection matrix based on FOV and aspect ratio.
1563
1582
  *
1564
- * @param object - Root object to start from
1565
- * @param oldMaterial - Material to replace
1566
- * @param newMaterial - Material to use as replacement
1583
+ * Landscape (aspect > 1): preserves horizontal FOV.
1584
+ * Portrait (aspect 1): preserves vertical FOV.
1585
+ *
1586
+ * @override
1567
1587
  */
1568
- static replaceMaterial(object, oldMaterial, newMaterial) {
1569
- if (object instanceof Mesh) {
1570
- if (Array.isArray(object.material)) {
1571
- object.material = object.material.map((material) => material === oldMaterial ? newMaterial : material);
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
- for (const child of object.children) {
1578
- SceneTraversal.replaceMaterial(child, oldMaterial, newMaterial);
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
- * Converts skinned mesh to static mesh in current pose.
1601
+ * Gets actual horizontal FOV after aspect ratio adjustments.
1589
1602
  *
1590
- * @param skinnedMesh - Mesh to convert
1591
- * @returns Static mesh with baked positions
1603
+ * @returns Horizontal FOV in degrees
1592
1604
  */
1593
- static bakePose(skinnedMesh) {
1594
- const bakedGeometry = skinnedMesh.geometry.clone();
1595
- const position = bakedGeometry.attributes["position"];
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
- bakedGeometry.setAttribute("position", new BufferAttribute(newPositions, COMPONENT_COUNT));
1606
- bakedGeometry.computeVertexNormals();
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
- * Bakes animation frame to static mesh.
1613
+ * Gets actual vertical FOV after aspect ratio adjustments.
1615
1614
  *
1616
- * @param armature - Root object with bones
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
- static bakeAnimationFrame(armature, skinnedMesh, timeOffset, clip) {
1623
- const mixer = new AnimationMixer(armature);
1624
- const action = mixer.clipAction(clip);
1625
- action.play();
1626
- mixer.setTime(timeOffset);
1627
- armature.updateWorldMatrix(true, true);
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
- * Sets sky and ground colors from an HDR texture.
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 texture - HDR texture to analyze (must have image data)
1656
- * @param options - Configuration options for color extraction
1627
+ * @param vertices - Array of 3D points in world coordinates
1657
1628
  */
1658
- setColorsFromHDRTexture(texture, options = {}) {
1659
- const { skySampleCount = 100, groundSampleCount = 100, applyGamma = true, gamma = 2.2, } = options;
1660
- const data = texture.image.data;
1661
- const width = texture.image.width;
1662
- const height = texture.image.height;
1663
- const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT$1 : RGB_CHANNEL_COUNT$1;
1664
- const skyPixels = [];
1665
- const groundPixels = [];
1666
- // Sample pixels from upper and lower hemispheres
1667
- for (let i = 0; i < data.length; i += step) {
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
- // Calculate average sky color from brightest samples
1686
- const skyColor = this._private_averagePixels(skyPixels);
1687
- // Calculate average ground color
1688
- const groundColor = this._private_averagePixels(groundPixels);
1689
- // Apply gamma correction if needed
1690
- if (applyGamma) {
1691
- const invGamma = 1 / gamma;
1692
- skyColor.r = Math.pow(skyColor.r, invGamma);
1693
- skyColor.g = Math.pow(skyColor.g, invGamma);
1694
- skyColor.b = Math.pow(skyColor.b, invGamma);
1695
- groundColor.r = Math.pow(groundColor.r, invGamma);
1696
- groundColor.g = Math.pow(groundColor.g, invGamma);
1697
- groundColor.b = Math.pow(groundColor.b, invGamma);
1698
- }
1699
- // Normalize HDR values to [0, 1] range
1700
- const maxSky = Math.max(skyColor.r, skyColor.g, skyColor.b, 1);
1701
- const maxGround = Math.max(groundColor.r, groundColor.g, groundColor.b, 1);
1702
- this.color.setRGB(skyColor.r / maxSky, skyColor.g / maxSky, skyColor.b / maxSky);
1703
- this.groundColor.setRGB(groundColor.r / maxGround, groundColor.g / maxGround, groundColor.b / maxGround);
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
- * Inserts pixel into sorted array, maintaining size limit.
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
- _private_insertSorted(array, pixel, maxSize, sortDescending) {
1709
- if (array.length < maxSize) {
1710
- array.push(pixel);
1711
- array.sort((a, b) => (sortDescending ? b.lum - a.lum : a.lum - b.lum));
1712
- }
1713
- else {
1714
- const threshold = array[array.length - 1].lum;
1715
- const shouldInsert = sortDescending
1716
- ? pixel.lum > threshold
1717
- : pixel.lum < threshold;
1718
- if (shouldInsert) {
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
- * Calculates average color from pixel array.
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
- _private_averagePixels(pixels) {
1729
- if (pixels.length === 0) {
1730
- return { r: 0.5, g: 0.5, b: 0.5 };
1731
- }
1732
- let totalR = 0;
1733
- let totalG = 0;
1734
- let totalB = 0;
1735
- for (const pixel of pixels) {
1736
- totalR += pixel.r;
1737
- totalG += pixel.g;
1738
- totalB += pixel.b;
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
- return {
1741
- r: totalR / pixels.length,
1742
- g: totalG / pixels.length,
1743
- b: totalB / pixels.length,
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
- * Directional light with spherical positioning and HDR environment support.
1745
+ * Static methods for traversing Three.js scene hierarchies.
1746
+ *
1747
+ * All methods use depth-first traversal.
1760
1748
  */
1761
- class Sun extends DirectionalLight {
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
- * @returns Distance from light position to origin
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
- get distance() {
1780
- return this.position.length();
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
- * @returns Elevation angle in radians (phi angle)
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
- get elevation() {
1786
- return this._private_tempSpherical.setFromVector3(this.position).phi;
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
- * @returns Azimuth angle in radians (theta angle)
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
- get azimuth() {
1792
- return this._private_tempSpherical.setFromVector3(this.position).theta;
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
- * @param value - New distance in world units
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
- set distance(value) {
1798
- this._private_tempSpherical.setFromVector3(this.position);
1799
- this.position.setFromSphericalCoords(value, this._private_tempSpherical.phi, this._private_tempSpherical.theta);
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
- * @param value - New elevation angle in radians (phi angle)
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
- set elevation(value) {
1805
- this._private_tempSpherical.setFromVector3(this.position);
1806
- this.position.setFromSphericalCoords(this._private_tempSpherical.radius, value, this._private_tempSpherical.theta);
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
- * @param value - New azimuth angle in radians (theta angle)
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
- set azimuth(value) {
1812
- this._private_tempSpherical.setFromVector3(this.position);
1813
- this.position.setFromSphericalCoords(this._private_tempSpherical.radius, this._private_tempSpherical.phi, value);
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
- * Sets spherical position of the sun.
1875
+ * Returns all objects matching filter criteria.
1817
1876
  *
1818
- * @param elevation - Elevation angle in radians (phi angle)
1819
- * @param azimuth - Azimuth angle in radians (theta angle)
1820
- * @param distance - Distance from origin in world units
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
- setPosition(elevation, azimuth, distance = 1) {
1823
- this.position.setFromSphericalCoords(distance, elevation, azimuth);
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
- * Configures shadow camera frustum to cover bounding box.
1899
+ * Returns all materials matching filter criteria from mesh objects.
1827
1900
  *
1828
- * @param box3 - 3D bounding box to cover with shadows
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
- configureShadowsForBoundingBox(box3) {
1831
- const camera = this.shadow.camera;
1832
- this.target.updateWorldMatrix(true, false);
1833
- this.lookAt(this.target.getWorldPosition(this._private_tempVector3D0));
1834
- this.updateWorldMatrix(true, false);
1835
- const points = [
1836
- this._private_tempVector3D0.set(box3.min.x, box3.min.y, box3.min.z),
1837
- this._private_tempVector3D1.set(box3.min.x, box3.min.y, box3.max.z),
1838
- this._private_tempVector3D2.set(box3.min.x, box3.max.y, box3.min.z),
1839
- this._private_tempVector3D3.set(box3.min.x, box3.max.y, box3.max.z),
1840
- this._private_tempVector3D4.set(box3.max.x, box3.min.y, box3.min.z),
1841
- this._private_tempVector3D5.set(box3.max.x, box3.min.y, box3.max.z),
1842
- this._private_tempVector3D6.set(box3.max.x, box3.max.y, box3.min.z),
1843
- this._private_tempVector3D7.set(box3.max.x, box3.max.y, box3.max.z),
1844
- ];
1845
- const inverseMatrix = this.matrixWorld.clone().invert();
1846
- for (const point of points) {
1847
- point.applyMatrix4(inverseMatrix);
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 newBox3 = this._private_tempBox3.setFromPoints(points);
1850
- camera.left = newBox3.min.x;
1851
- camera.bottom = newBox3.min.y;
1852
- camera.near = -newBox3.max.z;
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
- * Sets sun direction, color and intensity based on brightest point in HDR environment map.
1935
+ * Returns all mesh objects that use any of the specified materials.
1861
1936
  *
1862
- * @param texture - HDR texture to analyze (must have image data)
1863
- * @param distance - Distance to place sun from origin
1864
- * @param intensityScale - Multiplier for intensity derived from luminance (default: 1)
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
- setFromHDRTexture(texture, intensityScale = 1, distance = 1) {
1867
- this.setColorFromHDRTexture(texture);
1868
- this.setIntensityFromHDRTexture(texture, intensityScale);
1869
- this.setDirectionFromHDRTexture(texture, distance);
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
- * Sets sun color based on brightest point in HDR environment map.
1968
+ * Clones material by name and replaces all instances with the clone.
1873
1969
  *
1874
- * @param texture - HDR texture to analyze (must have image data)
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
- setColorFromHDRTexture(texture) {
1877
- const { index } = this._private_findBrightestPixel(texture);
1878
- const data = texture.image.data;
1879
- const r = data[index];
1880
- const g = data[index + 1];
1881
- const b = data[index + 2];
1882
- const maxChannel = Math.max(r, g, b, 1);
1883
- this.color.setRGB(r / maxChannel, g / maxChannel, b / maxChannel);
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
- * Sets sun intensity based on luminance of brightest point in HDR environment map.
1984
+ * Replaces all instances of a material with another material.
1887
1985
  *
1888
- * @param texture - HDR texture to analyze (must have image data)
1889
- * @param scale - Multiplier for intensity (default: 1)
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
- setIntensityFromHDRTexture(texture, scale = 1) {
1892
- const { luminance } = this._private_findBrightestPixel(texture);
1893
- this.intensity = luminance * scale;
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
- * Sets sun direction based on brightest point in HDR environment map.
2010
+ * Converts skinned mesh to static mesh in current pose.
1897
2011
  *
1898
- * @param texture - HDR texture to analyze (must have image data)
1899
- * @param distance - Distance to place sun from origin
2012
+ * @param skinnedMesh - Mesh to convert
2013
+ * @returns Static mesh with baked positions
1900
2014
  */
1901
- setDirectionFromHDRTexture(texture, distance = 1) {
1902
- const { index } = this._private_findBrightestPixel(texture);
1903
- const width = texture.image.width;
1904
- const height = texture.image.height;
1905
- const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
1906
- // Convert to spherical coordinates
1907
- const pixelIndex = index / step;
1908
- const x = pixelIndex % width;
1909
- const y = Math.floor(pixelIndex / width);
1910
- const u = x / width;
1911
- const v = y / height;
1912
- const elevation = v * Math.PI;
1913
- const azimuth = u * -Math.PI * 2 - Math.PI / 2;
1914
- this.position.setFromSphericalCoords(distance, elevation, azimuth);
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
- * Finds the brightest pixel in an HDR texture.
2036
+ * Bakes animation frame to static mesh.
1918
2037
  *
1919
- * @param texture - HDR texture to analyze
1920
- * @returns Index and luminance of brightest pixel
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
- _private_findBrightestPixel(texture) {
1923
- const data = texture.image.data;
1924
- let maxLuminance = 0;
1925
- let maxIndex = 0;
1926
- const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
1927
- for (let i = 0; i < data.length; i += step) {
1928
- const r = data[i];
1929
- const g = data[i + 1];
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