UNPKG

three-to-ammo

Version:

Convert THREE.Mesh to Ammo.btCollisionShape

784 lines (681 loc) 26.3 kB
"use strict"; /* global Ammo */ import * as THREE from "three"; export const TYPE = { BOX: "box", CYLINDER: "cylinder", SPHERE: "sphere", CAPSULE: "capsule", CONE: "cone", HULL: "hull", HACD: "hacd", //Hierarchical Approximate Convex Decomposition VHACD: "vhacd", //Volumetric Hierarchical Approximate Convex Decomposition MESH: "mesh", HEIGHTFIELD: "heightfield" }; export const FIT = { ALL: "all", //A single shape is automatically sized to bound all meshes within the entity. MANUAL: "manual" //A single shape is sized manually. Requires halfExtents or sphereRadius. }; export const HEIGHTFIELD_DATA_TYPE = { short: "short", float: "float" }; export const createCollisionShapes = function(vertices, matrices, indexes, matrixWorld, options = {}) { switch (options.type) { case TYPE.BOX: return [createBoxShape(vertices, matrices, matrixWorld, options)]; case TYPE.CYLINDER: return [createCylinderShape(vertices, matrices, matrixWorld, options)]; case TYPE.CAPSULE: return [createCapsuleShape(vertices, matrices, matrixWorld, options)]; case TYPE.CONE: return [createConeShape(vertices, matrices, matrixWorld, options)]; case TYPE.SPHERE: return [createSphereShape(vertices, matrices, matrixWorld, options)]; case TYPE.HULL: return [createHullShape(vertices, matrices, matrixWorld, options)]; case TYPE.HACD: return createHACDShapes(vertices, matrices, indexes, matrixWorld, options); case TYPE.VHACD: return createVHACDShapes(vertices, matrices, indexes, matrixWorld, options); case TYPE.MESH: return [createTriMeshShape(vertices, matrices, indexes, matrixWorld, options)]; case TYPE.HEIGHTFIELD: return [createHeightfieldTerrainShape(options)]; default: console.warn(options.type + " is not currently supported"); return []; } }; //TODO: support gimpact (dynamic trimesh) and heightmap export const createBoxShape = function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.BOX; _setOptions(options); if (options.fit === FIT.ALL) { options.halfExtents = _computeHalfExtents( _computeBounds(vertices, matrices), options.minHalfExtent, options.maxHalfExtent ); } const btHalfExtents = new Ammo.btVector3(options.halfExtents.x, options.halfExtents.y, options.halfExtents.z); const collisionShape = new Ammo.btBoxShape(btHalfExtents); Ammo.destroy(btHalfExtents); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; export const createCylinderShape = function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.CYLINDER; _setOptions(options); if (options.fit === FIT.ALL) { options.halfExtents = _computeHalfExtents( _computeBounds(vertices, matrices), options.minHalfExtent, options.maxHalfExtent ); } const btHalfExtents = new Ammo.btVector3(options.halfExtents.x, options.halfExtents.y, options.halfExtents.z); const collisionShape = (() => { switch (options.cylinderAxis) { case "y": return new Ammo.btCylinderShape(btHalfExtents); case "x": return new Ammo.btCylinderShapeX(btHalfExtents); case "z": return new Ammo.btCylinderShapeZ(btHalfExtents); } return null; })(); Ammo.destroy(btHalfExtents); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; export const createCapsuleShape = function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.CAPSULE; _setOptions(options); if (options.fit === FIT.ALL) { options.halfExtents = _computeHalfExtents( _computeBounds(vertices, matrices), options.minHalfExtent, options.maxHalfExtent ); } const { x, y, z } = options.halfExtents; const collisionShape = (() => { switch (options.cylinderAxis) { case "y": return new Ammo.btCapsuleShape(Math.max(x, z), y * 2); case "x": return new Ammo.btCapsuleShapeX(Math.max(y, z), x * 2); case "z": return new Ammo.btCapsuleShapeZ(Math.max(x, y), z * 2); } return null; })(); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; export const createConeShape = function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.CONE; _setOptions(options); if (options.fit === FIT.ALL) { options.halfExtents = _computeHalfExtents( _computeBounds(vertices, matrices), options.minHalfExtent, options.maxHalfExtent ); } const { x, y, z } = options.halfExtents; const collisionShape = (() => { switch (options.cylinderAxis) { case "y": return new Ammo.btConeShape(Math.max(x, z), y * 2); case "x": return new Ammo.btConeShapeX(Math.max(y, z), x * 2); case "z": return new Ammo.btConeShapeZ(Math.max(x, y), z * 2); } return null; })(); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; export const createSphereShape = function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.SPHERE; _setOptions(options); let radius; if (options.fit === FIT.MANUAL && !isNaN(options.sphereRadius)) { radius = options.sphereRadius; } else { radius = _computeRadius(vertices, matrices, _computeBounds(vertices, matrices)); } const collisionShape = new Ammo.btSphereShape(radius); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; export const createHullShape = (function() { const vertex = new THREE.Vector3(); const center = new THREE.Vector3(); const matrix = new THREE.Matrix4(); return function(vertices, matrices, matrixWorld, options = {}) { options.type = TYPE.HULL; _setOptions(options); if (options.fit === FIT.MANUAL) { console.warn("cannot use fit: manual with type: hull"); return null; } const bounds = _computeBounds(vertices, matrices); const btVertex = new Ammo.btVector3(); const originalHull = new Ammo.btConvexHullShape(); originalHull.setMargin(options.margin); center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5); let vertexCount = 0; for (let i = 0; i < vertices.length; i++) { vertexCount += vertices[i].length / 3; } const maxVertices = options.hullMaxVertices || 100000; // todo: might want to implement this in a deterministic way that doesn't do O(verts) calls to Math.random if (vertexCount > maxVertices) { console.warn(`too many vertices for hull shape; sampling ~${maxVertices} from ~${vertexCount} vertices`); } const p = Math.min(1, maxVertices / vertexCount); for (let i = 0; i < vertices.length; i++) { const components = vertices[i]; matrix.fromArray(matrices[i]); for (let j = 0; j < components.length; j += 3) { const isLastVertex = i === vertices.length - 1 && j === components.length - 3; if (Math.random() <= p || isLastVertex) { // always include the last vertex vertex .set(components[j], components[j + 1], components[j + 2]) .applyMatrix4(matrix) .sub(center); btVertex.setValue(vertex.x, vertex.y, vertex.z); originalHull.addPoint(btVertex, isLastVertex); // recalc AABB only on last geometry } } } let collisionShape = originalHull; if (originalHull.getNumVertices() >= 100) { //Bullet documentation says don't use convexHulls with 100 verts or more const shapeHull = new Ammo.btShapeHull(originalHull); shapeHull.buildHull(options.margin); Ammo.destroy(originalHull); collisionShape = new Ammo.btConvexHullShape( Ammo.getPointer(shapeHull.getVertexPointer()), shapeHull.numVertices() ); Ammo.destroy(shapeHull); // btConvexHullShape makes a copy } Ammo.destroy(btVertex); _finishCollisionShape(collisionShape, options, _computeScale(matrixWorld, options)); return collisionShape; }; })(); export const createHACDShapes = (function() { const vector = new THREE.Vector3(); const center = new THREE.Vector3(); const matrix = new THREE.Matrix4(); return function(vertices, matrices, indexes, matrixWorld, options = {}) { options.type = TYPE.HACD; _setOptions(options); if (options.fit === FIT.MANUAL) { console.warn("cannot use fit: manual with type: hacd"); return []; } if (!Ammo.hasOwnProperty("HACD")) { console.warn( "HACD unavailable in included build of Ammo.js. Visit https://github.com/mozillareality/ammo.js for the latest version." ); return []; } const bounds = _computeBounds(vertices, matrices); const scale = _computeScale(matrixWorld, options); let vertexCount = 0; let triCount = 0; center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5); for (let i = 0; i < vertices.length; i++) { vertexCount += vertices[i].length / 3; if (indexes && indexes[i]) { triCount += indexes[i].length / 3; } else { triCount += vertices[i].length / 9; } } const hacd = new Ammo.HACD(); if (options.hasOwnProperty("compacityWeight")) hacd.SetCompacityWeight(options.compacityWeight); if (options.hasOwnProperty("volumeWeight")) hacd.SetVolumeWeight(options.volumeWeight); if (options.hasOwnProperty("nClusters")) hacd.SetNClusters(options.nClusters); if (options.hasOwnProperty("nVerticesPerCH")) hacd.SetNVerticesPerCH(options.nVerticesPerCH); if (options.hasOwnProperty("concavity")) hacd.SetConcavity(options.concavity); const points = Ammo._malloc(vertexCount * 3 * 8); const triangles = Ammo._malloc(triCount * 3 * 4); hacd.SetPoints(points); hacd.SetTriangles(triangles); hacd.SetNPoints(vertexCount); hacd.SetNTriangles(triCount); let pptr = points / 8, tptr = triangles / 4; for (let i = 0; i < vertices.length; i++) { const components = vertices[i]; matrix.fromArray(matrices[i]); for (let j = 0; j < components.length; j += 3) { vector .set(components[j + 0], components[j + 1], components[j + 2]) .applyMatrix4(matrix) .sub(center); Ammo.HEAPF64[pptr + 0] = vector.x; Ammo.HEAPF64[pptr + 1] = vector.y; Ammo.HEAPF64[pptr + 2] = vector.z; pptr += 3; } if (indexes[i]) { const indices = indexes[i]; for (let j = 0; j < indices.length; j++) { Ammo.HEAP32[tptr] = indices[j]; tptr++; } } else { for (let j = 0; j < components.length / 3; j++) { Ammo.HEAP32[tptr] = j; tptr++; } } } hacd.Compute(); Ammo._free(points); Ammo._free(triangles); const nClusters = hacd.GetNClusters(); const shapes = []; for (let i = 0; i < nClusters; i++) { const hull = new Ammo.btConvexHullShape(); hull.setMargin(options.margin); const nPoints = hacd.GetNPointsCH(i); const nTriangles = hacd.GetNTrianglesCH(i); const hullPoints = Ammo._malloc(nPoints * 3 * 8); const hullTriangles = Ammo._malloc(nTriangles * 3 * 4); hacd.GetCH(i, hullPoints, hullTriangles); const pptr = hullPoints / 8; for (let pi = 0; pi < nPoints; pi++) { const btVertex = new Ammo.btVector3(); const px = Ammo.HEAPF64[pptr + pi * 3 + 0]; const py = Ammo.HEAPF64[pptr + pi * 3 + 1]; const pz = Ammo.HEAPF64[pptr + pi * 3 + 2]; btVertex.setValue(px, py, pz); hull.addPoint(btVertex, pi === nPoints - 1); Ammo.destroy(btVertex); } _finishCollisionShape(hull, options, scale); shapes.push(hull); } return shapes; }; })(); export const createVHACDShapes = (function() { const vector = new THREE.Vector3(); const center = new THREE.Vector3(); const matrix = new THREE.Matrix4(); return function(vertices, matrices, indexes, matrixWorld, options = {}) { options.type = TYPE.VHACD; _setOptions(options); if (options.fit === FIT.MANUAL) { console.warn("cannot use fit: manual with type: vhacd"); return []; } if (!Ammo.hasOwnProperty("VHACD")) { console.warn( "VHACD unavailable in included build of Ammo.js. Visit https://github.com/mozillareality/ammo.js for the latest version." ); return []; } const bounds = _computeBounds(vertices, matrices); const scale = _computeScale(matrixWorld, options); let vertexCount = 0; let triCount = 0; center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5); for (let i = 0; i < vertices.length; i++) { vertexCount += vertices[i].length / 3; if (indexes && indexes[i]) { triCount += indexes[i].length / 3; } else { triCount += vertices[i].length / 9; } } const vhacd = new Ammo.VHACD(); const params = new Ammo.Parameters(); //https://kmamou.blogspot.com/2014/12/v-hacd-20-parameters-description.html if (options.hasOwnProperty("resolution")) params.set_m_resolution(options.resolution); if (options.hasOwnProperty("depth")) params.set_m_depth(options.depth); if (options.hasOwnProperty("concavity")) params.set_m_concavity(options.concavity); if (options.hasOwnProperty("planeDownsampling")) params.set_m_planeDownsampling(options.planeDownsampling); if (options.hasOwnProperty("convexhullDownsampling")) params.set_m_convexhullDownsampling(options.convexhullDownsampling); if (options.hasOwnProperty("alpha")) params.set_m_alpha(options.alpha); if (options.hasOwnProperty("beta")) params.set_m_beta(options.beta); if (options.hasOwnProperty("gamma")) params.set_m_gamma(options.gamma); if (options.hasOwnProperty("pca")) params.set_m_pca(options.pca); if (options.hasOwnProperty("mode")) params.set_m_mode(options.mode); if (options.hasOwnProperty("maxNumVerticesPerCH")) params.set_m_maxNumVerticesPerCH(options.maxNumVerticesPerCH); if (options.hasOwnProperty("minVolumePerCH")) params.set_m_minVolumePerCH(options.minVolumePerCH); if (options.hasOwnProperty("convexhullApproximation")) params.set_m_convexhullApproximation(options.convexhullApproximation); if (options.hasOwnProperty("oclAcceleration")) params.set_m_oclAcceleration(options.oclAcceleration); const points = Ammo._malloc(vertexCount * 3 * 8 + 3); const triangles = Ammo._malloc(triCount * 3 * 4); let pptr = points / 8, tptr = triangles / 4; for (let i = 0; i < vertices.length; i++) { const components = vertices[i]; matrix.fromArray(matrices[i]); for (let j = 0; j < components.length; j += 3) { vector .set(components[j + 0], components[j + 1], components[j + 2]) .applyMatrix4(matrix) .sub(center); Ammo.HEAPF64[pptr + 0] = vector.x; Ammo.HEAPF64[pptr + 1] = vector.y; Ammo.HEAPF64[pptr + 2] = vector.z; pptr += 3; } if (indexes[i]) { const indices = indexes[i]; for (let j = 0; j < indices.length; j++) { Ammo.HEAP32[tptr] = indices[j]; tptr++; } } else { for (let j = 0; j < components.length / 3; j++) { Ammo.HEAP32[tptr] = j; tptr++; } } } vhacd.Compute(points, 3, vertexCount, triangles, 3, triCount, params); Ammo._free(points); Ammo._free(triangles); const nHulls = vhacd.GetNConvexHulls(); const shapes = []; const ch = new Ammo.ConvexHull(); for (let i = 0; i < nHulls; i++) { vhacd.GetConvexHull(i, ch); const nPoints = ch.get_m_nPoints(); const hullPoints = ch.get_m_points(); const hull = new Ammo.btConvexHullShape(); hull.setMargin(options.margin); for (let pi = 0; pi < nPoints; pi++) { const btVertex = new Ammo.btVector3(); const px = ch.get_m_points(pi * 3 + 0); const py = ch.get_m_points(pi * 3 + 1); const pz = ch.get_m_points(pi * 3 + 2); btVertex.setValue(px, py, pz); hull.addPoint(btVertex, pi === nPoints - 1); Ammo.destroy(btVertex); } _finishCollisionShape(hull, options, scale); shapes.push(hull); } Ammo.destroy(ch); Ammo.destroy(vhacd); return shapes; }; })(); export const createTriMeshShape = (function() { const va = new THREE.Vector3(); const vb = new THREE.Vector3(); const vc = new THREE.Vector3(); const matrix = new THREE.Matrix4(); return function(vertices, matrices, indexes, matrixWorld, options = {}) { options.type = TYPE.MESH; _setOptions(options); if (options.fit === FIT.MANUAL) { console.warn("cannot use fit: manual with type: mesh"); return null; } const scale = _computeScale(matrixWorld, options); const bta = new Ammo.btVector3(); const btb = new Ammo.btVector3(); const btc = new Ammo.btVector3(); const triMesh = new Ammo.btTriangleMesh(true, false); for (let i = 0; i < vertices.length; i++) { const components = vertices[i]; const index = indexes[i] ? indexes[i] : null; matrix.fromArray(matrices[i]); if (index) { for (let j = 0; j < index.length; j += 3) { const ai = index[j] * 3; const bi = index[j + 1] * 3; const ci = index[j + 2] * 3; va.set(components[ai], components[ai + 1], components[ai + 2]).applyMatrix4(matrix); vb.set(components[bi], components[bi + 1], components[bi + 2]).applyMatrix4(matrix); vc.set(components[ci], components[ci + 1], components[ci + 2]).applyMatrix4(matrix); bta.setValue(va.x, va.y, va.z); btb.setValue(vb.x, vb.y, vb.z); btc.setValue(vc.x, vc.y, vc.z); triMesh.addTriangle(bta, btb, btc, false); } } else { for (let j = 0; j < components.length; j += 9) { va.set(components[j + 0], components[j + 1], components[j + 2]).applyMatrix4(matrix); vb.set(components[j + 3], components[j + 4], components[j + 5]).applyMatrix4(matrix); vc.set(components[j + 6], components[j + 7], components[j + 8]).applyMatrix4(matrix); bta.setValue(va.x, va.y, va.z); btb.setValue(vb.x, vb.y, vb.z); btc.setValue(vc.x, vc.y, vc.z); triMesh.addTriangle(bta, btb, btc, false); } } } const localScale = new Ammo.btVector3(scale.x, scale.y, scale.z); triMesh.setScaling(localScale); Ammo.destroy(localScale); const collisionShape = new Ammo.btBvhTriangleMeshShape(triMesh, true, true); collisionShape.resources = [triMesh]; Ammo.destroy(bta); Ammo.destroy(btb); Ammo.destroy(btc); _finishCollisionShape(collisionShape, options); return collisionShape; }; })(); export const createHeightfieldTerrainShape = function(options = {}) { _setOptions(options); if (options.fit === FIT.ALL) { console.warn("cannot use fit: all with type: heightfield"); return null; } const heightfieldDistance = options.heightfieldDistance || 1; const heightfieldData = options.heightfieldData || []; const heightScale = options.heightScale || 0; const upAxis = options.hasOwnProperty("upAxis") ? options.upAxis : 1; // x = 0; y = 1; z = 2 const hdt = (() => { switch (options.heightDataType) { case "short": return Ammo.PHY_SHORT; case "float": return Ammo.PHY_FLOAT; default: return Ammo.PHY_FLOAT; } })(); const flipQuadEdges = options.hasOwnProperty("flipQuadEdges") ? options.flipQuadEdges : true; const heightStickLength = heightfieldData.length; const heightStickWidth = heightStickLength > 0 ? heightfieldData[0].length : 0; const data = Ammo._malloc(heightStickLength * heightStickWidth * 4); const ptr = data / 4; let minHeight = Number.POSITIVE_INFINITY; let maxHeight = Number.NEGATIVE_INFINITY; let index = 0; for (let l = 0; l < heightStickLength; l++) { for (let w = 0; w < heightStickWidth; w++) { const height = heightfieldData[l][w]; Ammo.HEAPF32[ptr + index] = height; index++; minHeight = Math.min(minHeight, height); maxHeight = Math.max(maxHeight, height); } } const collisionShape = new Ammo.btHeightfieldTerrainShape( heightStickWidth, heightStickLength, data, heightScale, minHeight, maxHeight, upAxis, hdt, flipQuadEdges ); const scale = new Ammo.btVector3(heightfieldDistance, 1, heightfieldDistance); collisionShape.setLocalScaling(scale); Ammo.destroy(scale); collisionShape.heightfieldData = data; _finishCollisionShape(collisionShape, options); return collisionShape; }; function _setOptions(options) { options.fit = options.hasOwnProperty("fit") ? options.fit : FIT.ALL; options.type = options.type || TYPE.HULL; options.minHalfExtent = options.hasOwnProperty("minHalfExtent") ? options.minHalfExtent : 0; options.maxHalfExtent = options.hasOwnProperty("maxHalfExtent") ? options.maxHalfExtent : Number.POSITIVE_INFINITY; options.cylinderAxis = options.cylinderAxis || "y"; options.margin = options.hasOwnProperty("margin") ? options.margin : 0.01; options.includeInvisible = options.hasOwnProperty("includeInvisible") ? options.includeInvisible : false; if (!options.offset) { options.offset = new THREE.Vector3(); } if (!options.orientation) { options.orientation = new THREE.Quaternion(); } } const _finishCollisionShape = function(collisionShape, options, scale) { collisionShape.type = options.type; collisionShape.setMargin(options.margin); collisionShape.destroy = () => { for (let res of collisionShape.resources || []) { Ammo.destroy(res); } if (collisionShape.heightfieldData) { Ammo._free(collisionShape.heightfieldData); } Ammo.destroy(collisionShape); }; const localTransform = new Ammo.btTransform(); const rotation = new Ammo.btQuaternion(); localTransform.setIdentity(); localTransform.getOrigin().setValue(options.offset.x, options.offset.y, options.offset.z); rotation.setValue(options.orientation.x, options.orientation.y, options.orientation.z, options.orientation.w); localTransform.setRotation(rotation); Ammo.destroy(rotation); if (scale) { const localScale = new Ammo.btVector3(scale.x, scale.y, scale.z); collisionShape.setLocalScaling(localScale); Ammo.destroy(localScale); } collisionShape.localTransform = localTransform; }; export const iterateGeometries = (function() { const inverse = new THREE.Matrix4(); return function(root, options, cb) { inverse.copy(root.matrixWorld).invert(); const scale = new THREE.Vector3(); scale.setFromMatrixScale(root.matrixWorld); root.traverse(mesh => { const transform = new THREE.Matrix4(); if ( mesh.isMesh && mesh.name !== "Sky" && (options.includeInvisible || (mesh.el && mesh.el.object3D.visible) || mesh.visible) ) { if (mesh === root) { transform.identity(); } else { mesh.updateWorldMatrix(true); transform.multiplyMatrices(inverse, mesh.matrixWorld); } // todo: might want to return null xform if this is the root so that callers can avoid multiplying // things by the identity matrix cb( mesh.geometry.isBufferGeometry ? mesh.geometry.attributes.position.array : mesh.geometry.vertices, transform.elements, mesh.geometry.index ? mesh.geometry.index.array : null ); } }); }; })(); const _computeScale = (function() { const matrix = new THREE.Matrix4(); return function(matrixWorld, options = {}) { const scale = new THREE.Vector3(1, 1, 1); if (options.fit === FIT.ALL) { matrix.fromArray(matrixWorld); scale.setFromMatrixScale(matrix); } return scale; }; })(); const _computeRadius = (function() { const center = new THREE.Vector3(); return function(vertices, matrices, bounds) { let maxRadiusSq = 0; let { x: cx, y: cy, z: cz } = bounds.getCenter(center); _iterateVertices(vertices, matrices, v => { const dx = cx - v.x; const dy = cy - v.y; const dz = cz - v.z; maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz); }); return Math.sqrt(maxRadiusSq); }; })(); const _computeHalfExtents = function(bounds, minHalfExtent, maxHalfExtent) { const halfExtents = new THREE.Vector3(); return halfExtents .subVectors(bounds.max, bounds.min) .multiplyScalar(0.5) .clampScalar(minHalfExtent, maxHalfExtent); }; const _computeLocalOffset = function(matrix, bounds, target) { target .addVectors(bounds.max, bounds.min) .multiplyScalar(0.5) .applyMatrix4(matrix); return target; }; // returns the bounding box for the geometries underneath `root`. const _computeBounds = function(vertices, matrices) { const bounds = new THREE.Box3(); let minX = +Infinity; let minY = +Infinity; let minZ = +Infinity; let maxX = -Infinity; let maxY = -Infinity; let maxZ = -Infinity; bounds.min.set(0, 0, 0); bounds.max.set(0, 0, 0); _iterateVertices(vertices, matrices, v => { if (v.x < minX) minX = v.x; if (v.y < minY) minY = v.y; if (v.z < minZ) minZ = v.z; if (v.x > maxX) maxX = v.x; if (v.y > maxY) maxY = v.y; if (v.z > maxZ) maxZ = v.z; }); bounds.min.set(minX, minY, minZ); bounds.max.set(maxX, maxY, maxZ); return bounds; }; const _iterateVertices = (function() { const vertex = new THREE.Vector3(); const matrix = new THREE.Matrix4(); return function(vertices, matrices, cb) { for (let i = 0; i < vertices.length; i++) { matrix.fromArray(matrices[i]); for (let j = 0; j < vertices[i].length; j += 3) { vertex.set(vertices[i][j], vertices[i][j + 1], vertices[i][j + 2]).applyMatrix4(matrix); cb(vertex); } } }; })();