import type { GaussianAttributes } from '../models/ImageToGaussianModel' const TRIPOSPLAT_EXPORT_TRANSFORM = new Float32Array([ 1, 0, 0, 0, 0, -1, 0, 1, 0, ]) const PROPERTY_NAMES = [ 'x', 'y', 'z', 'nx', 'ny', 'nz', 'f_dc_0', 'f_dc_1', 'f_dc_2', 'opacity', 'scale_0', 'scale_1', 'scale_2', 'rot_0', 'rot_1', 'rot_2', 'rot_3', ] as const function clampProbability(value: number): number { return Math.min(1 - 1e-6, Math.max(1e-6, value)) } function validate(attributes: GaussianAttributes): number { const count = attributes.opacities.length if (attributes.positions.length !== count * 3) throw new Error('positions length must equal count * 3.') if (attributes.scales.length !== count * 3) throw new Error('scales length must equal count * 3.') if (attributes.rotations.length !== count * 4) throw new Error('rotations length must equal count * 4.') if (attributes.sh0.length !== count * 3) throw new Error('sh0 length must equal count * 3.') return count } function quaternionToMatrix(w: number, x: number, y: number, z: number): Float64Array { const inverseNorm = 1 / (Math.hypot(w, x, y, z) || 1) w *= inverseNorm x *= inverseNorm y *= inverseNorm z *= inverseNorm return new Float64Array([ 1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y), 2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x), 2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y), ]) } function leftMultiply3x3(left: Float32Array, right: Float64Array): Float64Array { const result = new Float64Array(9) for (let row = 0; row < 3; row += 1) { for (let column = 0; column < 3; column += 1) { result[row * 3 + column] = left[row * 3] * right[column] + left[row * 3 + 1] * right[3 + column] + left[row * 3 + 2] * right[6 + column] } } return result } function matrixToQuaternion(matrix: Float64Array): [number, number, number, number] { const m00 = matrix[0] const m11 = matrix[4] const m22 = matrix[8] const trace = m00 + m11 + m22 let w: number let x: number let y: number let z: number if (trace > 0) { const s = Math.sqrt(trace + 1) * 2 w = 0.25 * s x = (matrix[7] - matrix[5]) / s y = (matrix[2] - matrix[6]) / s z = (matrix[3] - matrix[1]) / s } else if (m00 >= m11 && m00 >= m22) { const s = Math.sqrt(Math.max(0, 1 + m00 - m11 - m22)) * 2 || 1 w = (matrix[7] - matrix[5]) / s x = 0.25 * s y = (matrix[1] + matrix[3]) / s z = (matrix[2] + matrix[6]) / s } else if (m11 >= m22) { const s = Math.sqrt(Math.max(0, 1 + m11 - m00 - m22)) * 2 || 1 w = (matrix[2] - matrix[6]) / s x = (matrix[1] + matrix[3]) / s y = 0.25 * s z = (matrix[5] + matrix[7]) / s } else { const s = Math.sqrt(Math.max(0, 1 + m22 - m00 - m11)) * 2 || 1 w = (matrix[3] - matrix[1]) / s x = (matrix[2] + matrix[6]) / s y = (matrix[5] + matrix[7]) / s z = 0.25 * s } const inverseNorm = 1 / (Math.hypot(w, x, y, z) || 1) return [w * inverseNorm, x * inverseNorm, y * inverseNorm, z * inverseNorm] } /** Standard binary 3DGS PLY matching TripoSplat's official field semantics. */ export function buildGaussianPly(attributes: GaussianAttributes): Uint8Array { const count = validate(attributes) const header = [ 'ply', 'format binary_little_endian 1.0', `element vertex ${count}`, ...PROPERTY_NAMES.map((name) => `property float ${name}`), 'end_header', '', ].join('\n') const headerBytes = new TextEncoder().encode(header) const stride = PROPERTY_NAMES.length * 4 const bytes = new Uint8Array(headerBytes.length + count * stride) bytes.set(headerBytes) const view = new DataView(bytes.buffer) let offset = headerBytes.length for (let index = 0; index < count; index += 1) { const index3 = index * 3 const index4 = index * 4 const px = attributes.positions[index3] const py = attributes.positions[index3 + 1] const pz = attributes.positions[index3 + 2] const rotation = matrixToQuaternion( leftMultiply3x3( TRIPOSPLAT_EXPORT_TRANSFORM, quaternionToMatrix( attributes.rotations[index4], attributes.rotations[index4 + 1], attributes.rotations[index4 + 2], attributes.rotations[index4 + 3], ), ), ) const opacity = clampProbability(attributes.opacities[index]) const values = [ px, -pz, py, 0, 0, 0, attributes.sh0[index3], attributes.sh0[index3 + 1], attributes.sh0[index3 + 2], Math.log(opacity / (1 - opacity)), Math.log(Math.max(attributes.scales[index3], 1e-12)), Math.log(Math.max(attributes.scales[index3 + 1], 1e-12)), Math.log(Math.max(attributes.scales[index3 + 2], 1e-12)), rotation[0], rotation[1], rotation[2], rotation[3], ] for (const value of values) { view.setFloat32(offset, value, true) offset += 4 } } return bytes }