parseSplatPly function
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,
);
}