parseSplatPly function

SplatCloud parseSplatPly(
  1. Uint8List bytes
)

The cloud in bytes, or a thrown SplatPlyException saying why not.

Implementation

SplatCloud parseSplatPly(Uint8List bytes) {
  final header = _readHeader(bytes);
  final stride = header.stride;
  final count = header.count;
  final data = ByteData.view(
    bytes.buffer,
    bytes.offsetInBytes + header.dataStart,
    bytes.lengthInBytes - header.dataStart,
  );

  double read(_Property p, int row) {
    final at = row * stride + p.offset;
    return switch (p.type) {
      'float' || 'float32' => data.getFloat32(at, Endian.little),
      'double' || 'float64' => data.getFloat64(at, Endian.little),
      'char' || 'int8' => data.getInt8(at).toDouble(),
      'uchar' || 'uint8' => data.getUint8(at).toDouble(),
      'short' || 'int16' => data.getInt16(at, Endian.little).toDouble(),
      'ushort' || 'uint16' => data.getUint16(at, Endian.little).toDouble(),
      'int' || 'int32' => data.getInt32(at, Endian.little).toDouble(),
      'uint' || 'uint32' => data.getUint32(at, Endian.little).toDouble(),
      _ => throw SplatPlyException('unreadable scalar type ${p.type}'),
    };
  }

  _Property need(String name) {
    final found = header.properties[name];
    if (found == null) {
      throw SplatPlyException(
        'The vertex element has no `$name` property, so this is a point cloud '
        'rather than a fitted Gaussian cloud. A splat PLY carries x, y, z, '
        'f_dc_0..2, opacity, scale_0..2 and rot_0..3.',
      );
    }
    return found;
  }

  final x = need('x'), y = need('y'), z = need('z');
  final dc0 = need('f_dc_0'), dc1 = need('f_dc_1'), dc2 = need('f_dc_2');
  final opacity = need('opacity');
  final s0 = need('scale_0'), s1 = need('scale_1'), s2 = need('scale_2');
  final r0 = need('rot_0'), r1 = need('rot_1');
  final r2 = need('rot_2'), r3 = need('rot_3');

  if (header.dataStart + count * stride > bytes.lengthInBytes) {
    throw SplatPlyException(
      'The header claims $count vertices of $stride bytes, which runs past the '
      'end of a ${bytes.lengthInBytes}-byte file.',
    );
  }

  final centres = Float32List(count * 3);
  final colours = Float32List(count * 4);
  final scales = Float32List(count * 3);
  final rotations = Float32List(count * 4);

  for (var i = 0; i < count; i++) {
    centres[i * 3] = read(x, i);
    centres[i * 3 + 1] = read(y, i);
    centres[i * 3 + 2] = read(z, i);

    colours[i * 4] = splatChannel(read(dc0, i));
    colours[i * 4 + 1] = splatChannel(read(dc1, i));
    colours[i * 4 + 2] = splatChannel(read(dc2, i));
    colours[i * 4 + 3] = splatOpacity(read(opacity, i));

    scales[i * 3] = math.exp(read(s0, i));
    scales[i * 3 + 1] = math.exp(read(s1, i));
    scales[i * 3 + 2] = math.exp(read(s2, i));

    // **The file's quaternion is `w` first; this engine's is `w` last.** Not a
    // preference: `Quaternion` in `vector_math` takes xyzw, and a cloud read
    // in the file's order turns every splat by whatever rotation the
    // misreading happens to name — which looks like a capture full of streaks
    // rather than like a swapped component.
    final w = read(r0, i);
    final qx = read(r1, i);
    final qy = read(r2, i);
    final qz = read(r3, i);
    // Normalised on the way in: a trainer writes whatever it converged to, and
    // an unnormalised quaternion scales the ellipsoid as well as turning it.
    final length = math.sqrt(w * w + qx * qx + qy * qy + qz * qz);
    final scale = length > 1e-12 ? 1.0 / length : 0.0;
    rotations[i * 4] = qx * scale;
    rotations[i * 4 + 1] = qy * scale;
    rotations[i * 4 + 2] = qz * scale;
    // A zero-length quaternion is not a rotation; the identity is the only
    // answer that is not a division by nothing.
    rotations[i * 4 + 3] = length > 1e-12 ? w * scale : 1.0;
  }

  return SplatCloud(
    centres: centres,
    colours: colours,
    scales: scales,
    rotations: rotations,
  );
}