mergeSplats function

SplatCloud mergeSplats(
  1. List<SplatCloud> parts, {
  2. required Vector3 center,
  3. required double halfExtent,
  4. required int grid,
})

The splats of parts merged into at most grid³, one per occupied cell of a grid³ lattice over the cube of halfExtent-size halfExtent about center.

Each merged splat is the moment match of its cell: the mean and covariance of the cell's Gaussians weighted by opacity times footprint, the footprint being (s₀s₁s₂)^⅔, which is what a splat covers on screen averaged over the directions it could be seen from. Its colour is the same weighted mean, and its opacity is the cell's summed opacity-footprint spread over the merged splat's own footprint, capped at one — so a lone splat merges into itself, and many small faint ones into one larger one that is as opaque as they were together.

Implementation

SplatCloud mergeSplats(
  List<SplatCloud> parts, {
  required Vector3 center,
  required double halfExtent,
  required int grid,
}) {
  final cells = Float64List(grid * grid * grid * _kCell);
  final occupied = <int>[];
  final covariance = Float32List(6);
  final scale = halfExtent > 0 ? grid / (2 * halfExtent) : 0.0;
  int cellOf(double v, double at) =>
      ((v - at + halfExtent) * scale).floor().clamp(0, grid - 1);

  for (final part in parts) {
    for (var i = 0; i < part.count; i++) {
      final px = part.centers[i * 3] - center.x;
      final py = part.centers[i * 3 + 1] - center.y;
      final pz = part.centers[i * 3 + 2] - center.z;
      final cell =
          (cellOf(px, 0) * grid + cellOf(py, 0)) * grid + cellOf(pz, 0);
      final at = cell * _kCell;
      final footprint = math
          .pow(
            part.scales[i * 3] *
                part.scales[i * 3 + 1] *
                part.scales[i * 3 + 2],
            2 / 3,
          )
          .toDouble();
      final alpha = part.colors[i * 4 + 3];
      // Never zero, so a cell of fully transparent splats still has a place
      // and a shape — it merges into a transparent splat, not a division by
      // nothing.
      final w = math.max(alpha, 1e-6) * footprint + 1e-30;
      if (cells[at] == 0) occupied.add(cell);
      part.covarianceOf(i, covariance);
      cells[at] += w;
      cells[at + 1] += w * px;
      cells[at + 2] += w * py;
      cells[at + 3] += w * pz;
      cells[at + 4] += w * (covariance[0] + px * px);
      cells[at + 5] += w * (covariance[1] + px * py);
      cells[at + 6] += w * (covariance[2] + px * pz);
      cells[at + 7] += w * (covariance[3] + py * py);
      cells[at + 8] += w * (covariance[4] + py * pz);
      cells[at + 9] += w * (covariance[5] + pz * pz);
      cells[at + 10] += w * part.colors[i * 4];
      cells[at + 11] += w * part.colors[i * 4 + 1];
      cells[at + 12] += w * part.colors[i * 4 + 2];
      cells[at + 13] += alpha * footprint;
    }
  }

  occupied.sort();
  final n = occupied.length;
  final centers = Float32List(n * 3);
  final colors = Float32List(n * 4);
  final scales = Float32List(n * 3);
  final rotations = Float32List(n * 4);
  final sigma = Float64List(9);
  for (var k = 0; k < n; k++) {
    final at = occupied[k] * _kCell;
    final w = cells[at];
    final mx = cells[at + 1] / w;
    final my = cells[at + 2] / w;
    final mz = cells[at + 3] / w;
    // Second moment about the origin minus the mean's outer product: the
    // covariance about the mean. Computed about the node's centre, not the
    // world's, so the subtraction does not cancel away the precision.
    final xx = cells[at + 4] / w - mx * mx;
    final xy = cells[at + 5] / w - mx * my;
    final xz = cells[at + 6] / w - mx * mz;
    final yy = cells[at + 7] / w - my * my;
    final yz = cells[at + 8] / w - my * mz;
    final zz = cells[at + 9] / w - mz * mz;
    sigma
      ..[0] = xx
      ..[1] = xy
      ..[2] = xz
      ..[3] = xy
      ..[4] = yy
      ..[5] = yz
      ..[6] = xz
      ..[7] = yz
      ..[8] = zz;
    final SymmetricEigen(:values, :vectors) = symmetricEigen3(sigma);

    centers[k * 3] = mx + center.x;
    centers[k * 3 + 1] = my + center.y;
    centers[k * 3 + 2] = mz + center.z;
    var footprint = 1.0;
    for (var a = 0; a < 3; a++) {
      final s = math.sqrt(math.max(values[a], 1e-24));
      scales[k * 3 + a] = s;
      footprint *= s;
    }
    footprint = math.pow(footprint, 2 / 3).toDouble();
    final q = Quaternion.fromRotation(
      Matrix3(
        vectors[0],
        vectors[1],
        vectors[2], //
        vectors[3],
        vectors[4],
        vectors[5], //
        vectors[6],
        vectors[7],
        vectors[8],
      ),
    )..normalize();
    rotations
      ..[k * 4] = q.x
      ..[k * 4 + 1] = q.y
      ..[k * 4 + 2] = q.z
      ..[k * 4 + 3] = q.w;
    colors
      ..[k * 4] = cells[at + 10] / w
      ..[k * 4 + 1] = cells[at + 11] / w
      ..[k * 4 + 2] = cells[at + 12] / w
      ..[k * 4 + 3] = math.min(1.0, cells[at + 13] / footprint);
  }
  return SplatCloud(
    centers: centers,
    colors: colors,
    scales: scales,
    rotations: rotations,
  );
}