diff --git a/index.html b/index.html index bb3f0a89..8bf7b121 100644 --- a/index.html +++ b/index.html @@ -5651,11 +5651,11 @@
Texture resolution:
@@ -5682,7 +5682,7 @@
- +
@@ -7910,7 +7910,7 @@ - + @@ -7946,19 +7946,17 @@ - + - - - + diff --git a/libs/LoopSubdivision.js b/libs/LoopSubdivision.js deleted file mode 100644 index dfe83afc..00000000 --- a/libs/LoopSubdivision.js +++ /dev/null @@ -1,827 +0,0 @@ -/** - * @description Loop Subdivision Surface - * @about Smooth subdivision surface modifier for use with three.js BufferGeometry. - * @author Stephens Nunnally <@stevinz> - * @license MIT - Copyright (c) 2022 Stephens Nunnally - * @source https://github.com/stevinz/three-subdivide - */ -///////////////////////////////////////////////////////////////////////////////////// -// -// Functions -// modify Applies Loop subdivision to BufferGeometry, returns new BufferGeometry -// edgeSplit Splits all triangles at edges shared by coplanar triangles -// flat One iteration of Loop subdivision, without point averaging -// smooth One iteration of Loop subdivision, with point averaging -// -// Info -// This modifier uses the Loop (Charles Loop, 1987) subdivision surface algorithm to smooth -// modern three.js BufferGeometry. -// -// At one point, three.js included a subdivision surface modifier in the extended examples (see bottom -// of file for links), it was removed in r125. The modifier was originally based on the Catmull-Clark -// algorithm, which works best for geometry with convex coplanar n-gon faces. In three.js r60 the modifier -// was changed to utilize the Loop algorithm. The Loop algorithm was designed to work better with triangle -// based meshes. -// -// The Loop algorithm, however, doesn't always provide uniform results as the vertices are -// skewed toward the most used vertex positions. A triangle based box (e.g. BoxGeometry for example) will -// tend to favor the corners. To alleviate this issue, this implementation includes an initial pass to split -// coplanar faces at their shared edges. It starts by splitting along the longest shared edge first, and then -// from that midpoint it splits to any remaining coplanar shared edges. -// -// Also by default, this implementation inserts new uv coordinates, but does not average them using the Loop -// algorithm. In some cases (often in flat geometries) this will produce undesired results, a -// noticeable tearing will occur. In such cases, try passing 'uvSmooth' as true to enable uv averaging. -// -// Note(s) -// - This modifier returns a new BufferGeometry instance, it does not dispose() of the old geometry. -// -// - This modifier returns a NonIndexed geometry. An Indexed geometry can be created by using the -// BufferGeometryUtils.mergeVertices() function, see: -// https://threejs.org/docs/?q=buffer#examples/en/utils/BufferGeometryUtils.mergeVertices -// -// - This modifier works best with geometry whose triangles share edges AND edge vertices. See diagram below. -// -// OKAY NOT OKAY -// O O -// /|\ / \ -// / | \ / \ -// / | \ / \ -// O---O---O O---O---O -// \ | / \ | / -// \ | / \ | / -// \|/ \|/ -// O O -// -// Reference(s) -// - Subdivision Surfaces -// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/thesis-10.pdf -// https://en.wikipedia.org/wiki/Loop_subdivision_surface -// https://cseweb.ucsd.edu/~alchern/teaching/cse167_fa21/6-3Surfaces.pdf -// -// - Original three.js SubdivisionModifier, r124 (Loop) -// https://github.com/mrdoob/three.js/blob/r124/examples/jsm/modifiers/SubdivisionModifier.js -// -// - Original three.js SubdivisionModifier, r59 (Catmull-Clark) -// https://github.com/mrdoob/three.js/blob/r59/examples/js/modifiers/SubdivisionModifier.js -// -///////////////////////////////////////////////////////////////////////////////////// - -const loopSubdivision = {}; -(()=>{ - ///// Constants - - const POSITION_DECIMALS = 2; - - ///// Local Variables - - const _average = new THREE.Vector3(); - const _center = new THREE.Vector3(); - const _midpoint = new THREE.Vector3(); - const _normal = new THREE.Vector3(); - const _temp = new THREE.Vector3(); - - const _vector0 = new THREE.Vector3(); // .Vector4(); - const _vector1 = new THREE.Vector3(); // .Vector4(); - const _vector2 = new THREE.Vector3(); // .Vector4(); - const _vec0to1 = new THREE.Vector3(); - const _vec1to2 = new THREE.Vector3(); - const _vec2to0 = new THREE.Vector3(); - - const _position = [ - new THREE.Vector3(), - new THREE.Vector3(), - new THREE.Vector3(), - ]; - - const _vertex = [ - new THREE.Vector3(), - new THREE.Vector3(), - new THREE.Vector3(), - ]; - - const _triangle = new THREE.Triangle(); - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Loop Subdivision Surface - ///////////////////////////////////////////////////////////////////////////////////// - - /** Loop subdivision surface modifier for use with modern three.js BufferGeometry */ - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Modify - //////////////////// - - /** - * Applies Loop subdivision modifier to geometry - * - * @param {Object} bufferGeometry - Three.js geometry to be subdivided - * @param {Number} iterations - How many times to run subdividion - * @param {Object} params - Optional parameters object, see below - * @returns {Object} Returns new, subdivided, three.js BufferGeometry object - * - * Optional Parameters Object - * @param {Boolean} split - Should coplanar faces be divided along shared edges before running Loop subdivision? - * @param {Boolean} uvSmooth - Should UV values be averaged during subdivision? - * @param {Boolean} preserveEdges - Should edges / breaks in geometry be ignored during subdivision? - * @param {Boolean} flatOnly - If true, subdivision generates triangles, but does not modify positions - * @param {Number} maxTriangles - If geometry contains more than this many triangles, subdivision will not continue - */ - function modify(bufferGeometry, iterations = 1, params = {}) { - if (arguments.length > 3) console.warn(`modify() now uses a parameter object. See readme for more info!`); - - if (typeof params !== 'object') params = {}; - - ///// Parameters - if (params.split === undefined) params.split = true; - if (params.uvSmooth === undefined) params.uvSmooth = false; - if (params.preserveEdges === undefined) params.preserveEdges = false; - if (params.flatOnly === undefined) params.flatOnly = false; - if (params.maxTriangles === undefined) params.maxTriangles = Infinity; - - ///// Geometries - if (!verifyGeometry(bufferGeometry)) return bufferGeometry; - let modifiedGeometry = bufferGeometry.clone(); - - ///// Presplit - if (params.split) { - const splitGeometry = edgeSplit(modifiedGeometry) - modifiedGeometry.dispose(); - modifiedGeometry = splitGeometry; - } - - ///// Apply Subdivision - for (let i = 0; i < iterations; i++) { - let currentTriangles = modifiedGeometry.attributes.position.count / 3; - if (currentTriangles < params.maxTriangles) { - let subdividedGeometry; - - // Subdivide - if (params.flatOnly) { - subdividedGeometry = flat(modifiedGeometry); - } else { - subdividedGeometry = smooth(modifiedGeometry, params); - } - - // Copy and Resize Groups - modifiedGeometry.groups.forEach((group) => { - subdividedGeometry.addGroup(group.start * 4, group.count * 4, group.materialIndex); - }); - - // Clean Up - modifiedGeometry.dispose(); - modifiedGeometry = subdividedGeometry; - } - } - - ///// Return New Geometry - return modifiedGeometry; - } - loopSubdivision.modify = modify; - ///////////////////////////////////////////////////////////////////////////////////// - ///// Split Hypotenuse - //////////////////// - - /** - * Applies one iteration of split subdivision. Splits all triangles at edges shared by coplanar triangles. - * Starts by splitting at longest shared edge, followed by splitting from that new center edge point to the - * center of any other shared edges. - */ - function edgeSplit(geometry) { - - ///// Geometries - if (!verifyGeometry(geometry)) return geometry; - const existing = (geometry.index !== null) ? geometry.toNonIndexed() : geometry.clone(); - const split = new THREE.BufferGeometry(); - - ///// Attributes - const attributeList = gatherAttributes(existing); - const vertexCount = existing.attributes.position.count; - const posAttribute = existing.getAttribute('position'); - const norAttribute = existing.getAttribute('normal'); - const edgeHashToTriangle = {}; - const triangleEdgeHashes = []; - const edgeLength = {}; - const triangleExist = []; - - ///// Edges - for (let i = 0; i < vertexCount; i += 3) { - - // Positions - _vector0.fromBufferAttribute(posAttribute, i + 0); - _vector1.fromBufferAttribute(posAttribute, i + 1); - _vector2.fromBufferAttribute(posAttribute, i + 2); - _normal.fromBufferAttribute(norAttribute, i); - const vecHash0 = hashFromVector(_vector0); - const vecHash1 = hashFromVector(_vector1); - const vecHash2 = hashFromVector(_vector2); - - // Verify Area - const triangleSize = _triangle.set(_vector0, _vector1, _vector2).getArea(); - triangleExist.push(!fuzzy(triangleSize, 0)); - if (!triangleExist[i / 3]) { - triangleEdgeHashes.push([]); - continue; - } - - // Calculate Normals - calcNormal(_normal, _vector0, _vector1, _vector2); - const normalHash = hashFromVector(_normal); - - // Vertex Hashes - const hashes = [ - `${vecHash0}_${vecHash1}_${normalHash}`, // [0]: 0to1 - `${vecHash1}_${vecHash0}_${normalHash}`, // [1]: 1to0 - `${vecHash1}_${vecHash2}_${normalHash}`, // [2]: 1to2 - `${vecHash2}_${vecHash1}_${normalHash}`, // [3]: 2to1 - `${vecHash2}_${vecHash0}_${normalHash}`, // [4]: 2to0 - `${vecHash0}_${vecHash2}_${normalHash}`, // [5]: 0to2 - ]; - - // Store Edge Hashes - const index = i / 3; - for (let j = 0; j < hashes.length; j++) { - // Attach Triangle Index to Edge Hash - if (!edgeHashToTriangle[hashes[j]]) edgeHashToTriangle[hashes[j]] = []; - edgeHashToTriangle[hashes[j]].push(index); - - // Edge Length - if (!edgeLength[hashes[j]]) { - if (j === 0 || j === 1) edgeLength[hashes[j]] = _vector0.distanceTo(_vector1); - if (j === 2 || j === 3) edgeLength[hashes[j]] = _vector1.distanceTo(_vector2); - if (j === 4 || j === 5) edgeLength[hashes[j]] = _vector2.distanceTo(_vector0); - } - } - - // Triangle Edge Reference - triangleEdgeHashes.push([hashes[0], hashes[2], hashes[4]]); - } - - ///// Build Geometry, Set Attributes - attributeList.forEach((attributeName) => { - const attribute = existing.getAttribute(attributeName); - if (!attribute) return; - const floatArray = splitAttribute(attribute, attributeName); - split.setAttribute(attributeName, new THREE.BufferAttribute(floatArray, attribute.itemSize)); - }); - - ///// Morph Attributes - const morphAttributes = existing.morphAttributes; - for (const attributeName in morphAttributes) { - const array = []; - const morphAttribute = morphAttributes[attributeName]; - - // Process Array of Float32BufferAttributes - for (let i = 0, l = morphAttribute.length; i < l; i++) { - if (morphAttribute[i].count !== vertexCount) continue; - const floatArray = splitAttribute(morphAttribute[i], attributeName, true); - array.push(new THREE.BufferAttribute(floatArray, morphAttribute[i].itemSize)); - } - split.morphAttributes[attributeName] = array; - } - split.morphTargetsRelative = existing.morphTargetsRelative; - - // Clean Up, Return New Geometry - existing.dispose(); - return split; - - // Loop Subdivide Function - function splitAttribute(attribute, attributeName, morph = false) { - const newTriangles = 4; /* maximum number of new triangles */ - const arrayLength = (vertexCount * attribute.itemSize) * newTriangles; - const floatArray = new attribute.array.constructor(arrayLength); - - const processGroups = (attributeName === 'position' && !morph && existing.groups.length > 0); - let groupStart = undefined, groupMaterial = undefined; - - let index = 0; - let skipped = 0; - let step = attribute.itemSize; - for (let i = 0; i < vertexCount; i += 3) { - - // Verify Triangle is Valid - if (!triangleExist[i / 3]) { - skipped += 3; - continue; - } - - // Get Triangle Points - _vector0.fromBufferAttribute(attribute, i + 0); - _vector1.fromBufferAttribute(attribute, i + 1); - _vector2.fromBufferAttribute(attribute, i + 2); - - // Check for Shared Edges - const existingIndex = i / 3; - const edgeHash0to1 = triangleEdgeHashes[existingIndex][0]; - const edgeHash1to2 = triangleEdgeHashes[existingIndex][1]; - const edgeHash2to0 = triangleEdgeHashes[existingIndex][2]; - - const edgeCount0to1 = edgeHashToTriangle[edgeHash0to1].length; - const edgeCount1to2 = edgeHashToTriangle[edgeHash1to2].length; - const edgeCount2to0 = edgeHashToTriangle[edgeHash2to0].length; - const sharedCount = (edgeCount0to1 + edgeCount1to2 + edgeCount2to0) - 3; - - // New Index (Before New Triangles, used for Groups) - const loopStartIndex = ((index * 3) / step) / 3; - - // No Shared Edges - if (sharedCount === 0) { - setTriangle(floatArray, index, step, _vector0, _vector1, _vector2); index += (step * 3); - - // Shared Edges - } else { - const length0to1 = edgeLength[edgeHash0to1]; - const length1to2 = edgeLength[edgeHash1to2]; - const length2to0 = edgeLength[edgeHash2to0]; - - // Add New Triangle Positions - if ((length0to1 > length1to2 || edgeCount1to2 <= 1) && - (length0to1 > length2to0 || edgeCount2to0 <= 1) && edgeCount0to1 > 1) { - _center.copy(_vector0).add(_vector1).divideScalar(2.0); - if (edgeCount2to0 > 1) { - _midpoint.copy(_vector2).add(_vector0).divideScalar(2.0); - setTriangle(floatArray, index, step, _vector0, _center, _midpoint); index += (step * 3); - setTriangle(floatArray, index, step, _center, _vector2, _midpoint); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector0, _center, _vector2); index += (step * 3); - } - if (edgeCount1to2 > 1) { - _midpoint.copy(_vector1).add(_vector2).divideScalar(2.0); - setTriangle(floatArray, index, step, _center, _vector1, _midpoint); index += (step * 3); - setTriangle(floatArray, index, step, _midpoint, _vector2, _center); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector1, _vector2, _center); index += (step * 3); - } - - } else if ((length1to2 > length2to0 || edgeCount2to0 <= 1) && edgeCount1to2 > 1) { - _center.copy(_vector1).add(_vector2).divideScalar(2.0); - if (edgeCount0to1 > 1) { - _midpoint.copy(_vector0).add(_vector1).divideScalar(2.0); - setTriangle(floatArray, index, step, _center, _midpoint, _vector1); index += (step * 3); - setTriangle(floatArray, index, step, _midpoint, _center, _vector0); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector1, _center, _vector0); index += (step * 3); - } - if (edgeCount2to0 > 1) { - _midpoint.copy(_vector2).add(_vector0).divideScalar(2.0); - setTriangle(floatArray, index, step, _center, _vector2, _midpoint); index += (step * 3); - setTriangle(floatArray, index, step, _midpoint, _vector0, _center); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector2, _vector0, _center); index += (step * 3); - } - - } else if (edgeCount2to0 > 1) { - _center.copy(_vector2).add(_vector0).divideScalar(2.0); - if (edgeCount1to2 > 1) { - _midpoint.copy(_vector1).add(_vector2).divideScalar(2.0); - setTriangle(floatArray, index, step, _vector2, _center, _midpoint); index += (step * 3); - setTriangle(floatArray, index, step, _center, _vector1, _midpoint); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector2, _center, _vector1); index += (step * 3); - } - if (edgeCount0to1 > 1) { - _midpoint.copy(_vector0).add(_vector1).divideScalar(2.0); - setTriangle(floatArray, index, step, _vector0, _midpoint, _center); index += (step * 3); - setTriangle(floatArray, index, step, _midpoint, _vector1, _center); index += (step * 3); - } else { - setTriangle(floatArray, index, step, _vector0, _vector1, _center); index += (step * 3); - } - - } else { - setTriangle(floatArray, index, step, _vector0, _vector1, _vector2); index += (step * 3); - } - } - - // Process Groups - if (processGroups) { - existing.groups.forEach((group) => { - if (group.start === (i - skipped)) { - if (groupStart !== undefined && groupMaterial !== undefined) { - split.addGroup(groupStart, loopStartIndex - groupStart, groupMaterial); - } - groupStart = loopStartIndex; - groupMaterial = group.materialIndex; - } - }); - } - - // Reset Skipped Triangle Counter - skipped = 0; - } - - // Resize Array - const reducedCount = (index * 3) / step; - const reducedArray = new attribute.array.constructor(reducedCount); - for (let i = 0; i < reducedCount; i++) { - reducedArray[i] = floatArray[i]; - } - - // Final Group - if (processGroups && groupStart !== undefined && groupMaterial !== undefined) { - split.addGroup(groupStart, (((index * 3) / step) / 3) - groupStart, groupMaterial); - } - - return reducedArray; - } - } - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Flat - //////////////////// - - /** Applies one iteration of Loop (flat) subdivision (1 triangle split into 4 triangles) */ - function flat(geometry) { - - ///// Geometries - if (!verifyGeometry(geometry)) return geometry; - const existing = (geometry.index !== null) ? geometry.toNonIndexed() : geometry.clone(); - const loop = new THREE.BufferGeometry(); - - ///// Attributes - const attributeList = gatherAttributes(existing); - const vertexCount = existing.attributes.position.count; - - ///// Build Geometry - attributeList.forEach((attributeName) => { - const attribute = existing.getAttribute(attributeName); - if (!attribute) return; - - loop.setAttribute(attributeName, flatAttribute(attribute, vertexCount)); - }); - - ///// Morph Attributes - const morphAttributes = existing.morphAttributes; - for (const attributeName in morphAttributes) { - const array = []; - const morphAttribute = morphAttributes[attributeName]; - - // Process Array of Float32BufferAttributes - for (let i = 0, l = morphAttribute.length; i < l; i++) { - if (morphAttribute[i].count !== vertexCount) continue; - array.push(flatAttribute(morphAttribute[i], vertexCount)); - } - loop.morphAttributes[attributeName] = array; - } - loop.morphTargetsRelative = existing.morphTargetsRelative; - - ///// Clean Up - existing.dispose(); - return loop; - } - - function flatAttribute(attribute, vertexCount) { - const newTriangles = 4; - const arrayLength = (vertexCount * attribute.itemSize) * newTriangles; - const floatArray = new attribute.array.constructor(arrayLength); - - let index = 0; - let step = attribute.itemSize; - for (let i = 0; i < vertexCount; i += 3) { - - // Original Vertices - _vector0.fromBufferAttribute(attribute, i + 0); - _vector1.fromBufferAttribute(attribute, i + 1); - _vector2.fromBufferAttribute(attribute, i + 2); - - // Midpoints - _vec0to1.copy(_vector0).add(_vector1).divideScalar(2.0); - _vec1to2.copy(_vector1).add(_vector2).divideScalar(2.0); - _vec2to0.copy(_vector2).add(_vector0).divideScalar(2.0); - - // Add New Triangle Positions - setTriangle(floatArray, index, step, _vector0, _vec0to1, _vec2to0); index += (step * 3); - setTriangle(floatArray, index, step, _vector1, _vec1to2, _vec0to1); index += (step * 3); - setTriangle(floatArray, index, step, _vector2, _vec2to0, _vec1to2); index += (step * 3); - setTriangle(floatArray, index, step, _vec0to1, _vec1to2, _vec2to0); index += (step * 3); - } - - return new THREE.BufferAttribute(floatArray, attribute.itemSize); - } - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Smooth - //////////////////// - - /** Applies one iteration of Loop (smooth) subdivision (1 triangle split into 4 triangles) */ - function smooth(geometry, params = {}) { - - if (typeof params !== 'object') params = {}; - - ///// Parameters - if (params.uvSmooth === undefined) params.uvSmooth = false; - if (params.preserveEdges === undefined) params.preserveEdges = false; - - ///// Geometries - if (!verifyGeometry(geometry)) return geometry; - const existing = (geometry.index !== null) ? geometry.toNonIndexed() : geometry.clone(); - const flatGeometry = flat(existing); - const loop = new THREE.BufferGeometry(); - - ///// Attributes - const attributeList = gatherAttributes(existing); - const vertexCount = existing.attributes.position.count; - const posAttribute = existing.getAttribute('position'); - const flatPosition = flatGeometry.getAttribute('position'); - const hashToIndex = {}; // Position hash mapped to index values of same position - const existingNeighbors = {}; // Position hash mapped to existing vertex neighbors - const flatOpposites = {}; // Position hash mapped to new edge point opposites - const existingEdges = {}; - - function addNeighbor(posHash, neighborHash, index) { - if (!existingNeighbors[posHash]) existingNeighbors[posHash] = {}; - if (!existingNeighbors[posHash][neighborHash]) existingNeighbors[posHash][neighborHash] = []; - existingNeighbors[posHash][neighborHash].push(index); - } - - function addOpposite(posHash, index) { - if (!flatOpposites[posHash]) flatOpposites[posHash] = []; - flatOpposites[posHash].push(index); - } - - function addEdgePoint(posHash, edgeHash) { - if (!existingEdges[posHash]) existingEdges[posHash] = new Set(); - existingEdges[posHash].add(edgeHash); - } - - ///// Existing Vertex Hashes - for (let i = 0; i < vertexCount; i += 3) { - const posHash0 = hashFromVector(_vertex[0].fromBufferAttribute(posAttribute, i + 0)); - const posHash1 = hashFromVector(_vertex[1].fromBufferAttribute(posAttribute, i + 1)); - const posHash2 = hashFromVector(_vertex[2].fromBufferAttribute(posAttribute, i + 2)); - - // Neighbors (of Existing Geometry) - addNeighbor(posHash0, posHash1, i + 1); - addNeighbor(posHash0, posHash2, i + 2); - addNeighbor(posHash1, posHash0, i + 0); - addNeighbor(posHash1, posHash2, i + 2); - addNeighbor(posHash2, posHash0, i + 0); - addNeighbor(posHash2, posHash1, i + 1); - - // Opposites (of new FlatSubdivided vertices) - _vec0to1.copy(_vertex[0]).add(_vertex[1]).divideScalar(2.0); - _vec1to2.copy(_vertex[1]).add(_vertex[2]).divideScalar(2.0); - _vec2to0.copy(_vertex[2]).add(_vertex[0]).divideScalar(2.0); - const hash0to1 = hashFromVector(_vec0to1); - const hash1to2 = hashFromVector(_vec1to2); - const hash2to0 = hashFromVector(_vec2to0); - addOpposite(hash0to1, i + 2); - addOpposite(hash1to2, i + 0); - addOpposite(hash2to0, i + 1); - - // Track Edges for 'edgePreserve' - addEdgePoint(posHash0, hash0to1); - addEdgePoint(posHash0, hash2to0); - addEdgePoint(posHash1, hash0to1); - addEdgePoint(posHash1, hash1to2); - addEdgePoint(posHash2, hash1to2); - addEdgePoint(posHash2, hash2to0); - } - - ///// Flat Position to Index Map - for (let i = 0; i < flatGeometry.attributes.position.count; i++) { - const posHash = hashFromVector(_temp.fromBufferAttribute(flatPosition, i)); - if (!hashToIndex[posHash]) hashToIndex[posHash] = []; - hashToIndex[posHash].push(i); - } - - ///// Build Geometry, Set Attributes - attributeList.forEach((attributeName) => { - const existingAttribute = existing.getAttribute(attributeName); - const flatAttribute = flatGeometry.getAttribute(attributeName); - if (existingAttribute === undefined || flatAttribute === undefined) return; - - const floatArray = subdivideAttribute(attributeName, existingAttribute, flatAttribute); - loop.setAttribute(attributeName, new THREE.BufferAttribute(floatArray, flatAttribute.itemSize)); - }); - - ///// Morph Attributes - const morphAttributes = existing.morphAttributes; - for (const attributeName in morphAttributes) { - const array = []; - const morphAttribute = morphAttributes[attributeName]; - - // Process Array of Float32BufferAttributes - for (let i = 0, l = morphAttribute.length; i < l; i++) { - if (morphAttribute[i].count !== vertexCount) continue; - const existingAttribute = morphAttribute[i]; - const flatAttribute = flatAttribute(morphAttribute[i], morphAttribute[i].count) - - const floatArray = subdivideAttribute(attributeName, existingAttribute, flatAttribute); - array.push(new THREE.BufferAttribute(floatArray, flatAttribute.itemSize)); - } - loop.morphAttributes[attributeName] = array; - } - loop.morphTargetsRelative = existing.morphTargetsRelative; - - ///// Clean Up - flatGeometry.dispose(); - existing.dispose(); - return loop; - - ////////// - - // Loop Subdivide Function - function subdivideAttribute(attributeName, existingAttribute, flatAttribute) { - const arrayLength = (flatGeometry.attributes.position.count * flatAttribute.itemSize); - const floatArray = new existingAttribute.array.constructor(arrayLength); - - // Process Triangles - let index = 0; - for (let i = 0; i < flatGeometry.attributes.position.count; i += 3) { - - // Process Triangle Points - for (let v = 0; v < 3; v++) { - - if (attributeName === 'uv' && !params.uvSmooth) { - - _vertex[v].fromBufferAttribute(flatAttribute, i + v); - - } else if (attributeName === 'normal') { // && params.normalSmooth) { - - _position[v].fromBufferAttribute(flatPosition, i + v); - const positionHash = hashFromVector(_position[v]); - const positions = hashToIndex[positionHash]; - - const k = Object.keys(positions).length; - const beta = 0.75 / k; - const startWeight = 1.0 - (beta * k); - - _vertex[v].fromBufferAttribute(flatAttribute, i + v); - _vertex[v].multiplyScalar(startWeight); - - positions.forEach(positionIndex => { - _average.fromBufferAttribute(flatAttribute, positionIndex); - _average.multiplyScalar(beta); - _vertex[v].add(_average); - }); - - - } else { // 'position', 'color', etc... - - _vertex[v].fromBufferAttribute(flatAttribute, i + v); - _position[v].fromBufferAttribute(flatPosition, i + v); - - const positionHash = hashFromVector(_position[v]); - const neighbors = existingNeighbors[positionHash]; - const opposites = flatOpposites[positionHash]; - - ///// Adjust Source Vertex - if (neighbors) { - - // Check Edges have even Opposite Points - if (params.preserveEdges) { - const edgeSet = existingEdges[positionHash]; - let hasPair = true; - for (const edgeHash of edgeSet) { - if (flatOpposites[edgeHash].length % 2 !== 0) hasPair = false; - } - if (!hasPair) continue; - } - - // Number of Neighbors - const k = Object.keys(neighbors).length; - - ///// Loop's Formula - const beta = 1 / k * ((5 / 8) - Math.pow((3 / 8) + (1 / 4) * Math.cos(2 * Math.PI / k), 2)); - - ///// Warren's Formula - // const beta = (k > 3) ? 3 / (8 * k) : ((k === 3) ? 3 / 16 : 0); - - ///// Stevinz' Formula - // const beta = 0.5 / k; - - ///// Average with Neighbors - const startWeight = 1.0 - (beta * k); - _vertex[v].multiplyScalar(startWeight); - - for (let neighborHash in neighbors) { - const neighborIndices = neighbors[neighborHash]; - - _average.set(0, 0, 0); - for (let j = 0; j < neighborIndices.length; j++) { - _average.add(_temp.fromBufferAttribute(existingAttribute, neighborIndices[j])); - } - _average.divideScalar(neighborIndices.length); - - _average.multiplyScalar(beta); - _vertex[v].add(_average); - } - - ///// Newly Added Edge Vertex - } else if (opposites && opposites.length === 2) { - const k = opposites.length; - const beta = 0.125; /* 1/8 */ - const startWeight = 1.0 - (beta * k); - _vertex[v].multiplyScalar(startWeight); - - opposites.forEach(oppositeIndex => { - _average.fromBufferAttribute(existingAttribute, oppositeIndex); - _average.multiplyScalar(beta); - _vertex[v].add(_average); - }); - } - } - } - - // Add New Triangle Position - setTriangle(floatArray, index, flatAttribute.itemSize, _vertex[0], _vertex[1], _vertex[2]); - index += (flatAttribute.itemSize * 3); - } - - return floatArray; - } - - } - - - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Local Functions, Hash - ///////////////////////////////////////////////////////////////////////////////////// - - const _positionShift = Math.pow(10, POSITION_DECIMALS); - - /** Compares two numbers to see if they're almost the same */ - function fuzzy(a, b, tolerance = 0.00001) { - return ((a < (b + tolerance)) && (a > (b - tolerance))); - } - - /** Generates hash strong from Number */ - function hashFromNumber(num, shift = _positionShift) { - let roundedNumber = round(num * shift); - if (roundedNumber == 0) roundedNumber = 0; /* prevent -0 (signed 0 can effect Math.atan2(), etc.) */ - return `${roundedNumber}`; - } - - /** Generates hash strong from Vector3 */ - function hashFromVector(vector, shift = _positionShift) { - return `${hashFromNumber(vector.x, shift)},${hashFromNumber(vector.y, shift)},${hashFromNumber(vector.z, shift)}`; - } - - function round(x) { - return (x + ((x > 0) ? 0.5 : -0.5)) << 0; - } - - ///////////////////////////////////////////////////////////////////////////////////// - ///// Local Functions, Geometry - ///////////////////////////////////////////////////////////////////////////////////// - - function calcNormal(target, vec1, vec2, vec3) { - _temp.subVectors(vec1, vec2); - target.subVectors(vec2, vec3); - target.cross(_temp).normalize(); - } - - function gatherAttributes(geometry) { - const desired = ['position', 'normal', 'uv']; - const contains = Object.keys(geometry.attributes); - const attributeList = Array.from(new Set(desired.concat(contains))); - return attributeList; - } - - function setTriangle(positions, index, step, vec0, vec1, vec2) { - if (step >= 1) { - positions[index + 0 + (step * 0)] = vec0.x; - positions[index + 0 + (step * 1)] = vec1.x; - positions[index + 0 + (step * 2)] = vec2.x; - } - if (step >= 2) { - positions[index + 1 + (step * 0)] = vec0.y; - positions[index + 1 + (step * 1)] = vec1.y; - positions[index + 1 + (step * 2)] = vec2.y; - } - if (step >= 3) { - positions[index + 2 + (step * 0)] = vec0.z; - positions[index + 2 + (step * 1)] = vec1.z; - positions[index + 2 + (step * 2)] = vec2.z; - } - if (step >= 4) { - positions[index + 3 + (step * 0)] = vec0.w; - positions[index + 3 + (step * 1)] = vec1.w; - positions[index + 3 + (step * 2)] = vec2.w; - } - } - - function verifyGeometry(geometry) { - if (geometry === undefined) { - console.warn(`LoopSubdivision: Geometry provided is undefined`); - return false; - } - - if (!geometry.isBufferGeometry) { - console.warn(`LoopSubdivision: Geometry provided is not 'BufferGeometry' type`); - return false; - } - - if (geometry.attributes.position === undefined) { - console.warn(`LoopSubdivision: Geometry provided missing required 'position' attribute`); - return false; - } - - if (geometry.attributes.normal === undefined) { - geometry.computeVertexNormals(); - } - return true; - } - -})() diff --git a/libs/loopsubdivison.min.js b/libs/loopsubdivison.min.js new file mode 100644 index 00000000..b9482517 --- /dev/null +++ b/libs/loopsubdivison.min.js @@ -0,0 +1,132 @@ +/** + * @description Loop Subdivision Surface + * @about Smooth subdivision surface modifier for use with three.js BufferGeometry. + * @author Stephens Nunnally <@stevinz> + * @license MIT - Copyright (c) 2022 Stephens Nunnally + * @source https://github.com/stevinz/three-subdivide + */ +///////////////////////////////////////////////////////////////////////////////////// +// +// Functions +// modify Applies Loop subdivision to BufferGeometry, returns new BufferGeometry +// edgeSplit Splits all triangles at edges shared by coplanar triangles +// flat One iteration of Loop subdivision, without point averaging +// smooth One iteration of Loop subdivision, with point averaging +// +// Info +// This modifier uses the Loop (Charles Loop, 1987) subdivision surface algorithm to smooth +// modern three.js BufferGeometry. +// +// At one point, three.js included a subdivision surface modifier in the extended examples (see bottom +// of file for links), it was removed in r125. The modifier was originally based on the Catmull-Clark +// algorithm, which works best for geometry with convex coplanar n-gon faces. In three.js r60 the modifier +// was changed to utilize the Loop algorithm. The Loop algorithm was designed to work better with triangle +// based meshes. +// +// The Loop algorithm, however, doesn't always provide uniform results as the vertices are +// skewed toward the most used vertex positions. A triangle based box (e.g. BoxGeometry for example) will +// tend to favor the corners. To alleviate this issue, this implementation includes an initial pass to split +// coplanar faces at their shared edges. It starts by splitting along the longest shared edge first, and then +// from that midpoint it splits to any remaining coplanar shared edges. +// +// Also by default, this implementation inserts new uv coordinates, but does not average them using the Loop +// algorithm. In some cases (often in flat geometries) this will produce undesired results, a +// noticeable tearing will occur. In such cases, try passing 'uvSmooth' as true to enable uv averaging. +// +// Note(s) +// - This modifier returns a new BufferGeometry instance, it does not dispose() of the old geometry. +// +// - This modifier returns a NonIndexed geometry. An Indexed geometry can be created by using the +// BufferGeometryUtils.mergeVertices() function, see: +// https://threejs.org/docs/?q=buffer#examples/en/utils/BufferGeometryUtils.mergeVertices +// +// - This modifier works best with geometry whose triangles share edges AND edge vertices. See diagram below. +// +// OKAY NOT OKAY +// O O +// /|\ / \ +// / | \ / \ +// / | \ / \ +// O---O---O O---O---O +// \ | / \ | / +// \ | / \ | / +// \|/ \|/ +// O O +// +// Reference(s) +// - Subdivision Surfaces +// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/thesis-10.pdf +// https://en.wikipedia.org/wiki/Loop_subdivision_surface +// https://cseweb.ucsd.edu/~alchern/teaching/cse167_fa21/6-3Surfaces.pdf +// +// - Original three.js SubdivisionModifier, r124 (Loop) +// https://github.com/mrdoob/three.js/blob/r124/examples/jsm/modifiers/SubdivisionModifier.js +// +// - Original three.js SubdivisionModifier, r59 (Catmull-Clark) +// https://github.com/mrdoob/three.js/blob/r59/examples/js/modifiers/SubdivisionModifier.js +// +///////////////////////////////////////////////////////////////////////////////////// + +/** + * @description Loop Subdivision Surface + * @about Smooth subdivision surface modifier for use with three.js BufferGeometry. + * @author Stephens Nunnally <@stevinz> + * @license MIT - Copyright (c) 2022 Stephens Nunnally + * @source https://github.com/stevinz/three-subdivide + */ + +window.loopSubdivision={};(()=>{const POSITION_DECIMALS=2;const _average=new THREE.Vector3();const _center=new THREE.Vector3();const _midpoint=new THREE.Vector3();const _normal=new THREE.Vector3();const _temp=new THREE.Vector3();const _vector0=new THREE.Vector3();const _vector1=new THREE.Vector3();const _vector2=new THREE.Vector3();const _vec0to1=new THREE.Vector3();const _vec1to2=new THREE.Vector3();const _vec2to0=new THREE.Vector3();const _position=[new THREE.Vector3(),new THREE.Vector3(),new THREE.Vector3(),];const _vertex=[new THREE.Vector3(),new THREE.Vector3(),new THREE.Vector3(),];const _triangle=new THREE.Triangle();function modify(bufferGeometry,iterations=1,params={}){if(arguments.length>3)console.warn(`modify() now uses a parameter object. See readme for more info!`);if(typeof params!=='object')params={};if(params.split===undefined)params.split=!0;if(params.uvSmooth===undefined)params.uvSmooth=!1;if(params.preserveEdges===undefined)params.preserveEdges=!1;if(params.flatOnly===undefined)params.flatOnly=!1;if(params.maxTriangles===undefined)params.maxTriangles=Infinity;if(!verifyGeometry(bufferGeometry))return bufferGeometry;let modifiedGeometry=bufferGeometry.clone();if(params.split){const splitGeometry=edgeSplit(modifiedGeometry) +modifiedGeometry.dispose();modifiedGeometry=splitGeometry} +for(let i=0;i{subdividedGeometry.addGroup(group.start*4,group.count*4,group.materialIndex)});modifiedGeometry.dispose();modifiedGeometry=subdividedGeometry}} +return modifiedGeometry} +window.loopSubdivision.modify=modify;function edgeSplit(geometry){if(!verifyGeometry(geometry))return geometry;const existing=(geometry.index!==null)?geometry.toNonIndexed():geometry.clone();const split=new THREE.BufferGeometry();const attributeList=gatherAttributes(existing);const vertexCount=existing.attributes.position.count;const posAttribute=existing.getAttribute('position');const norAttribute=existing.getAttribute('normal');const edgeHashToTriangle={};const triangleEdgeHashes=[];const edgeLength={};const triangleExist=[];for(let i=0;i{const attribute=existing.getAttribute(attributeName);if(!attribute)return;const floatArray=splitAttribute(attribute,attributeName);split.setAttribute(attributeName,new THREE.BufferAttribute(floatArray,attribute.itemSize))});const morphAttributes=existing.morphAttributes;for(const attributeName in morphAttributes){const array=[];const morphAttribute=morphAttributes[attributeName];for(let i=0,l=morphAttribute.length;i0);let groupStart=undefined,groupMaterial=undefined;let index=0;let skipped=0;let step=attribute.itemSize;for(let i=0;ilength1to2||edgeCount1to2<=1)&&(length0to1>length2to0||edgeCount2to0<=1)&&edgeCount0to1>1){_center.copy(_vector0).add(_vector1).divideScalar(2.0);if(edgeCount2to0>1){_midpoint.copy(_vector2).add(_vector0).divideScalar(2.0);setTriangle(floatArray,index,step,_vector0,_center,_midpoint);index+=(step*3);setTriangle(floatArray,index,step,_center,_vector2,_midpoint);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector0,_center,_vector2);index+=(step*3)} +if(edgeCount1to2>1){_midpoint.copy(_vector1).add(_vector2).divideScalar(2.0);setTriangle(floatArray,index,step,_center,_vector1,_midpoint);index+=(step*3);setTriangle(floatArray,index,step,_midpoint,_vector2,_center);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector1,_vector2,_center);index+=(step*3)}}else if((length1to2>length2to0||edgeCount2to0<=1)&&edgeCount1to2>1){_center.copy(_vector1).add(_vector2).divideScalar(2.0);if(edgeCount0to1>1){_midpoint.copy(_vector0).add(_vector1).divideScalar(2.0);setTriangle(floatArray,index,step,_center,_midpoint,_vector1);index+=(step*3);setTriangle(floatArray,index,step,_midpoint,_center,_vector0);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector1,_center,_vector0);index+=(step*3)} +if(edgeCount2to0>1){_midpoint.copy(_vector2).add(_vector0).divideScalar(2.0);setTriangle(floatArray,index,step,_center,_vector2,_midpoint);index+=(step*3);setTriangle(floatArray,index,step,_midpoint,_vector0,_center);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector2,_vector0,_center);index+=(step*3)}}else if(edgeCount2to0>1){_center.copy(_vector2).add(_vector0).divideScalar(2.0);if(edgeCount1to2>1){_midpoint.copy(_vector1).add(_vector2).divideScalar(2.0);setTriangle(floatArray,index,step,_vector2,_center,_midpoint);index+=(step*3);setTriangle(floatArray,index,step,_center,_vector1,_midpoint);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector2,_center,_vector1);index+=(step*3)} +if(edgeCount0to1>1){_midpoint.copy(_vector0).add(_vector1).divideScalar(2.0);setTriangle(floatArray,index,step,_vector0,_midpoint,_center);index+=(step*3);setTriangle(floatArray,index,step,_midpoint,_vector1,_center);index+=(step*3)}else{setTriangle(floatArray,index,step,_vector0,_vector1,_center);index+=(step*3)}}else{setTriangle(floatArray,index,step,_vector0,_vector1,_vector2);index+=(step*3)}} +if(processGroups){existing.groups.forEach((group)=>{if(group.start===(i-skipped)){if(groupStart!==undefined&&groupMaterial!==undefined){split.addGroup(groupStart,loopStartIndex-groupStart,groupMaterial)} +groupStart=loopStartIndex;groupMaterial=group.materialIndex}})} +skipped=0} +const reducedCount=(index*3)/step;const reducedArray=new attribute.array.constructor(reducedCount);for(let i=0;i{const attribute=existing.getAttribute(attributeName);if(!attribute)return;loop.setAttribute(attributeName,flatAttribute(attribute,vertexCount))});const morphAttributes=existing.morphAttributes;for(const attributeName in morphAttributes){const array=[];const morphAttribute=morphAttributes[attributeName];for(let i=0,l=morphAttribute.length;i{const existingAttribute=existing.getAttribute(attributeName);const flatAttribute=flatGeometry.getAttribute(attributeName);if(existingAttribute===undefined||flatAttribute===undefined)return;const floatArray=subdivideAttribute(attributeName,existingAttribute,flatAttribute);loop.setAttribute(attributeName,new THREE.BufferAttribute(floatArray,flatAttribute.itemSize))});const morphAttributes=existing.morphAttributes;for(const attributeName in morphAttributes){const array=[];const morphAttribute=morphAttributes[attributeName];for(let i=0,l=morphAttribute.length;i{_average.fromBufferAttribute(flatAttribute,positionIndex);_average.multiplyScalar(beta);_vertex[v].add(_average)})}else{_vertex[v].fromBufferAttribute(flatAttribute,i+v);_position[v].fromBufferAttribute(flatPosition,i+v);const positionHash=hashFromVector(_position[v]);const neighbors=existingNeighbors[positionHash];const opposites=flatOpposites[positionHash];if(neighbors){if(params.preserveEdges){const edgeSet=existingEdges[positionHash];let hasPair=!0;for(const edgeHash of edgeSet){if(flatOpposites[edgeHash].length%2!==0)hasPair=!1} +if(!hasPair)continue} +const k=Object.keys(neighbors).length;const beta=1/k*((5/8)-Math.pow((3/8)+(1/4)*Math.cos(2*Math.PI/k),2));const startWeight=1.0-(beta*k);_vertex[v].multiplyScalar(startWeight);for(let neighborHash in neighbors){const neighborIndices=neighbors[neighborHash];_average.set(0,0,0);for(let j=0;j{_average.fromBufferAttribute(existingAttribute,oppositeIndex);_average.multiplyScalar(beta);_vertex[v].add(_average)})}}} +setTriangle(floatArray,index,flatAttribute.itemSize,_vertex[0],_vertex[1],_vertex[2]);index+=(flatAttribute.itemSize*3)} +return floatArray}} +const _positionShift=Math.pow(10,POSITION_DECIMALS);function fuzzy(a,b,tolerance=0.00001){return((a<(b+tolerance))&&(a>(b-tolerance)))} +function hashFromNumber(num,shift=_positionShift){let roundedNumber=round(num*shift);if(roundedNumber==0)roundedNumber=0;return `${roundedNumber}`} +function hashFromVector(vector,shift=_positionShift){return `${hashFromNumber(vector.x, shift)},${hashFromNumber(vector.y, shift)},${hashFromNumber(vector.z, shift)}`} +function round(x){return(x+((x>0)?0.5:-0.5))<<0} +function calcNormal(target,vec1,vec2,vec3){_temp.subVectors(vec1,vec2);target.subVectors(vec2,vec3);target.cross(_temp).normalize()} +function gatherAttributes(geometry){const desired=['position','normal','uv'];const contains=Object.keys(geometry.attributes);const attributeList=Array.from(new Set(desired.concat(contains)));return attributeList} +function setTriangle(positions,index,step,vec0,vec1,vec2){if(step>=1){positions[index+0+(step*0)]=vec0.x;positions[index+0+(step*1)]=vec1.x;positions[index+0+(step*2)]=vec2.x} +if(step>=2){positions[index+1+(step*0)]=vec0.y;positions[index+1+(step*1)]=vec1.y;positions[index+1+(step*2)]=vec2.y} +if(step>=3){positions[index+2+(step*0)]=vec0.z;positions[index+2+(step*1)]=vec1.z;positions[index+2+(step*2)]=vec2.z} +if(step>=4){positions[index+3+(step*0)]=vec0.w;positions[index+3+(step*1)]=vec1.w;positions[index+3+(step*2)]=vec2.w}} +function verifyGeometry(geometry){if(geometry===undefined){console.warn(`window.loopSubdivision: Geometry provided is undefined`);return!1} +if(!geometry.isBufferGeometry){console.warn(`window.loopSubdivision: Geometry provided is not 'BufferGeometry' type`);return!1} +if(geometry.attributes.position===undefined){console.warn(`window.loopSubdivision: Geometry provided missing required 'position' attribute`);return!1} +if(geometry.attributes.normal===undefined){geometry.computeVertexNormals()} +return!0}})() \ No newline at end of file diff --git a/modules/io/export.js b/modules/io/export.js index 191923a5..bcf802ad 100644 --- a/modules/io/export.js +++ b/modules/io/export.js @@ -158,7 +158,7 @@ async function getMapURL(type, options = {}) { noScaleBar = false, noIce = false, fullMap = false, - for3D = false + for3DRender = false } = options; if (fullMap) drawScaleBar(1); diff --git a/modules/ui/3d.js b/modules/ui/3d.js index 1dfe2a5c..ffe65299 100644 --- a/modules/ui/3d.js +++ b/modules/ui/3d.js @@ -14,7 +14,7 @@ window.ThreeD = (function () { labels3d: 0, wireframe: 0, resolution: 2, - resolutionScale: 3, + resolutionScale: 2048, sunColor: "#cccccc", subdivide: 0 }; @@ -328,9 +328,6 @@ window.ThreeD = (function () { line: 5 - towns.attr("data-size") / 2 }; - //Look for a custom model for city and town geometry. If not found use these. - //Maybe serialize the models to the .map file. - const city_icon_material = new THREE.MeshPhongMaterial({color: cityOptions.iconColor}); city_icon_material.wireframe = options.wireframe; const town_icon_material = new THREE.MeshPhongMaterial({color: townOptions.iconColor}); @@ -431,31 +428,13 @@ window.ThreeD = (function () { noLabels: options.labels3d, noWater: options.extendedWater, fullMap: true, - for3D: true + for3DRender: true }; - let sizeOfSkin = 512; - switch(options.resolutionScale){ - case 1: - sizeOfSkin = 512; - break; - case 2: - sizeOfSkin = 1024; - break; - case 3: - sizeOfSkin = 2048; - break; - case 4: - sizeOfSkin = 4096; - break; - case 5: - sizeOfSkin = 8192; - break; - } const url = await getMapURL("mesh",mapOptions); const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); - canvas.width = sizeOfSkin; - canvas.height = sizeOfSkin; + canvas.width = options.resolutionScale; + canvas.height = options.resolutionScale; const img = new Image(); img.src = url; @@ -480,6 +459,7 @@ window.ThreeD = (function () { //Try loading skin texture. texture = new THREE.TextureLoader().load(await createMeshTextureUrl(), render); texture.needsUpdate = true; + texture.anisotropy = Renderer.capabilities.getMaxAnisotropy(); } @@ -505,6 +485,7 @@ window.ThreeD = (function () { geometry.computeVertexNormals(); if (mesh) scene.remove(mesh); if(options.subdivide){ + await loadLoopSubdivision(); const subdivideParams = { split: true, uvSmooth: false, @@ -683,6 +664,17 @@ window.ThreeD = (function () { }); } + function loadLoopSubdivision(){ + if (window.loopSubdivision) return Promise.resolve(true); + + return new Promise(resolve => { + const script = document.createElement("script"); + script.src = "libs/loopsubdivison.min.js"; + document.head.append(script); + script.onload = () => resolve(true); + script.onerror = () => resolve(false); + }); + } function OrbitControls(camera, domElement) { if (THREE.OrbitControls) return new THREE.OrbitControls(camera, domElement); diff --git a/versioning.js b/versioning.js index 523dea9e..fa8ab0f5 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.89.35"; // generator version, update each time +const version = "1.89.36"; // generator version, update each time { document.title += " v" + version;