three-zoo 0.4.1 → 0.4.3
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/README.md +110 -0
- package/dist/BiFovCamera.d.ts +49 -0
- package/dist/Bounds.d.ts +15 -1
- package/dist/GeometryHasher.d.ts +17 -0
- package/dist/InstanceAssembler.d.ts +23 -7
- package/dist/SceneProcessor.d.ts +25 -10
- package/dist/SceneTraversal.d.ts +20 -0
- package/dist/SkinnedMeshBaker.d.ts +13 -14
- package/dist/Sun.d.ts +22 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +285 -240
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +2 -0
- package/dist/index.min.js.map +1 -0
- package/package.json +12 -4
- package/dist/DoubleFOVCamera.d.ts +0 -7
- package/dist/Enumerator.d.ts +0 -12
- package/dist/GeometryComparator.d.ts +0 -27
package/dist/index.js
CHANGED
|
@@ -1,60 +1,217 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PerspectiveCamera, MathUtils, Box3, Vector3, Mesh, InstancedMesh, FrontSide, BufferAttribute, AnimationMixer, DirectionalLight, Spherical, RGBAFormat } from 'three';
|
|
2
2
|
|
|
3
|
+
const DEFAULT_HORIZONTAL_FOV = 90;
|
|
4
|
+
const DEFAULT_VERTICAL_FOV = 90;
|
|
5
|
+
const DEFAULT_ASPECT = 1;
|
|
6
|
+
const DEFAULT_NEAR = 1;
|
|
7
|
+
const DEFAULT_FAR = 1000;
|
|
8
|
+
const MIN_FOV = 1;
|
|
9
|
+
const MAX_FOV = 179;
|
|
10
|
+
/**
|
|
11
|
+
* A camera that supports independent horizontal and vertical FOV settings.
|
|
12
|
+
* Extends Three.js PerspectiveCamera to allow separate control over horizontal
|
|
13
|
+
* and vertical fields of view.
|
|
14
|
+
*/
|
|
15
|
+
class BiFovCamera extends PerspectiveCamera {
|
|
16
|
+
/**
|
|
17
|
+
* @param horizontalFov - Horizontal FOV in degrees (90° default)
|
|
18
|
+
* @param verticalFov - Vertical FOV in degrees (90° default)
|
|
19
|
+
* @param aspect - Width/height ratio (1 default)
|
|
20
|
+
* @param near - Near clipping plane (1 default)
|
|
21
|
+
* @param far - Far clipping plane (1000 default)
|
|
22
|
+
*/
|
|
23
|
+
constructor(horizontalFov = DEFAULT_HORIZONTAL_FOV, verticalFov = DEFAULT_VERTICAL_FOV, aspect = DEFAULT_ASPECT, near = DEFAULT_NEAR, far = DEFAULT_FAR) {
|
|
24
|
+
super(verticalFov, aspect, near, far);
|
|
25
|
+
this.horizontalFovInternal = horizontalFov;
|
|
26
|
+
this.verticalFovInternal = verticalFov;
|
|
27
|
+
this.updateProjectionMatrix();
|
|
28
|
+
}
|
|
29
|
+
/** Current horizontal FOV in degrees */
|
|
30
|
+
get horizontalFov() {
|
|
31
|
+
return this.horizontalFovInternal;
|
|
32
|
+
}
|
|
33
|
+
/** Current vertical FOV in degrees */
|
|
34
|
+
get verticalFov() {
|
|
35
|
+
return this.verticalFovInternal;
|
|
36
|
+
}
|
|
37
|
+
/** Set horizontal FOV in degrees (clamped between 1° and 179°) */
|
|
38
|
+
set horizontalFov(value) {
|
|
39
|
+
this.horizontalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
40
|
+
this.updateProjectionMatrix();
|
|
41
|
+
}
|
|
42
|
+
/** Set vertical FOV in degrees (clamped between 1° and 179°) */
|
|
43
|
+
set verticalFov(value) {
|
|
44
|
+
this.verticalFovInternal = MathUtils.clamp(value, MIN_FOV, MAX_FOV);
|
|
45
|
+
this.updateProjectionMatrix();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Update both horizontal and vertical FOV
|
|
49
|
+
* @param horizontal - Horizontal FOV in degrees
|
|
50
|
+
* @param vertical - Vertical FOV in degrees
|
|
51
|
+
*/
|
|
52
|
+
setFov(horizontal, vertical) {
|
|
53
|
+
this.horizontalFovInternal = MathUtils.clamp(horizontal, MIN_FOV, MAX_FOV);
|
|
54
|
+
this.verticalFovInternal = MathUtils.clamp(vertical, MIN_FOV, MAX_FOV);
|
|
55
|
+
this.updateProjectionMatrix();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Copy FOV settings from another BiFovCamera
|
|
59
|
+
* @param source - Camera to copy from
|
|
60
|
+
*/
|
|
61
|
+
copyFovSettings(source) {
|
|
62
|
+
this.horizontalFovInternal = source.horizontalFov;
|
|
63
|
+
this.verticalFovInternal = source.verticalFov;
|
|
64
|
+
this.updateProjectionMatrix();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Updates the projection matrix based on FOV settings and aspect ratio.
|
|
68
|
+
* In landscape: preserves horizontal FOV
|
|
69
|
+
* In portrait: preserves vertical FOV
|
|
70
|
+
*/
|
|
71
|
+
updateProjectionMatrix() {
|
|
72
|
+
if (this.aspect >= 1) {
|
|
73
|
+
// Landscape orientation: preserve horizontal FOV
|
|
74
|
+
const radians = MathUtils.degToRad(this.horizontalFovInternal);
|
|
75
|
+
this.fov = MathUtils.radToDeg(Math.atan(Math.tan(radians / 2) / this.aspect) * 2);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Portrait orientation: preserve vertical FOV
|
|
79
|
+
this.fov = this.verticalFovInternal;
|
|
80
|
+
}
|
|
81
|
+
super.updateProjectionMatrix();
|
|
82
|
+
}
|
|
83
|
+
/** Get actual horizontal FOV after aspect ratio adjustments */
|
|
84
|
+
getEffectiveHorizontalFov() {
|
|
85
|
+
if (this.aspect >= 1) {
|
|
86
|
+
return this.horizontalFovInternal;
|
|
87
|
+
}
|
|
88
|
+
const verticalRadians = MathUtils.degToRad(this.verticalFovInternal);
|
|
89
|
+
return MathUtils.radToDeg(Math.atan(Math.tan(verticalRadians / 2) * this.aspect) * 2);
|
|
90
|
+
}
|
|
91
|
+
/** Get actual vertical FOV after aspect ratio adjustments */
|
|
92
|
+
getEffectiveVerticalFov() {
|
|
93
|
+
if (this.aspect < 1) {
|
|
94
|
+
return this.verticalFovInternal;
|
|
95
|
+
}
|
|
96
|
+
const horizontalRadians = MathUtils.degToRad(this.horizontalFovInternal);
|
|
97
|
+
return MathUtils.radToDeg(Math.atan(Math.tan(horizontalRadians / 2) / this.aspect) * 2);
|
|
98
|
+
}
|
|
99
|
+
/** Create a clone of this camera */
|
|
100
|
+
clone() {
|
|
101
|
+
const camera = new BiFovCamera(this.horizontalFovInternal, this.verticalFovInternal, this.aspect, this.near, this.far);
|
|
102
|
+
camera.copy(this, true);
|
|
103
|
+
return camera;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Box3 with additional convenience methods for width, height, depth, etc.
|
|
109
|
+
*/
|
|
3
110
|
class Bounds extends Box3 {
|
|
4
|
-
constructor() {
|
|
5
|
-
super(
|
|
6
|
-
|
|
111
|
+
constructor(object) {
|
|
112
|
+
super();
|
|
113
|
+
/** Temporary vector for calculations */
|
|
114
|
+
this.tempVector3A = new Vector3();
|
|
115
|
+
if (object) {
|
|
116
|
+
this.setFromObject(object);
|
|
117
|
+
}
|
|
7
118
|
}
|
|
119
|
+
/** Width (x-axis length) */
|
|
8
120
|
get width() {
|
|
9
121
|
return this.max.x - this.min.x;
|
|
10
122
|
}
|
|
123
|
+
/** Height (y-axis length) */
|
|
11
124
|
get height() {
|
|
12
125
|
return this.max.y - this.min.y;
|
|
13
126
|
}
|
|
127
|
+
/** Depth (z-axis length) */
|
|
14
128
|
get depth() {
|
|
15
129
|
return this.max.z - this.min.z;
|
|
16
130
|
}
|
|
131
|
+
/** Length of the box's diagonal */
|
|
17
132
|
get diagonal() {
|
|
18
|
-
this.
|
|
19
|
-
|
|
133
|
+
return this.tempVector3A.subVectors(this.max, this.min).length();
|
|
134
|
+
}
|
|
135
|
+
/** Volume (width * height * depth) */
|
|
136
|
+
getVolume() {
|
|
137
|
+
return this.width * this.height * this.depth;
|
|
138
|
+
}
|
|
139
|
+
/** Surface area (sum of all six faces) */
|
|
140
|
+
getSurfaceArea() {
|
|
141
|
+
const w = this.width;
|
|
142
|
+
const h = this.height;
|
|
143
|
+
const d = this.depth;
|
|
144
|
+
return 2 * (w * h + h * d + d * w);
|
|
20
145
|
}
|
|
21
146
|
}
|
|
22
147
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
148
|
+
const POSITION_COMPONENT_COUNT = 3;
|
|
149
|
+
const NORMAL_COMPONENT_COUNT = 3;
|
|
150
|
+
/**
|
|
151
|
+
* Internal utility to identify identical geometries.
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
class GeometryHasher {
|
|
155
|
+
/**
|
|
156
|
+
* Creates a hash for a geometry based on its vertex data.
|
|
157
|
+
* Vertices that differ by less than tolerance are considered the same.
|
|
158
|
+
*
|
|
159
|
+
* @param geometry - Geometry to hash
|
|
160
|
+
* @param tolerance - How close vertices need to be to count as identical
|
|
161
|
+
* @returns Hash string that's the same for matching geometries
|
|
162
|
+
* @internal
|
|
163
|
+
*/
|
|
164
|
+
static getGeometryHash(geometry, tolerance) {
|
|
165
|
+
const position = geometry.attributes["position"];
|
|
166
|
+
const positionArray = position.array;
|
|
167
|
+
const positionHashParts = [];
|
|
168
|
+
// Sample vertex positions with tolerance
|
|
169
|
+
for (let i = 0; i < positionArray.length; i += POSITION_COMPONENT_COUNT) {
|
|
170
|
+
const x = Math.round(positionArray[i] / tolerance);
|
|
171
|
+
const y = Math.round(positionArray[i + 1] / tolerance);
|
|
172
|
+
const z = Math.round(positionArray[i + 2] / tolerance);
|
|
173
|
+
positionHashParts.push(`${x},${y},${z}`);
|
|
33
174
|
}
|
|
34
|
-
|
|
35
|
-
|
|
175
|
+
// Hash normal data if available
|
|
176
|
+
const normal = geometry.attributes["normal"];
|
|
177
|
+
const normalHashParts = [];
|
|
178
|
+
const normalArray = normal.array;
|
|
179
|
+
for (let i = 0; i < normalArray.length; i += NORMAL_COMPONENT_COUNT) {
|
|
180
|
+
const x = Math.round(normalArray[i] / tolerance);
|
|
181
|
+
const y = Math.round(normalArray[i + 1] / tolerance);
|
|
182
|
+
const z = Math.round(normalArray[i + 2] / tolerance);
|
|
183
|
+
normalHashParts.push(`${x},${y},${z}`);
|
|
36
184
|
}
|
|
37
|
-
|
|
185
|
+
// Combine position and normal hashes
|
|
186
|
+
const positionHash = positionHashParts.join("|");
|
|
187
|
+
const normalHash = normalHashParts.join("|");
|
|
188
|
+
return `${positionHash}#${normalHash}`;
|
|
38
189
|
}
|
|
39
190
|
}
|
|
40
191
|
|
|
41
|
-
|
|
192
|
+
/** Find and modify objects in a Three.js scene */
|
|
193
|
+
class SceneTraversal {
|
|
194
|
+
/** Find first object with exact name match */
|
|
42
195
|
static getObjectByName(object, name) {
|
|
43
|
-
if (object.name === name)
|
|
196
|
+
if (object.name === name) {
|
|
44
197
|
return object;
|
|
198
|
+
}
|
|
45
199
|
for (const child of object.children) {
|
|
46
|
-
const result =
|
|
47
|
-
if (result)
|
|
200
|
+
const result = SceneTraversal.getObjectByName(child, name);
|
|
201
|
+
if (result) {
|
|
48
202
|
return result;
|
|
203
|
+
}
|
|
49
204
|
}
|
|
50
205
|
return null;
|
|
51
206
|
}
|
|
207
|
+
/** Find first material with exact name match */
|
|
52
208
|
static getMaterialByName(object, name) {
|
|
53
209
|
if (object instanceof Mesh) {
|
|
54
210
|
if (Array.isArray(object.material)) {
|
|
55
211
|
for (const material of object.material) {
|
|
56
|
-
if (material.name === name)
|
|
212
|
+
if (material.name === name) {
|
|
57
213
|
return material;
|
|
214
|
+
}
|
|
58
215
|
}
|
|
59
216
|
}
|
|
60
217
|
else if (object.material.name === name) {
|
|
@@ -62,20 +219,23 @@ class Enumerator {
|
|
|
62
219
|
}
|
|
63
220
|
}
|
|
64
221
|
for (const child of object.children) {
|
|
65
|
-
const material =
|
|
66
|
-
if (material)
|
|
222
|
+
const material = SceneTraversal.getMaterialByName(child, name);
|
|
223
|
+
if (material) {
|
|
67
224
|
return material;
|
|
225
|
+
}
|
|
68
226
|
}
|
|
69
227
|
return null;
|
|
70
228
|
}
|
|
229
|
+
/** Process all objects of a specific type */
|
|
71
230
|
static enumerateObjectsByType(object, type, callback) {
|
|
72
231
|
if (object instanceof type) {
|
|
73
232
|
callback(object);
|
|
74
233
|
}
|
|
75
234
|
for (const child of object.children) {
|
|
76
|
-
|
|
235
|
+
SceneTraversal.enumerateObjectsByType(child, type, callback);
|
|
77
236
|
}
|
|
78
237
|
}
|
|
238
|
+
/** Process all materials in meshes */
|
|
79
239
|
static enumerateMaterials(object, callback) {
|
|
80
240
|
if (object instanceof Mesh) {
|
|
81
241
|
if (Array.isArray(object.material)) {
|
|
@@ -88,19 +248,21 @@ class Enumerator {
|
|
|
88
248
|
}
|
|
89
249
|
}
|
|
90
250
|
for (const child of object.children) {
|
|
91
|
-
|
|
251
|
+
SceneTraversal.enumerateMaterials(child, callback);
|
|
92
252
|
}
|
|
93
253
|
}
|
|
254
|
+
/** Find all objects whose names match a pattern */
|
|
94
255
|
static filterObjects(object, name) {
|
|
95
256
|
let result = [];
|
|
96
257
|
if (object.name && name.test(object.name)) {
|
|
97
258
|
result.push(object);
|
|
98
259
|
}
|
|
99
260
|
for (const child of object.children) {
|
|
100
|
-
result = result.concat(
|
|
261
|
+
result = result.concat(SceneTraversal.filterObjects(child, name));
|
|
101
262
|
}
|
|
102
263
|
return result;
|
|
103
264
|
}
|
|
265
|
+
/** Find all materials whose names match a pattern */
|
|
104
266
|
static filterMaterials(object, name) {
|
|
105
267
|
let result = [];
|
|
106
268
|
if (object instanceof Mesh) {
|
|
@@ -118,224 +280,79 @@ class Enumerator {
|
|
|
118
280
|
}
|
|
119
281
|
}
|
|
120
282
|
for (const child of object.children) {
|
|
121
|
-
result = result.concat(
|
|
283
|
+
result = result.concat(SceneTraversal.filterMaterials(child, name));
|
|
122
284
|
}
|
|
123
285
|
return result;
|
|
124
286
|
}
|
|
125
|
-
|
|
287
|
+
/** Set shadow properties on meshes */
|
|
288
|
+
static setShadowRecursive(object, castShadow = true, receiveShadow = true, filter) {
|
|
126
289
|
if (object instanceof Mesh || "isMesh" in object) {
|
|
127
290
|
object.castShadow = castShadow;
|
|
128
291
|
object.receiveShadow = receiveShadow;
|
|
129
292
|
}
|
|
130
293
|
for (const child of object.children) {
|
|
131
|
-
|
|
294
|
+
SceneTraversal.setShadowRecursive(child, castShadow, receiveShadow, filter);
|
|
132
295
|
}
|
|
133
296
|
}
|
|
134
297
|
}
|
|
135
298
|
|
|
299
|
+
const MIN_INSTANCE_COUNT = 2;
|
|
300
|
+
const DEFAULT_TOLERANCE = 1e-6;
|
|
136
301
|
/**
|
|
137
|
-
*
|
|
302
|
+
* Combines identical meshes into instanced versions for better performance.
|
|
303
|
+
* Meshes are considered identical if they share the same geometry and materials.
|
|
138
304
|
*/
|
|
139
|
-
class
|
|
305
|
+
class InstanceAssembler {
|
|
140
306
|
/**
|
|
141
|
-
*
|
|
307
|
+
* Find meshes that can be instanced and combine them.
|
|
308
|
+
* Only processes meshes that:
|
|
309
|
+
* - Have no children
|
|
310
|
+
* - Pass the filter function (if any)
|
|
311
|
+
* - Share geometry with at least one other mesh
|
|
142
312
|
*
|
|
143
|
-
* @param
|
|
144
|
-
* @param
|
|
145
|
-
* @returns A string hash that will be identical for geometrically equivalent geometries
|
|
146
|
-
*/
|
|
147
|
-
static getGeometryHash(geometry, tolerance = 1e-6) {
|
|
148
|
-
const hashParts = [];
|
|
149
|
-
// Process attributes
|
|
150
|
-
const attributes = geometry.attributes;
|
|
151
|
-
const attributeNames = Object.keys(attributes).sort(); // Sort for consistent order
|
|
152
|
-
for (const name of attributeNames) {
|
|
153
|
-
const attribute = attributes[name];
|
|
154
|
-
hashParts.push(`${name}:${attribute.itemSize}:${this.getAttributeHash(attribute, tolerance)}`);
|
|
155
|
-
}
|
|
156
|
-
// Process index if present
|
|
157
|
-
if (geometry.index) {
|
|
158
|
-
hashParts.push(`index:${this.getAttributeHash(geometry.index, tolerance)}`);
|
|
159
|
-
}
|
|
160
|
-
return hashParts.join("|");
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Compares two BufferGeometry instances for approximate equality.
|
|
164
|
-
* Early exit if UUIDs match (same object or cloned geometry).
|
|
165
|
-
*/
|
|
166
|
-
static compare(firstGeometry, secondGeometry, tolerance = 1e-6) {
|
|
167
|
-
if (firstGeometry.uuid === secondGeometry.uuid) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
// Use hash comparison for consistent results
|
|
171
|
-
return (this.getGeometryHash(firstGeometry, tolerance) ===
|
|
172
|
-
this.getGeometryHash(secondGeometry, tolerance));
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Generates a hash for a buffer attribute with tolerance.
|
|
313
|
+
* @param container - Object containing meshes to process
|
|
314
|
+
* @param options - Optional settings
|
|
176
315
|
*/
|
|
177
|
-
static
|
|
178
|
-
const array = attribute.array;
|
|
179
|
-
const itemSize = "itemSize" in attribute ? attribute.itemSize : 1;
|
|
180
|
-
const hashParts = [];
|
|
181
|
-
// Group values by their "tolerance buckets"
|
|
182
|
-
for (let i = 0; i < array.length; i += itemSize) {
|
|
183
|
-
const itemValues = [];
|
|
184
|
-
for (let j = 0; j < itemSize; j++) {
|
|
185
|
-
const val = array[i + j];
|
|
186
|
-
// Round to nearest tolerance multiple to group similar values
|
|
187
|
-
itemValues.push(Math.round(val / tolerance) * tolerance);
|
|
188
|
-
}
|
|
189
|
-
hashParts.push(itemValues.join(","));
|
|
190
|
-
}
|
|
191
|
-
return hashParts.join(";");
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Compares two buffer attributes with tolerance.
|
|
195
|
-
*/
|
|
196
|
-
static compareBufferAttributes(firstAttribute, secondAttribute, tolerance) {
|
|
197
|
-
return (this.getAttributeHash(firstAttribute, tolerance) ===
|
|
198
|
-
this.getAttributeHash(secondAttribute, tolerance));
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
// import {
|
|
202
|
-
// BufferAttribute,
|
|
203
|
-
// BufferGeometry,
|
|
204
|
-
// InterleavedBufferAttribute,
|
|
205
|
-
// } from "three";
|
|
206
|
-
// type AnySuitableAttribute = BufferAttribute | InterleavedBufferAttribute;
|
|
207
|
-
// /**
|
|
208
|
-
// * Utility class for comparing two BufferGeometry instances with tolerance support.
|
|
209
|
-
// * Checks geometry attributes (positions, normals, UVs, etc.) and indices (if present).
|
|
210
|
-
// */
|
|
211
|
-
// export class GeometryComparator {
|
|
212
|
-
// /**
|
|
213
|
-
// * Compares two BufferGeometry instances for approximate equality.
|
|
214
|
-
// * Early exit if UUIDs match (same object or cloned geometry).
|
|
215
|
-
// *
|
|
216
|
-
// * @param firstGeometry - The first geometry to compare.
|
|
217
|
-
// * @param secondGeometry - The second geometry to compare.
|
|
218
|
-
// * @param tolerance - Maximum allowed difference between numeric values (default: 1e-6).
|
|
219
|
-
// * @returns `true` if geometries are equivalent within tolerance, otherwise `false`.
|
|
220
|
-
// */
|
|
221
|
-
// public static compare(
|
|
222
|
-
// firstGeometry: BufferGeometry,
|
|
223
|
-
// secondGeometry: BufferGeometry,
|
|
224
|
-
// tolerance = 1e-6,
|
|
225
|
-
// ): boolean {
|
|
226
|
-
// if (firstGeometry.uuid === secondGeometry.uuid) {
|
|
227
|
-
// return true;
|
|
228
|
-
// }
|
|
229
|
-
// const firstAttributes = firstGeometry.attributes;
|
|
230
|
-
// const secondAttributes = secondGeometry.attributes;
|
|
231
|
-
// const firstAttributeNames = Object.keys(firstAttributes);
|
|
232
|
-
// const secondAttributeNames = Object.keys(secondAttributes);
|
|
233
|
-
// if (firstAttributeNames.length !== secondAttributeNames.length) {
|
|
234
|
-
// return false;
|
|
235
|
-
// }
|
|
236
|
-
// for (const attributeName of firstAttributeNames) {
|
|
237
|
-
// if (!secondAttributes[attributeName]) {
|
|
238
|
-
// return false;
|
|
239
|
-
// }
|
|
240
|
-
// const firstAttribute = firstAttributes[
|
|
241
|
-
// attributeName
|
|
242
|
-
// ] as AnySuitableAttribute;
|
|
243
|
-
// const secondAttribute = secondAttributes[
|
|
244
|
-
// attributeName
|
|
245
|
-
// ] as AnySuitableAttribute;
|
|
246
|
-
// if (
|
|
247
|
-
// firstAttribute.count !== secondAttribute.count ||
|
|
248
|
-
// firstAttribute.itemSize !== secondAttribute.itemSize ||
|
|
249
|
-
// !this.compareBufferAttributes(
|
|
250
|
-
// firstAttribute,
|
|
251
|
-
// secondAttribute,
|
|
252
|
-
// tolerance,
|
|
253
|
-
// )
|
|
254
|
-
// ) {
|
|
255
|
-
// return false;
|
|
256
|
-
// }
|
|
257
|
-
// }
|
|
258
|
-
// if (firstGeometry.index || secondGeometry.index) {
|
|
259
|
-
// if (!firstGeometry.index || !secondGeometry.index) {
|
|
260
|
-
// return false;
|
|
261
|
-
// }
|
|
262
|
-
// if (
|
|
263
|
-
// !this.compareBufferAttributes(
|
|
264
|
-
// firstGeometry.index,
|
|
265
|
-
// secondGeometry.index,
|
|
266
|
-
// tolerance,
|
|
267
|
-
// )
|
|
268
|
-
// ) {
|
|
269
|
-
// return false;
|
|
270
|
-
// }
|
|
271
|
-
// }
|
|
272
|
-
// return true;
|
|
273
|
-
// }
|
|
274
|
-
// /**
|
|
275
|
-
// * Compares two buffer attributes (or index buffers) with tolerance.
|
|
276
|
-
// *
|
|
277
|
-
// * @param firstAttribute - First attribute/indices to compare.
|
|
278
|
-
// * @param secondAttribute - Second attribute/indices to compare.
|
|
279
|
-
// * @param tolerance - Maximum allowed difference between array elements.
|
|
280
|
-
// * @returns `true` if arrays are equal within tolerance, otherwise `false`.
|
|
281
|
-
// */
|
|
282
|
-
// private static compareBufferAttributes(
|
|
283
|
-
// firstAttribute: AnySuitableAttribute,
|
|
284
|
-
// secondAttribute: AnySuitableAttribute,
|
|
285
|
-
// tolerance: number,
|
|
286
|
-
// ): boolean {
|
|
287
|
-
// const firstArray = firstAttribute.array;
|
|
288
|
-
// const secondArray = secondAttribute.array;
|
|
289
|
-
// if (firstArray.length !== secondArray.length) {
|
|
290
|
-
// return false;
|
|
291
|
-
// }
|
|
292
|
-
// for (let index = 0; index < firstArray.length; index++) {
|
|
293
|
-
// if (Math.abs(firstArray[index] - secondArray[index]) > tolerance) {
|
|
294
|
-
// return false;
|
|
295
|
-
// }
|
|
296
|
-
// }
|
|
297
|
-
// return true;
|
|
298
|
-
// }
|
|
299
|
-
// }
|
|
300
|
-
|
|
301
|
-
class InstanceAssembler {
|
|
302
|
-
static assemble(options) {
|
|
316
|
+
static assemble(container, options = {}) {
|
|
303
317
|
var _a, _b;
|
|
304
318
|
const dictionary = new Map();
|
|
305
|
-
const
|
|
306
|
-
const tolerance = (_a = options.geometryTolerance) !== null && _a !== void 0 ? _a :
|
|
307
|
-
const
|
|
308
|
-
|
|
319
|
+
const instances = [];
|
|
320
|
+
const tolerance = (_a = options.geometryTolerance) !== null && _a !== void 0 ? _a : DEFAULT_TOLERANCE;
|
|
321
|
+
const geometryHashes = new Map();
|
|
322
|
+
SceneTraversal.enumerateObjectsByType(container, Mesh, (child) => {
|
|
309
323
|
var _a;
|
|
310
324
|
if (child.children.length === 0 &&
|
|
311
325
|
(!options.filter || options.filter(child))) {
|
|
312
326
|
const materials = Array.isArray(child.material)
|
|
313
327
|
? child.material
|
|
314
328
|
: [child.material];
|
|
315
|
-
let geometryHash =
|
|
329
|
+
let geometryHash = geometryHashes.get(child.geometry.uuid);
|
|
316
330
|
if (!geometryHash) {
|
|
317
|
-
geometryHash =
|
|
318
|
-
|
|
331
|
+
geometryHash = GeometryHasher.getGeometryHash(child.geometry, tolerance);
|
|
332
|
+
geometryHashes.set(child.geometry.uuid, geometryHash);
|
|
319
333
|
}
|
|
320
334
|
const materialKey = materials.map((m) => m.uuid).join(",");
|
|
321
335
|
const compositeKey = `${geometryHash}|${materialKey}`;
|
|
322
336
|
const entry = (_a = dictionary.get(compositeKey)) !== null && _a !== void 0 ? _a : {
|
|
323
337
|
meshes: [],
|
|
324
|
-
materials
|
|
338
|
+
materials,
|
|
325
339
|
castShadow: false,
|
|
326
340
|
receiveShadow: false,
|
|
327
341
|
};
|
|
328
|
-
if (child.castShadow)
|
|
342
|
+
if (child.castShadow) {
|
|
329
343
|
entry.castShadow = true;
|
|
330
|
-
|
|
344
|
+
}
|
|
345
|
+
if (child.receiveShadow) {
|
|
331
346
|
entry.receiveShadow = true;
|
|
347
|
+
}
|
|
332
348
|
entry.meshes.push(child);
|
|
333
349
|
dictionary.set(compositeKey, entry);
|
|
334
350
|
}
|
|
335
351
|
});
|
|
336
352
|
for (const descriptor of dictionary.values()) {
|
|
337
|
-
if (descriptor.meshes.length <
|
|
353
|
+
if (descriptor.meshes.length < MIN_INSTANCE_COUNT) {
|
|
338
354
|
continue;
|
|
355
|
+
}
|
|
339
356
|
const { meshes, materials, castShadow, receiveShadow } = descriptor;
|
|
340
357
|
const sortedMeshes = meshes.sort((a, b) => a.name.localeCompare(b.name));
|
|
341
358
|
const defaultMesh = sortedMeshes[0];
|
|
@@ -350,63 +367,74 @@ class InstanceAssembler {
|
|
|
350
367
|
instancedMesh.userData[mesh.uuid] = mesh.userData;
|
|
351
368
|
}
|
|
352
369
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
|
353
|
-
|
|
370
|
+
instances.push(instancedMesh);
|
|
354
371
|
for (const mesh of sortedMeshes) {
|
|
355
372
|
(_b = mesh.parent) === null || _b === void 0 ? void 0 : _b.remove(mesh);
|
|
356
373
|
}
|
|
357
374
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
375
|
+
if (instances.length > 0) {
|
|
376
|
+
container.add(...instances);
|
|
360
377
|
}
|
|
361
378
|
}
|
|
362
379
|
}
|
|
363
380
|
|
|
381
|
+
/** Post-processes a scene based on name patterns */
|
|
364
382
|
class SceneProcessor {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Process a scene to set up materials and shadows.
|
|
385
|
+
*
|
|
386
|
+
* @param asset - Scene to process
|
|
387
|
+
* @param options - How to process the scene
|
|
388
|
+
* @returns Processed scene root objects
|
|
389
|
+
*/
|
|
390
|
+
static process(asset, options) {
|
|
391
|
+
const container = options.cloneAsset !== false ? asset.clone() : asset;
|
|
392
|
+
if (options.assembleInstances !== false) {
|
|
393
|
+
InstanceAssembler.assemble(container);
|
|
394
|
+
}
|
|
395
|
+
SceneTraversal.enumerateMaterials(container, (material) => {
|
|
396
|
+
material.transparent = SceneProcessor.matchesAny(material.name, options.transparentMaterialExpressions);
|
|
397
|
+
material.depthWrite = !SceneProcessor.matchesAny(material.name, options.noDepthWriteMaterialExpressions);
|
|
371
398
|
material.side = FrontSide;
|
|
372
399
|
material.forceSinglePass = true;
|
|
373
400
|
material.depthTest = true;
|
|
374
401
|
});
|
|
375
|
-
|
|
376
|
-
child.castShadow = SceneProcessor.matchesAny(child.name, options.
|
|
377
|
-
child.receiveShadow = SceneProcessor.matchesAny(child.name, options.
|
|
402
|
+
SceneTraversal.enumerateObjectsByType(container, Mesh, (child) => {
|
|
403
|
+
child.castShadow = SceneProcessor.matchesAny(child.name, options.castShadowExpressions);
|
|
404
|
+
child.receiveShadow = SceneProcessor.matchesAny(child.name, options.receiveShadwoExpressions);
|
|
378
405
|
});
|
|
379
406
|
return container.children;
|
|
380
407
|
}
|
|
381
|
-
|
|
382
|
-
|
|
408
|
+
/** Does the string match any of the patterns? */
|
|
409
|
+
static matchesAny(value, expressions = []) {
|
|
410
|
+
return expressions.some((p) => p.test(value));
|
|
383
411
|
}
|
|
384
412
|
}
|
|
385
413
|
|
|
386
|
-
/**
|
|
387
|
-
|
|
388
|
-
*/
|
|
414
|
+
/** Number of components per vertex */
|
|
415
|
+
const COMPONENT_COUNT = 3;
|
|
416
|
+
/** Convert skinned meshes to regular static meshes */
|
|
389
417
|
class SkinnedMeshBaker {
|
|
390
418
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
419
|
+
* Convert a skinned mesh to a regular mesh in its current pose.
|
|
420
|
+
* The resulting mesh will have no bones but look identical.
|
|
393
421
|
*
|
|
394
|
-
* @param skinnedMesh -
|
|
395
|
-
* @returns
|
|
422
|
+
* @param skinnedMesh - Mesh to convert
|
|
423
|
+
* @returns Static mesh with baked vertex positions
|
|
396
424
|
*/
|
|
397
425
|
static bakePose(skinnedMesh) {
|
|
398
426
|
const bakedGeometry = skinnedMesh.geometry.clone();
|
|
399
427
|
const position = bakedGeometry.attributes["position"];
|
|
400
|
-
const newPositions = new Float32Array(position.count *
|
|
428
|
+
const newPositions = new Float32Array(position.count * COMPONENT_COUNT);
|
|
401
429
|
const target = new Vector3();
|
|
402
430
|
for (let i = 0; i < position.count; i++) {
|
|
403
431
|
target.fromBufferAttribute(position, i);
|
|
404
432
|
skinnedMesh.applyBoneTransform(i, target);
|
|
405
|
-
newPositions[i *
|
|
406
|
-
newPositions[i *
|
|
407
|
-
newPositions[i *
|
|
433
|
+
newPositions[i * COMPONENT_COUNT + 0] = target.x;
|
|
434
|
+
newPositions[i * COMPONENT_COUNT + 1] = target.y;
|
|
435
|
+
newPositions[i * COMPONENT_COUNT + 2] = target.z;
|
|
408
436
|
}
|
|
409
|
-
bakedGeometry.setAttribute("position", new BufferAttribute(newPositions,
|
|
437
|
+
bakedGeometry.setAttribute("position", new BufferAttribute(newPositions, COMPONENT_COUNT));
|
|
410
438
|
bakedGeometry.computeVertexNormals();
|
|
411
439
|
bakedGeometry.deleteAttribute("skinIndex");
|
|
412
440
|
bakedGeometry.deleteAttribute("skinWeight");
|
|
@@ -415,13 +443,13 @@ class SkinnedMeshBaker {
|
|
|
415
443
|
return mesh;
|
|
416
444
|
}
|
|
417
445
|
/**
|
|
418
|
-
*
|
|
446
|
+
* Bake a single frame from an animation into a static mesh.
|
|
419
447
|
*
|
|
420
|
-
* @param armature -
|
|
421
|
-
* @param skinnedMesh -
|
|
422
|
-
* @param timeOffset -
|
|
423
|
-
* @param clip -
|
|
424
|
-
* @returns
|
|
448
|
+
* @param armature - Root object with bones (usually from GLTF)
|
|
449
|
+
* @param skinnedMesh - Mesh to convert
|
|
450
|
+
* @param timeOffset - Time in seconds within the animation
|
|
451
|
+
* @param clip - Animation to get the pose from
|
|
452
|
+
* @returns Static mesh with baked vertex positions
|
|
425
453
|
*/
|
|
426
454
|
static bakeAnimationFrame(armature, skinnedMesh, timeOffset, clip) {
|
|
427
455
|
const mixer = new AnimationMixer(armature);
|
|
@@ -434,9 +462,16 @@ class SkinnedMeshBaker {
|
|
|
434
462
|
}
|
|
435
463
|
}
|
|
436
464
|
|
|
465
|
+
const RGBA_CHANNEL_COUNT = 4;
|
|
466
|
+
const RGB_CHANNEL_COUNT = 3;
|
|
467
|
+
const LUMINANCE_R = 0.2126;
|
|
468
|
+
const LUMINANCE_G = 0.7152;
|
|
469
|
+
const LUMINANCE_B = 0.0722;
|
|
470
|
+
/** A directional light with spherical positioning controls */
|
|
437
471
|
class Sun extends DirectionalLight {
|
|
438
472
|
constructor() {
|
|
439
473
|
super(...arguments);
|
|
474
|
+
/** Internal vectors to avoid garbage collection */
|
|
440
475
|
this.tempVector3D0 = new Vector3();
|
|
441
476
|
this.tempVector3D1 = new Vector3();
|
|
442
477
|
this.tempVector3D2 = new Vector3();
|
|
@@ -448,27 +483,34 @@ class Sun extends DirectionalLight {
|
|
|
448
483
|
this.tempBox3 = new Box3();
|
|
449
484
|
this.tempSpherical = new Spherical();
|
|
450
485
|
}
|
|
486
|
+
/** Distance from the light to its target */
|
|
451
487
|
get distance() {
|
|
452
488
|
return this.position.length();
|
|
453
489
|
}
|
|
490
|
+
/** Vertical angle from the ground in radians */
|
|
454
491
|
get elevation() {
|
|
455
492
|
return this.tempSpherical.setFromVector3(this.position).phi;
|
|
456
493
|
}
|
|
494
|
+
/** Horizontal angle around the target in radians */
|
|
457
495
|
get azimuth() {
|
|
458
496
|
return this.tempSpherical.setFromVector3(this.position).theta;
|
|
459
497
|
}
|
|
498
|
+
/** Set distance while keeping current angles */
|
|
460
499
|
set distance(value) {
|
|
461
500
|
this.tempSpherical.setFromVector3(this.position);
|
|
462
501
|
this.position.setFromSphericalCoords(value, this.tempSpherical.phi, this.tempSpherical.theta);
|
|
463
502
|
}
|
|
503
|
+
/** Set elevation while keeping current distance and azimuth */
|
|
464
504
|
set elevation(value) {
|
|
465
505
|
this.tempSpherical.setFromVector3(this.position);
|
|
466
506
|
this.position.setFromSphericalCoords(this.tempSpherical.radius, value, this.tempSpherical.theta);
|
|
467
507
|
}
|
|
508
|
+
/** Set azimuth while keeping current distance and elevation */
|
|
468
509
|
set azimuth(value) {
|
|
469
510
|
this.tempSpherical.setFromVector3(this.position);
|
|
470
511
|
this.position.setFromSphericalCoords(this.tempSpherical.radius, this.tempSpherical.phi, value);
|
|
471
512
|
}
|
|
513
|
+
/** Configure shadows to cover all corners of a bounding box */
|
|
472
514
|
setShadowMapFromBox3(box3) {
|
|
473
515
|
const camera = this.shadow.camera;
|
|
474
516
|
this.target.updateWorldMatrix(true, false);
|
|
@@ -498,23 +540,26 @@ class Sun extends DirectionalLight {
|
|
|
498
540
|
camera.updateWorldMatrix(true, false);
|
|
499
541
|
camera.updateProjectionMatrix();
|
|
500
542
|
}
|
|
543
|
+
/** Set light direction based on brightest point in an HDR texture */
|
|
501
544
|
setDirectionFromHDR(texture, distance = 1) {
|
|
502
545
|
const data = texture.image.data;
|
|
503
546
|
const width = texture.image.width;
|
|
504
547
|
const height = texture.image.height;
|
|
505
548
|
let maxLuminance = 0;
|
|
506
549
|
let maxIndex = 0;
|
|
507
|
-
|
|
550
|
+
// Find brightest pixel
|
|
551
|
+
const step = texture.format === RGBAFormat ? RGBA_CHANNEL_COUNT : RGB_CHANNEL_COUNT;
|
|
508
552
|
for (let i = 0; i < data.length; i += step) {
|
|
509
553
|
const r = data[i];
|
|
510
554
|
const g = data[i + 1];
|
|
511
555
|
const b = data[i + 2];
|
|
512
|
-
const luminance =
|
|
556
|
+
const luminance = LUMINANCE_R * r + LUMINANCE_G * g + LUMINANCE_B * b;
|
|
513
557
|
if (luminance > maxLuminance) {
|
|
514
558
|
maxLuminance = luminance;
|
|
515
559
|
maxIndex = i;
|
|
516
560
|
}
|
|
517
561
|
}
|
|
562
|
+
// Convert to spherical coordinates
|
|
518
563
|
const pixelIndex = maxIndex / step;
|
|
519
564
|
const x = pixelIndex % width;
|
|
520
565
|
const y = Math.floor(pixelIndex / width);
|
|
@@ -526,5 +571,5 @@ class Sun extends DirectionalLight {
|
|
|
526
571
|
}
|
|
527
572
|
}
|
|
528
573
|
|
|
529
|
-
export {
|
|
574
|
+
export { BiFovCamera, Bounds, InstanceAssembler, SceneProcessor, SceneTraversal, SkinnedMeshBaker, Sun };
|
|
530
575
|
//# sourceMappingURL=index.js.map
|