@playcanvas/splat-transform
Version:
CLI tool for converting PLY gaussian splat scenes to compressed.ply
1,135 lines (1,124 loc) • 32.4 kB
JavaScript
import { open } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
var version = "0.1.3";
const getDataType = (type) => {
switch (type) {
case 'char': return Int8Array;
case 'uchar': return Uint8Array;
case 'short': return Int16Array;
case 'ushort': return Uint16Array;
case 'int': return Int32Array;
case 'uint': return Uint32Array;
case 'float': return Float32Array;
case 'double': return Float64Array;
default: return null;
}
};
const calcDataSize = (plyFile) => {
let result = 0;
for (const element of plyFile.elements) {
for (const property of element.properties) {
result += getDataType(property.type).BYTES_PER_ELEMENT * element.count;
}
}
return result;
};
const shNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`);
// parse the ply header text and return an array of Element structures and a
// string containing the ply format
const parsePlyHeader = (data) => {
// decode header and split into lines
const strings = new TextDecoder('ascii')
.decode(data)
.split('\n')
.filter(line => line);
const elements = [];
let element;
for (let i = 1; i < strings.length; ++i) {
const words = strings[i].split(' ');
switch (words[0]) {
case 'ply':
case 'format':
case 'comment':
case 'end_header':
// skip
break;
case 'element': {
if (words.length !== 3) {
throw new Error('invalid ply header');
}
element = {
name: words[1],
count: parseInt(words[2], 10),
properties: []
};
elements.push(element);
break;
}
case 'property': {
if (!element || words.length !== 3 || !getDataType(words[1])) {
throw new Error('invalid ply header');
}
element.properties.push({
name: words[2],
type: words[1]
});
break;
}
default: {
throw new Error(`unrecognized header value '${words[0]}' in ply header`);
}
}
}
return { strings, elements };
};
const cmp = (a, b, aOffset = 0) => {
for (let i = 0; i < b.length; ++i) {
if (a[aOffset + i] !== b[i]) {
return false;
}
}
return true;
};
const magicBytes = new Uint8Array([112, 108, 121, 10]); // ply\n
const endHeaderBytes = new Uint8Array([10, 101, 110, 100, 95, 104, 101, 97, 100, 101, 114, 10]); // \nend_header\n
const readPly = async (fileHandle) => {
// we don't support ply text header larger than 128k
const headerBuf = Buffer.alloc(128 * 1024);
// smallest possible header size
let headerSize = magicBytes.length + endHeaderBytes.length;
if ((await fileHandle.read(headerBuf, 0, headerSize)).bytesRead !== headerSize) {
throw new Error('failed to read file header');
}
if (!cmp(headerBuf, magicBytes)) {
throw new Error('invalid file header');
}
// read the rest of the header till we find end header byte pattern
while (true) {
// read the next character
if ((await fileHandle.read(headerBuf, headerSize++, 1)).bytesRead !== 1) {
throw new Error('failed to read file header');
}
// check if we've reached the end of the header
if (cmp(headerBuf, endHeaderBytes, headerSize - endHeaderBytes.length)) {
break;
}
}
// parse the header
const header = parsePlyHeader(headerBuf.subarray(0, headerSize));
const dataSize = calcDataSize(header);
const data = Buffer.alloc(dataSize);
if ((await fileHandle.read(data, 0, dataSize)).bytesRead !== dataSize) {
throw new Error('failed reading ply data');
}
return { header, data };
};
// wraps ply file data and adds helpers accessors
class Splat {
plyFile;
vertex;
properties = {};
constructor(plyFile) {
this.plyFile = plyFile;
// find vertex element and populate property offsets
let offset = 0;
for (let i = 0; i < plyFile.header.elements.length; ++i) {
const element = plyFile.header.elements[i];
if (element.name === 'vertex') {
this.vertex = element;
}
for (let j = 0; j < element.properties.length; ++j) {
const property = element.properties[j];
if (element === this.vertex) {
this.properties[property.name] = {
type: property.type,
offset
};
}
offset += getDataType(property.type).BYTES_PER_ELEMENT;
}
}
}
// return the total number of splats
get numSplats() {
return this.vertex?.count;
}
// return the number of spherical harmonic bands present in the data
get numSHBands() {
return { '9': 1, '24': 2, '-1': 3 }[shNames.findIndex(v => !this.properties.hasOwnProperty(v))] ?? 0;
}
// simple iterator that assumes input data is float32
createIterator(fields, result) {
const offsets = fields.map(f => this.properties[f].offset / 4);
const float32 = new Float32Array(this.plyFile.data.buffer);
return (index) => {
const base = index * this.vertex.properties.length;
for (let i = 0; i < fields.length; ++i) {
result[i] = float32[base + offsets[i]];
}
};
}
}
const math = {
DEG_TO_RAD: Math.PI / 180,
RAD_TO_DEG: 180 / Math.PI,
clamp(value, min, max) {
if (value >= max) return max;
if (value <= min) return min;
return value;
},
intToBytes24(i) {
const r = i >> 16 & 0xff;
const g = i >> 8 & 0xff;
const b = i & 0xff;
return [r, g, b];
},
intToBytes32(i) {
const r = i >> 24 & 0xff;
const g = i >> 16 & 0xff;
const b = i >> 8 & 0xff;
const a = i & 0xff;
return [r, g, b, a];
},
bytesToInt24(r, g, b) {
if (r.length) {
b = r[2];
g = r[1];
r = r[0];
}
return r << 16 | g << 8 | b;
},
bytesToInt32(r, g, b, a) {
if (r.length) {
a = r[3];
b = r[2];
g = r[1];
r = r[0];
}
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
},
lerp(a, b, alpha) {
return a + (b - a) * math.clamp(alpha, 0, 1);
},
lerpAngle(a, b, alpha) {
if (b - a > 180) {
b -= 360;
}
if (b - a < -180) {
b += 360;
}
return math.lerp(a, b, math.clamp(alpha, 0, 1));
},
powerOfTwo(x) {
return x !== 0 && !(x & x - 1);
},
nextPowerOfTwo(val) {
val--;
val |= val >> 1;
val |= val >> 2;
val |= val >> 4;
val |= val >> 8;
val |= val >> 16;
val++;
return val;
},
nearestPowerOfTwo(val) {
return Math.pow(2, Math.round(Math.log(val) / Math.log(2)));
},
random(min, max) {
const diff = max - min;
return Math.random() * diff + min;
},
smoothstep(min, max, x) {
if (x <= min) return 0;
if (x >= max) return 1;
x = (x - min) / (max - min);
return x * x * (3 - 2 * x);
},
smootherstep(min, max, x) {
if (x <= min) return 0;
if (x >= max) return 1;
x = (x - min) / (max - min);
return x * x * x * (x * (x * 6 - 15) + 10);
},
roundUp(numToRound, multiple) {
if (multiple === 0) {
return numToRound;
}
return Math.ceil(numToRound / multiple) * multiple;
},
between(num, a, b, inclusive) {
const min = Math.min(a, b);
const max = Math.max(a, b);
return inclusive ? num >= min && num <= max : num > min && num < max;
}
};
var _Vec;
class Vec3 {
constructor(x = 0, y = 0, z = 0) {
this.x = void 0;
this.y = void 0;
this.z = void 0;
if (x.length === 3) {
this.x = x[0];
this.y = x[1];
this.z = x[2];
} else {
this.x = x;
this.y = y;
this.z = z;
}
}
add(rhs) {
this.x += rhs.x;
this.y += rhs.y;
this.z += rhs.z;
return this;
}
add2(lhs, rhs) {
this.x = lhs.x + rhs.x;
this.y = lhs.y + rhs.y;
this.z = lhs.z + rhs.z;
return this;
}
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
this.z += scalar;
return this;
}
addScaled(rhs, scalar) {
this.x += rhs.x * scalar;
this.y += rhs.y * scalar;
this.z += rhs.z * scalar;
return this;
}
clone() {
const cstr = this.constructor;
return new cstr(this.x, this.y, this.z);
}
copy(rhs) {
this.x = rhs.x;
this.y = rhs.y;
this.z = rhs.z;
return this;
}
cross(lhs, rhs) {
const lx = lhs.x;
const ly = lhs.y;
const lz = lhs.z;
const rx = rhs.x;
const ry = rhs.y;
const rz = rhs.z;
this.x = ly * rz - ry * lz;
this.y = lz * rx - rz * lx;
this.z = lx * ry - rx * ly;
return this;
}
distance(rhs) {
const x = this.x - rhs.x;
const y = this.y - rhs.y;
const z = this.z - rhs.z;
return Math.sqrt(x * x + y * y + z * z);
}
div(rhs) {
this.x /= rhs.x;
this.y /= rhs.y;
this.z /= rhs.z;
return this;
}
div2(lhs, rhs) {
this.x = lhs.x / rhs.x;
this.y = lhs.y / rhs.y;
this.z = lhs.z / rhs.z;
return this;
}
divScalar(scalar) {
this.x /= scalar;
this.y /= scalar;
this.z /= scalar;
return this;
}
dot(rhs) {
return this.x * rhs.x + this.y * rhs.y + this.z * rhs.z;
}
equals(rhs) {
return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z;
}
equalsApprox(rhs, epsilon = 1e-6) {
return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon;
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
lengthSq() {
return this.x * this.x + this.y * this.y + this.z * this.z;
}
lerp(lhs, rhs, alpha) {
this.x = lhs.x + alpha * (rhs.x - lhs.x);
this.y = lhs.y + alpha * (rhs.y - lhs.y);
this.z = lhs.z + alpha * (rhs.z - lhs.z);
return this;
}
mul(rhs) {
this.x *= rhs.x;
this.y *= rhs.y;
this.z *= rhs.z;
return this;
}
mul2(lhs, rhs) {
this.x = lhs.x * rhs.x;
this.y = lhs.y * rhs.y;
this.z = lhs.z * rhs.z;
return this;
}
mulScalar(scalar) {
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
return this;
}
normalize(src = this) {
const lengthSq = src.x * src.x + src.y * src.y + src.z * src.z;
if (lengthSq > 0) {
const invLength = 1 / Math.sqrt(lengthSq);
this.x = src.x * invLength;
this.y = src.y * invLength;
this.z = src.z * invLength;
}
return this;
}
floor(src = this) {
this.x = Math.floor(src.x);
this.y = Math.floor(src.y);
this.z = Math.floor(src.z);
return this;
}
ceil(src = this) {
this.x = Math.ceil(src.x);
this.y = Math.ceil(src.y);
this.z = Math.ceil(src.z);
return this;
}
round(src = this) {
this.x = Math.round(src.x);
this.y = Math.round(src.y);
this.z = Math.round(src.z);
return this;
}
min(rhs) {
if (rhs.x < this.x) this.x = rhs.x;
if (rhs.y < this.y) this.y = rhs.y;
if (rhs.z < this.z) this.z = rhs.z;
return this;
}
max(rhs) {
if (rhs.x > this.x) this.x = rhs.x;
if (rhs.y > this.y) this.y = rhs.y;
if (rhs.z > this.z) this.z = rhs.z;
return this;
}
project(rhs) {
const a_dot_b = this.x * rhs.x + this.y * rhs.y + this.z * rhs.z;
const b_dot_b = rhs.x * rhs.x + rhs.y * rhs.y + rhs.z * rhs.z;
const s = a_dot_b / b_dot_b;
this.x = rhs.x * s;
this.y = rhs.y * s;
this.z = rhs.z * s;
return this;
}
set(x, y, z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
sub(rhs) {
this.x -= rhs.x;
this.y -= rhs.y;
this.z -= rhs.z;
return this;
}
sub2(lhs, rhs) {
this.x = lhs.x - rhs.x;
this.y = lhs.y - rhs.y;
this.z = lhs.z - rhs.z;
return this;
}
subScalar(scalar) {
this.x -= scalar;
this.y -= scalar;
this.z -= scalar;
return this;
}
toString() {
return `[${this.x}, ${this.y}, ${this.z}]`;
}
}
_Vec = Vec3;
Vec3.ZERO = Object.freeze(new _Vec(0, 0, 0));
Vec3.HALF = Object.freeze(new _Vec(0.5, 0.5, 0.5));
Vec3.ONE = Object.freeze(new _Vec(1, 1, 1));
Vec3.UP = Object.freeze(new _Vec(0, 1, 0));
Vec3.DOWN = Object.freeze(new _Vec(0, -1, 0));
Vec3.RIGHT = Object.freeze(new _Vec(1, 0, 0));
Vec3.LEFT = Object.freeze(new _Vec(-1, 0, 0));
Vec3.FORWARD = Object.freeze(new _Vec(0, 0, -1));
Vec3.BACK = Object.freeze(new _Vec(0, 0, 1));
var _Quat;
class Quat {
constructor(x = 0, y = 0, z = 0, w = 1) {
this.x = void 0;
this.y = void 0;
this.z = void 0;
this.w = void 0;
if (x.length === 4) {
this.x = x[0];
this.y = x[1];
this.z = x[2];
this.w = x[3];
} else {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
}
clone() {
const cstr = this.constructor;
return new cstr(this.x, this.y, this.z, this.w);
}
conjugate(src = this) {
this.x = src.x * -1;
this.y = src.y * -1;
this.z = src.z * -1;
this.w = src.w;
return this;
}
copy(rhs) {
this.x = rhs.x;
this.y = rhs.y;
this.z = rhs.z;
this.w = rhs.w;
return this;
}
equals(rhs) {
return this.x === rhs.x && this.y === rhs.y && this.z === rhs.z && this.w === rhs.w;
}
equalsApprox(rhs, epsilon = 1e-6) {
return Math.abs(this.x - rhs.x) < epsilon && Math.abs(this.y - rhs.y) < epsilon && Math.abs(this.z - rhs.z) < epsilon && Math.abs(this.w - rhs.w) < epsilon;
}
getAxisAngle(axis) {
let rad = Math.acos(this.w) * 2;
const s = Math.sin(rad / 2);
if (s !== 0) {
axis.x = this.x / s;
axis.y = this.y / s;
axis.z = this.z / s;
if (axis.x < 0 || axis.y < 0 || axis.z < 0) {
axis.x *= -1;
axis.y *= -1;
axis.z *= -1;
rad *= -1;
}
} else {
axis.x = 1;
axis.y = 0;
axis.z = 0;
}
return rad * math.RAD_TO_DEG;
}
getEulerAngles(eulers = new Vec3()) {
let x, y, z;
const qx = this.x;
const qy = this.y;
const qz = this.z;
const qw = this.w;
const a2 = 2 * (qw * qy - qx * qz);
if (a2 <= -0.99999) {
x = 2 * Math.atan2(qx, qw);
y = -Math.PI / 2;
z = 0;
} else if (a2 >= 0.99999) {
x = 2 * Math.atan2(qx, qw);
y = Math.PI / 2;
z = 0;
} else {
x = Math.atan2(2 * (qw * qx + qy * qz), 1 - 2 * (qx * qx + qy * qy));
y = Math.asin(a2);
z = Math.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz));
}
return eulers.set(x, y, z).mulScalar(math.RAD_TO_DEG);
}
invert(src = this) {
return this.conjugate(src).normalize();
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
}
lengthSq() {
return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;
}
mul(rhs) {
const q1x = this.x;
const q1y = this.y;
const q1z = this.z;
const q1w = this.w;
const q2x = rhs.x;
const q2y = rhs.y;
const q2z = rhs.z;
const q2w = rhs.w;
this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y;
this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z;
this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x;
this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z;
return this;
}
mulScalar(scalar, src = this) {
this.x = src.x * scalar;
this.y = src.y * scalar;
this.z = src.z * scalar;
this.w = src.w * scalar;
return this;
}
mul2(lhs, rhs) {
const q1x = lhs.x;
const q1y = lhs.y;
const q1z = lhs.z;
const q1w = lhs.w;
const q2x = rhs.x;
const q2y = rhs.y;
const q2z = rhs.z;
const q2w = rhs.w;
this.x = q1w * q2x + q1x * q2w + q1y * q2z - q1z * q2y;
this.y = q1w * q2y + q1y * q2w + q1z * q2x - q1x * q2z;
this.z = q1w * q2z + q1z * q2w + q1x * q2y - q1y * q2x;
this.w = q1w * q2w - q1x * q2x - q1y * q2y - q1z * q2z;
return this;
}
normalize(src = this) {
let len = src.length();
if (len === 0) {
this.x = this.y = this.z = 0;
this.w = 1;
} else {
len = 1 / len;
this.x = src.x * len;
this.y = src.y * len;
this.z = src.z * len;
this.w = src.w * len;
}
return this;
}
set(x, y, z, w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
return this;
}
setFromAxisAngle(axis, angle) {
angle *= 0.5 * math.DEG_TO_RAD;
const sa = Math.sin(angle);
const ca = Math.cos(angle);
this.x = sa * axis.x;
this.y = sa * axis.y;
this.z = sa * axis.z;
this.w = ca;
return this;
}
setFromEulerAngles(ex, ey, ez) {
if (ex instanceof Vec3) {
const vec = ex;
ex = vec.x;
ey = vec.y;
ez = vec.z;
}
const halfToRad = 0.5 * math.DEG_TO_RAD;
ex *= halfToRad;
ey *= halfToRad;
ez *= halfToRad;
const sx = Math.sin(ex);
const cx = Math.cos(ex);
const sy = Math.sin(ey);
const cy = Math.cos(ey);
const sz = Math.sin(ez);
const cz = Math.cos(ez);
this.x = sx * cy * cz - cx * sy * sz;
this.y = cx * sy * cz + sx * cy * sz;
this.z = cx * cy * sz - sx * sy * cz;
this.w = cx * cy * cz + sx * sy * sz;
return this;
}
setFromMat4(m) {
const d = m.data;
let m00 = d[0];
let m01 = d[1];
let m02 = d[2];
let m10 = d[4];
let m11 = d[5];
let m12 = d[6];
let m20 = d[8];
let m21 = d[9];
let m22 = d[10];
let l;
l = m00 * m00 + m01 * m01 + m02 * m02;
if (l === 0) return this.set(0, 0, 0, 1);
l = 1 / Math.sqrt(l);
m00 *= l;
m01 *= l;
m02 *= l;
l = m10 * m10 + m11 * m11 + m12 * m12;
if (l === 0) return this.set(0, 0, 0, 1);
l = 1 / Math.sqrt(l);
m10 *= l;
m11 *= l;
m12 *= l;
l = m20 * m20 + m21 * m21 + m22 * m22;
if (l === 0) return this.set(0, 0, 0, 1);
l = 1 / Math.sqrt(l);
m20 *= l;
m21 *= l;
m22 *= l;
if (m22 < 0) {
if (m00 > m11) {
this.set(1 + m00 - m11 - m22, m01 + m10, m20 + m02, m12 - m21);
} else {
this.set(m01 + m10, 1 - m00 + m11 - m22, m12 + m21, m20 - m02);
}
} else {
if (m00 < -m11) {
this.set(m20 + m02, m12 + m21, 1 - m00 - m11 + m22, m01 - m10);
} else {
this.set(m12 - m21, m20 - m02, m01 - m10, 1 + m00 + m11 + m22);
}
}
return this.mulScalar(1.0 / this.length());
}
setFromDirections(from, to) {
const dotProduct = 1 + from.dot(to);
if (dotProduct < Number.EPSILON) {
if (Math.abs(from.x) > Math.abs(from.y)) {
this.x = -from.z;
this.y = 0;
this.z = from.x;
this.w = 0;
} else {
this.x = 0;
this.y = -from.z;
this.z = from.y;
this.w = 0;
}
} else {
this.x = from.y * to.z - from.z * to.y;
this.y = from.z * to.x - from.x * to.z;
this.z = from.x * to.y - from.y * to.x;
this.w = dotProduct;
}
return this.normalize();
}
slerp(lhs, rhs, alpha) {
const lx = lhs.x;
const ly = lhs.y;
const lz = lhs.z;
const lw = lhs.w;
let rx = rhs.x;
let ry = rhs.y;
let rz = rhs.z;
let rw = rhs.w;
let cosHalfTheta = lw * rw + lx * rx + ly * ry + lz * rz;
if (cosHalfTheta < 0) {
rw = -rw;
rx = -rx;
ry = -ry;
rz = -rz;
cosHalfTheta = -cosHalfTheta;
}
if (Math.abs(cosHalfTheta) >= 1) {
this.w = lw;
this.x = lx;
this.y = ly;
this.z = lz;
return this;
}
const halfTheta = Math.acos(cosHalfTheta);
const sinHalfTheta = Math.sqrt(1 - cosHalfTheta * cosHalfTheta);
if (Math.abs(sinHalfTheta) < 0.001) {
this.w = lw * 0.5 + rw * 0.5;
this.x = lx * 0.5 + rx * 0.5;
this.y = ly * 0.5 + ry * 0.5;
this.z = lz * 0.5 + rz * 0.5;
return this;
}
const ratioA = Math.sin((1 - alpha) * halfTheta) / sinHalfTheta;
const ratioB = Math.sin(alpha * halfTheta) / sinHalfTheta;
this.w = lw * ratioA + rw * ratioB;
this.x = lx * ratioA + rx * ratioB;
this.y = ly * ratioA + ry * ratioB;
this.z = lz * ratioA + rz * ratioB;
return this;
}
transformVector(vec, res = new Vec3()) {
const x = vec.x,
y = vec.y,
z = vec.z;
const qx = this.x,
qy = this.y,
qz = this.z,
qw = this.w;
const ix = qw * x + qy * z - qz * y;
const iy = qw * y + qz * x - qx * z;
const iz = qw * z + qx * y - qy * x;
const iw = -qx * x - qy * y - qz * z;
res.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
res.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
res.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
return res;
}
toString() {
return `[${this.x}, ${this.y}, ${this.z}, ${this.w}]`;
}
}
_Quat = Quat;
Quat.IDENTITY = Object.freeze(new _Quat(0, 0, 0, 1));
Quat.ZERO = Object.freeze(new _Quat(0, 0, 0, 0));
const generatedByString = `Generated by splat-transform ${version}`;
const shBandCoeffs = [0, 3, 8, 15];
const q = new Quat();
// process and compress a chunk of 256 splats
class Chunk {
static members = [
'x', 'y', 'z',
'scale_0', 'scale_1', 'scale_2',
'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity',
'rot_0', 'rot_1', 'rot_2', 'rot_3'
];
size;
data = {};
// compressed data
chunkData;
position;
rotation;
scale;
color;
constructor(size = 256) {
this.size = size;
Chunk.members.forEach((m) => {
this.data[m] = new Float32Array(size);
});
this.chunkData = new Float32Array(18);
this.position = new Uint32Array(size);
this.rotation = new Uint32Array(size);
this.scale = new Uint32Array(size);
this.color = new Uint32Array(size);
}
set(index, singleSplat) {
for (let i = 0; i < Chunk.members.length; ++i) {
this.data[Chunk.members[i]][index] = singleSplat[i];
}
}
pack() {
const calcMinMax = (data) => {
let min;
let max;
min = max = data[0];
for (let i = 1; i < data.length; ++i) {
const v = data[i];
min = Math.min(min, v);
max = Math.max(max, v);
}
return { min, max };
};
const normalize = (x, min, max) => {
if (x <= min)
return 0;
if (x >= max)
return 1;
return (max - min < 0.00001) ? 0 : (x - min) / (max - min);
};
const data = this.data;
const x = data.x;
const y = data.y;
const z = data.z;
const scale_0 = data.scale_0;
const scale_1 = data.scale_1;
const scale_2 = data.scale_2;
const rot_0 = data.rot_0;
const rot_1 = data.rot_1;
const rot_2 = data.rot_2;
const rot_3 = data.rot_3;
const f_dc_0 = data.f_dc_0;
const f_dc_1 = data.f_dc_1;
const f_dc_2 = data.f_dc_2;
const opacity = data.opacity;
const px = calcMinMax(x);
const py = calcMinMax(y);
const pz = calcMinMax(z);
const sx = calcMinMax(scale_0);
const sy = calcMinMax(scale_1);
const sz = calcMinMax(scale_2);
// clamp scale because sometimes values are at infinity
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
sx.min = clamp(sx.min, -20, 20);
sx.max = clamp(sx.max, -20, 20);
sy.min = clamp(sy.min, -20, 20);
sy.max = clamp(sy.max, -20, 20);
sz.min = clamp(sz.min, -20, 20);
sz.max = clamp(sz.max, -20, 20);
// convert f_dc_ to colors before calculating min/max and packaging
const SH_C0 = 0.28209479177387814;
for (let i = 0; i < f_dc_0.length; ++i) {
f_dc_0[i] = f_dc_0[i] * SH_C0 + 0.5;
f_dc_1[i] = f_dc_1[i] * SH_C0 + 0.5;
f_dc_2[i] = f_dc_2[i] * SH_C0 + 0.5;
}
const cr = calcMinMax(f_dc_0);
const cg = calcMinMax(f_dc_1);
const cb = calcMinMax(f_dc_2);
const packUnorm = (value, bits) => {
const t = (1 << bits) - 1;
return Math.max(0, Math.min(t, Math.floor(value * t + 0.5)));
};
const pack111011 = (x, y, z) => {
return packUnorm(x, 11) << 21 |
packUnorm(y, 10) << 11 |
packUnorm(z, 11);
};
const pack8888 = (x, y, z, w) => {
return packUnorm(x, 8) << 24 |
packUnorm(y, 8) << 16 |
packUnorm(z, 8) << 8 |
packUnorm(w, 8);
};
// pack quaternion into 2,10,10,10
const packRot = (x, y, z, w) => {
q.set(x, y, z, w).normalize();
const a = [q.x, q.y, q.z, q.w];
const largest = a.reduce((curr, v, i) => (Math.abs(v) > Math.abs(a[curr]) ? i : curr), 0);
if (a[largest] < 0) {
a[0] = -a[0];
a[1] = -a[1];
a[2] = -a[2];
a[3] = -a[3];
}
const norm = Math.sqrt(2) * 0.5;
let result = largest;
for (let i = 0; i < 4; ++i) {
if (i !== largest) {
result = (result << 10) | packUnorm(a[i] * norm + 0.5, 10);
}
}
return result;
};
// pack
for (let i = 0; i < this.size; ++i) {
this.position[i] = pack111011(normalize(x[i], px.min, px.max), normalize(y[i], py.min, py.max), normalize(z[i], pz.min, pz.max));
this.rotation[i] = packRot(rot_0[i], rot_1[i], rot_2[i], rot_3[i]);
this.scale[i] = pack111011(normalize(scale_0[i], sx.min, sx.max), normalize(scale_1[i], sy.min, sy.max), normalize(scale_2[i], sz.min, sz.max));
this.color[i] = pack8888(normalize(f_dc_0[i], cr.min, cr.max), normalize(f_dc_1[i], cg.min, cg.max), normalize(f_dc_2[i], cb.min, cb.max), 1 / (1 + Math.exp(-opacity[i])));
}
this.chunkData.set([
px.min, py.min, pz.min, px.max, py.max, pz.max,
sx.min, sy.min, sz.min, sx.max, sy.max, sz.max,
cr.min, cg.min, cb.min, cr.max, cg.max, cb.max
], 0);
}
}
// sort the compressed indices into morton order
const sortSplats = (splat, indices) => {
// https://fgiesen.wordpress.com/2009/12/13/decoding-morton-codes/
const encodeMorton3 = (x, y, z) => {
const Part1By2 = (x) => {
x &= 0x000003ff;
x = (x ^ (x << 16)) & 0xff0000ff;
x = (x ^ (x << 8)) & 0x0300f00f;
x = (x ^ (x << 4)) & 0x030c30c3;
x = (x ^ (x << 2)) & 0x09249249;
return x;
};
return (Part1By2(z) << 2) + (Part1By2(y) << 1) + Part1By2(x);
};
let minx;
let miny;
let minz;
let maxx;
let maxy;
let maxz;
const vertex = [0, 0, 0];
const it = splat.createIterator(['x', 'y', 'z'], vertex);
// calculate scene extents across all splats (using sort centers, because they're in world space)
for (let i = 0; i < splat.numSplats; ++i) {
it(i);
const x = vertex[0];
const y = vertex[1];
const z = vertex[2];
if (minx === undefined) {
minx = maxx = x;
miny = maxy = y;
minz = maxz = z;
}
else {
if (x < minx)
minx = x;
else if (x > maxx)
maxx = x;
if (y < miny)
miny = y;
else if (y > maxy)
maxy = y;
if (z < minz)
minz = z;
else if (z > maxz)
maxz = z;
}
}
const xlen = maxx - minx;
const ylen = maxy - miny;
const zlen = maxz - minz;
const morton = new Uint32Array(indices.length);
let idx = 0;
for (let i = 0; i < splat.numSplats; ++i) {
it(i);
const x = vertex[0];
const y = vertex[1];
const z = vertex[2];
const ix = Math.floor(1024 * (x - minx) / xlen);
const iy = Math.floor(1024 * (y - miny) / ylen);
const iz = Math.floor(1024 * (z - minz) / zlen);
morton[idx++] = encodeMorton3(ix, iy, iz);
}
// order splats by morton code
indices.sort((a, b) => morton[a.globalIndex] - morton[b.globalIndex]);
};
const chunkProps = [
'min_x', 'min_y', 'min_z',
'max_x', 'max_y', 'max_z',
'min_scale_x', 'min_scale_y', 'min_scale_z',
'max_scale_x', 'max_scale_y', 'max_scale_z',
'min_r', 'min_g', 'min_b',
'max_r', 'max_g', 'max_b'
];
const vertexProps = [
'packed_position',
'packed_rotation',
'packed_scale',
'packed_color'
];
const writeCompressedPly = async (fileHandle, splat) => {
// make a list of indices spanning all splats (so we can sort them together)
const indices = [];
for (let i = 0; i < splat.numSplats; ++i) {
indices.push({ splatIndex: 0, i, globalIndex: indices.length });
}
if (indices.length === 0) {
throw new Error('no splats to write');
}
const numSplats = indices.length;
const numChunks = Math.ceil(numSplats / 256);
const outputSHBands = splat.numSHBands;
const outputSHCoeffs = shBandCoeffs[outputSHBands];
const shHeader = outputSHBands ? [
`element sh ${numSplats}`,
new Array(outputSHCoeffs * 3).fill('').map((_, i) => `property uchar f_rest_${i}`)
].flat() : [];
const headerText = [
'ply',
'format binary_little_endian 1.0',
`comment ${generatedByString}`,
`element chunk ${numChunks}`,
chunkProps.map(p => `property float ${p}`),
`element vertex ${numSplats}`,
vertexProps.map(p => `property uint ${p}`),
shHeader,
'end_header\n'
].flat().join('\n');
const header = (new TextEncoder()).encode(headerText);
const chunkData = new Float32Array(numChunks * chunkProps.length);
const splatData = new Uint32Array(numSplats * vertexProps.length);
const shData = new Uint8Array(numSplats * outputSHCoeffs * 3);
// sort splats into some kind of order (morton order rn)
sortSplats(splat, indices);
const singleSplat = Chunk.members.map(_ => 0);
const it = splat.createIterator(Chunk.members, singleSplat);
const shMembers = shNames.slice(0, outputSHCoeffs * 3);
const singleSH = shMembers.map(_ => 0);
const shIt = splat.createIterator(shMembers, singleSH);
const chunk = new Chunk();
for (let i = 0; i < numChunks; ++i) {
const num = Math.min(numSplats, (i + 1) * 256) - i * 256;
for (let j = 0; j < num; ++j) {
const index = indices[i * 256 + j];
// read splat data
it(index.i);
// update chunk
chunk.set(j, singleSplat);
shIt(index.i);
// quantize and write sh data
let off = (i * 256 + j) * outputSHCoeffs * 3;
for (let k = 0; k < outputSHCoeffs * 3; ++k) {
const nvalue = singleSH[k] / 8 + 0.5;
shData[off++] = Math.max(0, Math.min(255, Math.trunc(nvalue * 256)));
}
}
// pack the chunk
chunk.pack();
// store the float data
chunkData.set(chunk.chunkData, i * 18);
// write packed bits
const offset = i * 256 * 4;
for (let j = 0; j < num; ++j) {
splatData[offset + j * 4 + 0] = chunk.position[j];
splatData[offset + j * 4 + 1] = chunk.rotation[j];
splatData[offset + j * 4 + 2] = chunk.scale[j];
splatData[offset + j * 4 + 3] = chunk.color[j];
}
}
await fileHandle.write(header);
await fileHandle.write(new Uint8Array(chunkData.buffer));
await fileHandle.write(new Uint8Array(splatData.buffer));
await fileHandle.write(shData);
};
const readData = async (filename) => {
// open input
console.log(`loading '${filename}'...`);
const inputFile = await open(filename, 'r');
// read contents
console.log(`reading contents...`);
const plyFile = await readPly(inputFile);
// close file
await inputFile.close();
return plyFile;
};
const processData = (plyFile) => {
// check we have the necessary elements for processing
};
const writeData = async (filename, plyFile) => {
const outputFile = await open(filename, 'w');
await writeCompressedPly(outputFile, new Splat(plyFile));
await outputFile.close();
};
const main = async () => {
console.log(`splat-transform v${version}`);
if (process.argv.length < 3) {
console.error('Usage: splat-transform <input-file> <output-file>');
process.exit(1);
}
const inputFilename = process.argv[2];
const outputFilename = process.argv[3];
try {
// open input
const plyFile = await readData(inputFilename);
// process
processData(plyFile);
// write
await writeData(outputFilename, plyFile);
}
catch (err) {
// handle errors
console.error(`error: ${err.message}`);
process.exit(1);
}
console.log('done');
};
export { main };
//# sourceMappingURL=index.mjs.map