smooth static method

dynamic smooth(
  1. BufferGeometry geometry, [
  2. LoopParameters? params
])

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;
}