import { Scene } from "../core/Scene"; import { Vector3 } from "../math/Vector3"; import { Quaternion } from "../math/Quaternion"; import { SplatData } from "../splats/SplatData"; import { Splat } from "../splats/Splat"; import { Converter } from "../utils/Converter"; import { initiateFetchRequest, loadRequestDataIntoBuffer } from "../utils/LoaderUtils"; class PLYLoader { static async LoadAsync( url: string, scene: Scene, onProgress?: (progress: number) => void, format: string = "", useCache: boolean = false, ): Promise { const res: Response = await initiateFetchRequest(url, useCache); const plyData = await loadRequestDataIntoBuffer(res, onProgress); if (plyData[0] !== 112 || plyData[1] !== 108 || plyData[2] !== 121 || plyData[3] !== 10) { throw new Error("Invalid PLY file"); } return this.LoadFromArrayBuffer(plyData.buffer, scene, format); } static async LoadFromFileAsync( file: File, scene: Scene, onProgress?: (progress: number) => void, format: string = "", ): Promise { const reader = new FileReader(); let splat = new Splat(); reader.onload = (e) => { splat = this.LoadFromArrayBuffer(e.target!.result as ArrayBuffer, scene, format); }; reader.onprogress = (e) => { onProgress?.(e.loaded / e.total); }; reader.readAsArrayBuffer(file); await new Promise((resolve) => { reader.onloadend = () => { resolve(); }; }); return splat; } static LoadFromArrayBuffer(arrayBuffer: ArrayBufferLike, scene: Scene, format: string = ""): Splat { const buffer = new Uint8Array(this._ParsePLYBuffer(arrayBuffer, format)); const data = SplatData.Deserialize(buffer); const splat = new Splat(data); scene.addObject(splat); return splat; } private static _ParsePLYBuffer(inputBuffer: ArrayBuffer, format: string): ArrayBuffer { type PlyProperty = { name: string; type: string; offset: number; }; const ubuf = new Uint8Array(inputBuffer); const headerText = new TextDecoder().decode(ubuf.slice(0, 1024 * 10)); const header_end = "end_header\n"; const header_end_index = headerText.indexOf(header_end); if (header_end_index < 0) throw new Error("Unable to read .ply file header"); const vertexCount = parseInt(/element vertex (\d+)\n/.exec(headerText)![1]); let rowOffset = 0; const offsets: Record = { double: 8, int: 4, uint: 4, float: 4, short: 2, ushort: 2, uchar: 1, }; const properties: PlyProperty[] = []; for (const prop of headerText .slice(0, header_end_index) .split("\n") .filter((k) => k.startsWith("property "))) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_p, type, name] = prop.split(" "); properties.push({ name, type, offset: rowOffset }); if (!offsets[type]) throw new Error(`Unsupported property type: ${type}`); rowOffset += offsets[type]; } const dataView = new DataView(inputBuffer, header_end_index + header_end.length); const buffer = new ArrayBuffer(SplatData.RowLength * vertexCount); const q_polycam = Quaternion.FromEuler(new Vector3(Math.PI / 2, 0, 0)); for (let i = 0; i < vertexCount; i++) { const position = new Float32Array(buffer, i * SplatData.RowLength, 3); const scale = new Float32Array(buffer, i * SplatData.RowLength + 12, 3); const rgba = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 24, 4); const rot = new Uint8ClampedArray(buffer, i * SplatData.RowLength + 28, 4); let r0: number = 255; let r1: number = 0; let r2: number = 0; let r3: number = 0; properties.forEach((property) => { let value; switch (property.type) { case "float": value = dataView.getFloat32(property.offset + i * rowOffset, true); break; case "int": value = dataView.getInt32(property.offset + i * rowOffset, true); break; default: throw new Error(`Unsupported property type: ${property.type}`); } switch (property.name) { case "x": position[0] = value; break; case "y": position[1] = value; break; case "z": position[2] = value; break; case "scale_0": case "scaling_0": scale[0] = Math.exp(value); break; case "scale_1": case "scaling_1": scale[1] = Math.exp(value); break; case "scale_2": case "scaling_2": scale[2] = Math.exp(value); break; case "red": rgba[0] = value; break; case "green": rgba[1] = value; break; case "blue": rgba[2] = value; break; case "f_dc_0": case "features_0": rgba[0] = (0.5 + Converter.SH_C0 * value) * 255; break; case "f_dc_1": case "features_1": rgba[1] = (0.5 + Converter.SH_C0 * value) * 255; break; case "f_dc_2": case "features_2": rgba[2] = (0.5 + Converter.SH_C0 * value) * 255; break; case "f_dc_3": rgba[3] = (0.5 + Converter.SH_C0 * value) * 255; break; case "opacity": case "opacity_0": rgba[3] = (1 / (1 + Math.exp(-value))) * 255; break; case "rot_0": case "rotation_0": r0 = value; break; case "rot_1": case "rotation_1": r1 = value; break; case "rot_2": case "rotation_2": r2 = value; break; case "rot_3": case "rotation_3": r3 = value; break; } }); let q = new Quaternion(r1, r2, r3, r0); switch (format) { case "polycam": { const temp = position[1]; position[1] = -position[2]; position[2] = temp; q = q_polycam.multiply(q); break; } case "": break; default: throw new Error(`Unsupported format: ${format}`); } q = q.normalize(); rot[0] = q.w * 128 + 128; rot[1] = q.x * 128 + 128; rot[2] = q.y * 128 + 128; rot[3] = q.z * 128 + 128; } return buffer; } } export { PLYLoader };