/** * @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}})()