UNPKG

@babylonjs/loaders

Version:

For usage documentation please visit https://doc.babylonjs.com/features/featuresDeepDive/importers/loadingFileTypes/.

295 lines 12.9 kB
import { Scalar } from "@babylonjs/core/Maths/math.scalar.js"; const SH_C0 = 0.28209479177387814; async function LoadWebpImageData(rootUrlOrData, filename, engine) { const promise = new Promise((resolve, reject) => { const image = engine.createCanvasImage(); if (!image) { throw new Error("Failed to create ImageBitmap"); } image.onload = () => { try { // Draw to canvas const canvas = engine.createCanvas(image.width, image.height); if (!canvas) { throw new Error("Failed to create canvas"); } const ctx = canvas.getContext("2d"); if (!ctx) { throw new Error("Failed to get 2D context"); } ctx.drawImage(image, 0, 0); // Extract pixel data (RGBA per pixel) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); resolve({ bits: new Uint8Array(imageData.data.buffer), width: imageData.width }); } catch (error) { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(`Error loading image ${image.src} with exception: ${error}`); } }; image.onerror = (error) => { // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors reject(`Error loading image ${image.src} with exception: ${error}`); }; image.crossOrigin = "anonymous"; // To avoid CORS issues let objectUrl; if (typeof rootUrlOrData === "string") { // old behavior: URL + filename if (!filename) { throw new Error("filename is required when using a URL"); } image.src = rootUrlOrData + filename; } else { // new behavior: Uint8Array const blob = new Blob([rootUrlOrData], { type: "image/webp" }); objectUrl = URL.createObjectURL(blob); image.src = objectUrl; } }); return await promise; } async function ParseSogDatas(data, imageDataArrays, scene) { const splatCount = data.count ? data.count : data.means.shape[0]; const rowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // 32 const buffer = new ArrayBuffer(rowOutputLength * splatCount); const position = new Float32Array(buffer); const scale = new Float32Array(buffer); const rgba = new Uint8ClampedArray(buffer); const rot = new Uint8ClampedArray(buffer); // Undo the symmetric log transform used at encode time: const unlog = (n) => Math.sign(n) * (Math.exp(Math.abs(n)) - 1); const meansl = imageDataArrays[0].bits; const meansu = imageDataArrays[1].bits; // Check that data.means.mins is an array if (!Array.isArray(data.means.mins) || !Array.isArray(data.means.maxs)) { throw new Error("Missing arrays in SOG data."); } // --- Positions for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 3; j++) { const meansMin = data.means.mins[j]; const meansMax = data.means.maxs[j]; const meansup = meansu[index + j]; const meanslow = meansl[index + j]; const q = (meansup << 8) | meanslow; const n = Scalar.Lerp(meansMin, meansMax, q / 65535); position[i * 8 + j] = unlog(n); } } // --- Scales const scales = imageDataArrays[2].bits; if (data.version === 2) { if (!data.scales.codebook) { throw new Error("Missing codebook in SOG version 2 scales data."); } for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 3; j++) { const sc = data.scales.codebook[scales[index + j]]; const sce = Math.exp(sc); scale[i * 8 + 3 + j] = sce; } } } else { if (!Array.isArray(data.scales.mins) || !Array.isArray(data.scales.maxs)) { throw new Error("Missing arrays in SOG scales data."); } for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 3; j++) { const sc = scales[index + j]; const lsc = Scalar.Lerp(data.scales.mins[j], data.scales.maxs[j], sc / 255); const lsce = Math.exp(lsc); scale[i * 8 + 3 + j] = lsce; } } } // --- Colors/SH0 const colors = imageDataArrays[4].bits; if (data.version === 2) { if (!data.sh0.codebook) { throw new Error("Missing codebook in SOG version 2 sh0 data."); } for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 3; j++) { const component = 0.5 + data.sh0.codebook[colors[index + j]] * SH_C0; rgba[i * 32 + 24 + j] = Math.max(0, Math.min(255, Math.round(255 * component))); } rgba[i * 32 + 24 + 3] = colors[index + 3]; } } else { if (!Array.isArray(data.sh0.mins) || !Array.isArray(data.sh0.maxs)) { throw new Error("Missing arrays in SOG sh0 data."); } for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 4; j++) { const colorsMin = data.sh0.mins[j]; const colorsMax = data.sh0.maxs[j]; const colort = colors[index + j]; const c = Scalar.Lerp(colorsMin, colorsMax, colort / 255); let csh; if (j < 3) { csh = 0.5 + c * SH_C0; } else { csh = 1.0 / (1.0 + Math.exp(-c)); } rgba[i * 32 + 24 + j] = Math.max(0, Math.min(255, Math.round(255 * csh))); } } } // --- Rotations // Dequantize the stored three components: const toComp = (c) => ((c / 255 - 0.5) * 2.0) / Math.SQRT2; const quatArray = imageDataArrays[3].bits; for (let i = 0; i < splatCount; i++) { const quatsr = quatArray[i * 4 + 0]; const quatsg = quatArray[i * 4 + 1]; const quatsb = quatArray[i * 4 + 2]; const quatsa = quatArray[i * 4 + 3]; const a = toComp(quatsr); const b = toComp(quatsg); const c = toComp(quatsb); const mode = quatsa - 252; // 0..3 (R,G,B,A is one of the four components) // Reconstruct the omitted component so that ||q|| = 1 and w.l.o.g. the omitted one is non-negative const t = a * a + b * b + c * c; const d = Math.sqrt(Math.max(0, 1 - t)); // Place components according to mode let q; switch (mode) { case 0: q = [d, a, b, c]; break; // omitted = x case 1: q = [a, d, b, c]; break; // omitted = y case 2: q = [a, b, d, c]; break; // omitted = z case 3: q = [a, b, c, d]; break; // omitted = w default: throw new Error("Invalid quaternion mode"); } rot[i * 32 + 28 + 0] = q[0] * 127.5 + 127.5; rot[i * 32 + 28 + 1] = q[1] * 127.5 + 127.5; rot[i * 32 + 28 + 2] = q[2] * 127.5 + 127.5; rot[i * 32 + 28 + 3] = q[3] * 127.5 + 127.5; } // --- SH if (data.shN) { const coeffCounts = [0, 3, 8, 15]; const coeffs = data.shN.bands ? coeffCounts[data.shN.bands] : data.shN.shape[1] / 3; // 3 components per coeff const shCentroids = imageDataArrays[5].bits; const shLabelsData = imageDataArrays[6].bits; const shCentroidsWidth = imageDataArrays[5].width; const shComponentCount = coeffs * 3; const textureCount = Math.ceil(shComponentCount / 16); // 4 components can be stored per texture, 4 sh per component //let shIndexRead = byteOffset; // sh is an array of uint8array that will be used to create sh textures const sh = []; const engine = scene.getEngine(); const width = engine.getCaps().maxTextureSize; const height = Math.ceil(splatCount / width); // create array for the number of textures needed. for (let textureIndex = 0; textureIndex < textureCount; textureIndex++) { const texture = new Uint8Array(height * width * 4 * 4); // 4 components per texture, 4 sh per component sh.push(texture); } if (data.version === 2) { if (!data.shN.codebook) { throw new Error("Missing codebook in SOG version 2 shN data."); } for (let i = 0; i < splatCount; i++) { const n = shLabelsData[i * 4 + 0] + (shLabelsData[i * 4 + 1] << 8); const u = (n % 64) * coeffs; const v = Math.floor(n / 64); for (let k = 0; k < coeffs; k++) { for (let j = 0; j < 3; j++) { const shIndexWrite = k * 3 + j; const textureIndex = Math.floor(shIndexWrite / 16); const shArray = sh[textureIndex]; const byteIndexInTexture = shIndexWrite % 16; // [0..15] const offsetPerSplat = i * 16; // 16 sh values per texture per splat. const shValue = data.shN.codebook[shCentroids[(u + k) * 4 + j + v * shCentroidsWidth * 4]] * 127.5 + 127.5; shArray[byteIndexInTexture + offsetPerSplat] = Math.max(0, Math.min(255, shValue)); } } } } else { for (let i = 0; i < splatCount; i++) { const n = shLabelsData[i * 4 + 0] + (shLabelsData[i * 4 + 1] << 8); const u = (n % 64) * coeffs; const v = Math.floor(n / 64); const shMin = data.shN.mins; const shMax = data.shN.maxs; for (let j = 0; j < 3; j++) { for (let k = 0; k < coeffs / 3; k++) { const shIndexWrite = k * 3 + j; const textureIndex = Math.floor(shIndexWrite / 16); const shArray = sh[textureIndex]; const byteIndexInTexture = shIndexWrite % 16; // [0..15] const offsetPerSplat = i * 16; // 16 sh values per texture per splat. const shValue = Scalar.Lerp(shMin, shMax, shCentroids[(u + k) * 4 + j + v * shCentroidsWidth * 4] / 255) * 127.5 + 127.5; shArray[byteIndexInTexture + offsetPerSplat] = Math.max(0, Math.min(255, shValue)); } } } } return await new Promise((resolve) => { resolve({ mode: 0 /* Mode.Splat */, data: buffer, hasVertexColors: false, sh: sh }); }); } return await new Promise((resolve) => { resolve({ mode: 0 /* Mode.Splat */, data: buffer, hasVertexColors: false }); }); } /** * Parse SOG data from either a SOGRootData object (with webp files loaded from rootUrl) or from a Map of filenames to Uint8Array file data (including meta.json) * @param dataOrFiles Either the SOGRootData or a Map of filenames to Uint8Array file data (including meta.json) * @param rootUrl Base URL to load webp files from (if dataOrFiles is SOGRootData) * @param scene The Babylon.js scene * @returns Parsed data */ export async function ParseSogMeta(dataOrFiles, rootUrl, scene) { let data; let files; if (dataOrFiles instanceof Map) { files = dataOrFiles; const metaFile = files.get("meta.json"); if (!metaFile) { throw new Error("meta.json not found in files Map"); } data = JSON.parse(new TextDecoder().decode(metaFile)); } else { data = dataOrFiles; } // Collect all file names const urls = [...data.means.files, ...data.scales.files, ...data.quats.files, ...data.sh0.files]; if (data.shN) { urls.push(...data.shN.files); } // Load webp images in parallel const imageDataArrays = await Promise.all(urls.map(async (fileName) => { if (files && files.has(fileName)) { // load from in-memory Uint8Array const fileData = files.get(fileName); return await LoadWebpImageData(fileData, fileName, scene.getEngine()); } else { // fallback: load from URL return await LoadWebpImageData(rootUrl, fileName, scene.getEngine()); } })); return await ParseSogDatas(data, imageDataArrays, scene); } //# sourceMappingURL=sog.js.map