UNPKG

@playcanvas/splat-transform

Version:

CLI tool for converting PLY gaussian splat scenes to compressed.ply

1,135 lines (1,124 loc) 32.4 kB
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