smooth static method
Applies one iteration of Loop (smooth) subdivision (1 triangle split into 4 triangles)
Implementation
static smooth(BufferGeometry geometry, [LoopParameters? params]) {
params ??= LoopParameters();
///// Geometries
if (! verifyGeometry(geometry)) return geometry;
final existing = (geometry.index != null) ? geometry.toNonIndexed() : geometry.clone();
final flat = LoopSubdivision.flat(existing, params);
final loop = BufferGeometry();
///// Attributes
final attributeList = gatherAttributes(existing);
final vertexCount = existing.attributes['position'].count;
final posAttribute = existing.getAttributeFromString('position');
final flatPosition = flat.getAttributeFromString('position');
final hashToIndex = {}; // Position hash mapped to index values of same position
final existingNeighbors = {}; // Position hash mapped to existing vertex neighbors
final flatOpposites = {}; // Position hash mapped to edge point opposites
final existingEdges = {};
void addNeighbor(posHash, neighborHash, index) {
if (existingNeighbors[posHash] == null) existingNeighbors[posHash] = {};
if (existingNeighbors[posHash][neighborHash] == null) existingNeighbors[posHash][neighborHash] = [];
existingNeighbors[posHash][neighborHash].add(index);
}
void addOpposite(posHash, index) {
if (flatOpposites[posHash] == null) flatOpposites[posHash] = [];
flatOpposites[posHash].add(index);
}
void addEdgePoint(posHash, edgeHash) {
if (existingEdges[posHash] == null) existingEdges[posHash] = [];
existingEdges[posHash].add(edgeHash);
}
///// Existing Vertex Hashes
for (int i = 0; i < vertexCount; i += 3) {
final posHash0 = hashFromVector(_vertex[0].fromBuffer(posAttribute, i + 0));
final posHash1 = hashFromVector(_vertex[1].fromBuffer(posAttribute, i + 1));
final posHash2 = hashFromVector(_vertex[2].fromBuffer(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 FlatSubdivided vertices)
_vec0to1.setFrom(_vertex[0]).add(_vertex[1]).divideScalar(2.0);
_vec1to2.setFrom(_vertex[1]).add(_vertex[2]).divideScalar(2.0);
_vec2to0.setFrom(_vertex[2]).add(_vertex[0]).divideScalar(2.0);
final hash0to1 = hashFromVector(_vec0to1);
final hash1to2 = hashFromVector(_vec1to2);
final 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 (int i = 0; i < flat.attributes['position'].count; i++) {
final posHash = hashFromVector(_temp.fromBuffer(flatPosition, i));
if (hashToIndex[posHash] == null) hashToIndex[posHash] = [];
hashToIndex[posHash].add(i);
}
List<double> subdivideAttribute(String attributeName, BufferAttribute existingAttribute, BufferAttribute flattenedAttribute) {
final arrayLength = (flat.attributes['position'].count * flattenedAttribute.itemSize);
final List<double> floatArray = List.filled(arrayLength, 0.0);//attribute.array.constructor(arrayLength);
// Process Triangles
int index = 0;
for (int i = 0; i < flat.attributes['position'].count; i += 3) {
// Process Triangle Points
for (int v = 0; v < 3; v++) {
if (attributeName == 'uv' && !params!.uvSmooth) {
_vertex[v].fromBuffer(flattenedAttribute, i + v);
}
else if (attributeName == 'normal') { // && params.normalSmooth) {
_position[v].fromBuffer(flatPosition, i + v);
final positionHash = hashFromVector(_position[v]);
final positions = hashToIndex[positionHash];
final k = positions.length;
final beta = 0.75 / k;
final startWeight = 1.0 - (beta * k);
_vertex[v].fromBuffer(flattenedAttribute, i + v);
_vertex[v].scale(startWeight);
positions.forEach((positionIndex){
_average.fromBuffer(flattenedAttribute, positionIndex);
_average.scale(beta);
_vertex[v].add(_average);
});
}
else { // 'position', 'color', etc...
_vertex[v].fromBuffer(flattenedAttribute, i + v);
_position[v].fromBuffer(flatPosition, i + v);
final positionHash = hashFromVector(_position[v]);
final neighbors = existingNeighbors[positionHash];
final opposites = flatOpposites[positionHash];
///// Adjust Source Vertex
if (neighbors != null) {
// Check Edges have even Opposite Points
if (params!.preserveEdges) {
final edgeSet = existingEdges[positionHash];
bool hasPair = true;
for (final edgeHash in edgeSet.keys) {
if (flatOpposites[edgeHash].length % 2 != 0) hasPair = false;
}
if (! hasPair) continue;
}
// Number of Neighbors
final k = neighbors.keys.length;
///// Loop's Formula
final beta = 1 / k * ((5/8) - math.pow((3/8) + (1/4) * math.cos(2 * math.pi / k), 2));
///// Warren's Formula
// final beta = (k > 3) ? 3 / (8 * k) : ((k == 3) ? 3 / 16 : 0);
///// Stevinz' Formula
// final beta = 0.5 / k;
///// Corners
final heavy = (1 / k) / k;
///// Interpolate Beta -> Heavy
final weight = lerp(heavy, beta, params.weight);
///// Average with Neighbors
final startWeight = 1.0 - (weight * k);
_vertex[v].scale(startWeight);
for (final neighborHash in neighbors.keys) {
final neighborIndices = neighbors[neighborHash];
_average.setValues(0, 0, 0);
for (int j = 0; j < neighborIndices.length; j++) {
_average.add(_temp.fromBuffer(existingAttribute, neighborIndices[j]));
}
_average.divideScalar(neighborIndices.length);
_average.scale(weight);
_vertex[v].add(_average);
}
}
else if (opposites != null && opposites.length == 2) {
final k = opposites.length;
const beta = 0.125; /* 1/8 */
final startWeight = 1.0 - (beta * k);
_vertex[v].scale(startWeight);
opposites.forEach((oppositeIndex){
_average.fromBuffer(existingAttribute, oppositeIndex);
_average.scale(beta);
_vertex[v].add(_average);
});
}
}
}
// Add New Triangle Position
setTriangle(floatArray, index, flattenedAttribute.itemSize, _vertex[0], _vertex[1], _vertex[2]);
index += (flattenedAttribute.itemSize * 3);
}
return floatArray;
}
///// Build Geometry, Set Attributes
attributeList.forEach((attributeName){
final existingAttribute = existing.getAttributeFromString(attributeName);
final flattenedAttribute = flat.getAttributeFromString(attributeName);
if (existingAttribute == null || flattenedAttribute == null) return;
final floatArray = subdivideAttribute(attributeName, existingAttribute, flattenedAttribute);
loop.setAttributeFromString(attributeName, Float32BufferAttribute.fromList(floatArray, flattenedAttribute.itemSize));
});
///// Morph Attributes
final morphAttributes = existing.morphAttributes;
for (final attributeName in morphAttributes.keys) {
final List<BufferAttribute> array = [];
final morphAttribute = morphAttributes[attributeName];
// Process Array of Float32BufferAttributes
for (int i = 0, l = (morphAttribute?.length ?? 0); i < l; i++) {
if (morphAttribute![i].count != vertexCount) continue;
final existingAttribute = morphAttribute[i];
final flattenedAttribute = LoopSubdivision.flatAttribute(morphAttribute[i], morphAttribute[i].count, params);
final floatArray = subdivideAttribute(attributeName, existingAttribute, flattenedAttribute);
array.add(Float32BufferAttribute.fromList(floatArray, flattenedAttribute.itemSize));
}
loop.morphAttributes[attributeName] = array;
}
loop.morphTargetsRelative = existing.morphTargetsRelative;
///// Clean Up
flat.dispose();
existing.dispose();
return loop;
}