import { Camera } from "../cameras/Camera"; import type { Scene } from "../core/Scene"; import { Matrix3 } from "../math/Matrix3"; import { Quaternion } from "../math/Quaternion"; import { Vector3 } from "../math/Vector3"; import { Splatv } from "../splats/Splatv"; import { SplatvData } from "../splats/SplatvData"; import { initiateFetchRequest, loadRequestDataIntoBuffer } from "../utils/LoaderUtils"; class SplatvLoader { static async LoadAsync( url: string, scene: Scene, camera: Camera | null, onProgress?: (progress: number) => void, useCache: boolean = false, ): Promise { const res: Response = await initiateFetchRequest(url, useCache); const buffer = await loadRequestDataIntoBuffer(res, onProgress); return this._ParseSplatvBuffer(buffer.buffer, scene, camera); } static async LoadFromFileAsync( file: File, scene: Scene, camera: Camera | null, onProgress?: (progress: number) => void, ): Promise { const reader = new FileReader(); let splatv: Splatv | null = null; reader.onload = (e) => { splatv = this._ParseSplatvBuffer(e.target!.result as ArrayBuffer, scene, camera); }; reader.onprogress = (e) => { onProgress?.(e.loaded / e.total); }; reader.readAsArrayBuffer(file); await new Promise((resolve) => { reader.onloadend = () => { resolve(); }; }); if (!splatv) { throw new Error("Failed to load splatv file"); } return splatv; } private static _ParseSplatvBuffer(inputBuffer: ArrayBuffer, scene: Scene, camera: Camera | null): Splatv { let result: Splatv | null = null; const handleChunk = ( chunk: { size: number; type: string; texwidth: number; texheight: number }, buffer: Uint8Array, chunks: { size: number; type: string }[], ) => { if (chunk.type === "magic") { const intView = new Int32Array(buffer.buffer); if (intView[0] !== 0x674b) { throw new Error("Invalid splatv file"); } chunks.push({ size: intView[1], type: "chunks" }); } else if (chunk.type === "chunks") { const splatChunks = JSON.parse(new TextDecoder("utf-8").decode(buffer)); if (splatChunks.length == 0) { throw new Error("Invalid splatv file"); } else if (splatChunks.length > 1) { console.warn("Splatv file contains more than one chunk, only the first one will be loaded"); } const chunk = splatChunks[0]; const cameras = chunk.cameras as { position: number[]; rotation: number[][] }[]; if (camera && cameras && cameras.length) { const cameraData = cameras[0]; const position = new Vector3( cameraData.position[0], cameraData.position[1], cameraData.position[2], ); const rotation = Quaternion.FromMatrix3( new Matrix3( cameraData.rotation[0][0], cameraData.rotation[0][1], cameraData.rotation[0][2], cameraData.rotation[1][0], cameraData.rotation[1][1], cameraData.rotation[1][2], cameraData.rotation[2][0], cameraData.rotation[2][1], cameraData.rotation[2][2], ), ); camera.position = position; camera.rotation = rotation; } chunks.push(chunk); } else if (chunk.type === "splat") { const data = SplatvData.Deserialize(buffer, chunk.texwidth, chunk.texheight); const splatv = new Splatv(data); scene.addObject(splatv); result = splatv; } }; const ubuf = new Uint8Array(inputBuffer); const chunks: { size: number; type: string; texwidth: number; texheight: number }[] = [ { size: 8, type: "magic", texwidth: 0, texheight: 0 }, ]; let chunk: { size: number; type: string; texwidth: number; texheight: number } | undefined = chunks.shift(); let buffer = new Uint8Array(chunk!.size); let offset = 0; let inputOffset = 0; while (chunk) { while (offset < chunk.size) { const sizeToRead = Math.min(chunk.size - offset, ubuf.length - inputOffset); buffer.set(ubuf.subarray(inputOffset, inputOffset + sizeToRead), offset); offset += sizeToRead; inputOffset += sizeToRead; } handleChunk(chunk, buffer, chunks); if (result) { return result; } chunk = chunks.shift(); if (chunk) { buffer = new Uint8Array(chunk.size); offset = 0; } } throw new Error("Invalid splatv file"); } } export { SplatvLoader };