UNPKG

@babylonjs/viewer

Version:

The Babylon Viewer aims to simplify a specific but common Babylon.js use case: loading, viewing, and interacting with a 3D model.

1,079 lines (1,072 loc) 294 kB
import { cq as RegisterGaussianSplattingPartProxyMesh, cr as RegisterGaussianSplattingMesh, cs as RegisterCamera, aV as Mesh, i as Color3, b1 as Color4, c as ShaderMaterial, a7 as VertexBuffer, F as Material, aE as _MissingSideEffect, aW as VertexData, V as Vector3, y as Logger, ct as _CreationDataStorage, aH as RawTexture, C as Constants, cu as Scalar, br as AllocateShBuffers, aY as Tools, be as MultiRenderTarget, bc as Vector4, bq as GaussianSplattingMesh, cv as Plane, M as Matrix, aU as Camera, bl as BoundingInfo, cw as Frustum, b3 as Vector2, T as TmpVectors, aD as Quaternion, a as EngineStore, ca as RandomRange, p as BaseTexture, ci as IsWindowObjectExist, cx as runCoroutineAsync, cy as createYieldingScheduler, cz as SPLATFileLoaderMetadata, bn as RegisterSceneLoaderPlugin, cA as ThinEngine, w as InternalTexture, cB as GetExponentOfTwo } from './index-MZPybX0H.esm.js'; import './thinInstanceMesh-Dqmzprij.esm.js'; import './tools-DC6rsfz_.esm.js'; import { I as InstancedMesh, A as AssetContainer } from './assetContainer-Bm0vsreJ.esm.js'; import './buffer-DweiwKKq.esm.js'; import './shaderMaterial-fmN6xXWd.esm.js'; import { R as Ray } from './ray.core-DJg2QKb7.esm.js'; import { S as StandardMaterial } from './standardMaterial.pure-D4Vk5EeI.esm.js'; import './prepass.defines-D50C_zO6.esm.js'; import './material.detailMapConfiguration-CHgbrJ-3.esm.js'; /** * Re-exports pure implementation and applies runtime side effects. * Import gaussianSplattingPartProxyMesh.pure for tree-shakeable, side-effect-free usage. */ RegisterGaussianSplattingPartProxyMesh(); /** * Re-exports pure implementation and applies runtime side effects. * Import gaussianSplattingMesh.pure for tree-shakeable, side-effect-free usage. */ RegisterGaussianSplattingMesh(); /** * Re-exports pure implementation and applies runtime side effects. * Import camera.pure for tree-shakeable, side-effect-free usage. */ RegisterCamera(); /** This file must only contain pure code and pure imports */ /** * Line mesh * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param */ class LinesMesh extends Mesh { _isShaderMaterial(shader) { if (!shader) { return false; } return shader.getClassName() === "ShaderMaterial"; } /** * Creates a new LinesMesh * @param name defines the name * @param scene defines the hosting scene * @param parent defines the parent mesh if any * @param source defines the optional source LinesMesh used to clone data from * @param doNotCloneChildren When cloning, skip cloning child meshes of source, default False. * When false, achieved by calling a clone(), also passing False. * This will make creation of children, recursive. * @param useVertexColor defines if this LinesMesh supports vertex color * @param useVertexAlpha defines if this LinesMesh supports vertex alpha * @param material material to use to draw the line. If not provided, will create a new one */ constructor(name, scene = null, parent = null, source = null, doNotCloneChildren, /** * If vertex color should be applied to the mesh */ useVertexColor, /** * If vertex alpha should be applied to the mesh */ useVertexAlpha, material) { super(name, scene, parent, source, doNotCloneChildren); this.useVertexColor = useVertexColor; this.useVertexAlpha = useVertexAlpha; /** * Color of the line (Default: White) */ this.color = new Color3(1, 1, 1); /** * Alpha of the line (Default: 1) */ this.alpha = 1; /** Shader language used by the material */ this._shaderLanguage = 0 /* ShaderLanguage.GLSL */; this._ownsMaterial = false; if (source) { this.color = source.color.clone(); this.alpha = source.alpha; this.useVertexColor = source.useVertexColor; this.useVertexAlpha = source.useVertexAlpha; } this.intersectionThreshold = 0.1; const defines = []; const options = { attributes: [VertexBuffer.PositionKind], uniforms: ["world", "viewProjection"], needAlphaBlending: true, defines: defines, useClipPlane: null, shaderLanguage: 0 /* ShaderLanguage.GLSL */, }; if (!this.useVertexAlpha) { options.needAlphaBlending = false; } else { options.defines.push("#define VERTEXALPHA"); } if (!this.useVertexColor) { options.uniforms.push("color"); this._color4 = new Color4(); } else { options.defines.push("#define VERTEXCOLOR"); options.attributes.push(VertexBuffer.ColorKind); } if (material) { this.material = material; } else { const engine = this.getScene().getEngine(); if (engine.isWebGPU && !LinesMesh.ForceGLSL) { this._shaderLanguage = 1 /* ShaderLanguage.WGSL */; } options.shaderLanguage = this._shaderLanguage; options.extraInitializationsAsync = async () => { if (this._shaderLanguage === 1 /* ShaderLanguage.WGSL */) { await Promise.all([import('./color.vertex-CN7ukM8n.esm.js'), import('./color.fragment-DK1mmbTU.esm.js')]); } else { await Promise.all([import('./color.vertex-DjhpY8QM.esm.js'), import('./color.fragment-BqGox7i0.esm.js')]); } }; const material = new ShaderMaterial("colorShader", this.getScene(), "color", options, false); material.doNotSerialize = true; this._ownsMaterial = true; this._setInternalMaterial(material); } } /** * @returns the string "LineMesh" */ getClassName() { return "LinesMesh"; } /** * @internal */ get material() { return this._internalAbstractMeshDataInfo._material; } /** * @internal */ set material(value) { const currentMaterial = this.material; if (currentMaterial === value) { return; } const shouldDispose = currentMaterial && this._ownsMaterial; this._ownsMaterial = false; this._setInternalMaterial(value); if (shouldDispose) { currentMaterial?.dispose(); } } _setInternalMaterial(material) { this._setMaterial(material); if (this.material) { this.material.fillMode = Material.LineListDrawMode; this.material.disableLighting = true; } } /** * @internal */ get checkCollisions() { return false; } set checkCollisions(value) { // Just ignore it } /** * @internal */ _bind(_subMesh, colorEffect) { if (!this._geometry) { return this; } // VBOs const indexToBind = this.isUnIndexed ? null : this._geometry.getIndexBuffer(); if (!this._userInstancedBuffersStorage || this.hasThinInstances) { this._geometry._bind(colorEffect, indexToBind); } else { this._geometry._bind(colorEffect, indexToBind, this._userInstancedBuffersStorage.vertexBuffers, this._userInstancedBuffersStorage.vertexArrayObjects); } // Color if (!this.useVertexColor && this._isShaderMaterial(this.material)) { const { r, g, b } = this.color; this._color4.set(r, g, b, this.alpha); this.material.setColor4("color", this._color4); } return this; } /** * @internal */ _draw(subMesh, fillMode, instancesCount) { if (!this._geometry || !this._geometry.getVertexBuffers() || (!this._unIndexed && !this._geometry.getIndexBuffer())) { return this; } const engine = this.getScene().getEngine(); // Draw order if (this._unIndexed) { engine.drawArraysType(Material.LineListDrawMode, subMesh.verticesStart, subMesh.verticesCount, instancesCount); } else { engine.drawElementsType(Material.LineListDrawMode, subMesh.indexStart, subMesh.indexCount, instancesCount); } return this; } /** * Disposes of the line mesh (this disposes of the automatically created material if not instructed otherwise). * @param doNotRecurse If children should be disposed * @param disposeMaterialAndTextures This parameter is used to force disposing the material in case it is not the default one * @param doNotDisposeMaterial If the material should not be disposed (default: false, meaning the material might be disposed) */ dispose(doNotRecurse, disposeMaterialAndTextures = false, doNotDisposeMaterial) { if (!doNotDisposeMaterial) { if (this._ownsMaterial) { this.material?.dispose(false, false, true); } else if (disposeMaterialAndTextures) { this.material?.dispose(false, false, true); } } super.dispose(doNotRecurse); } /** * Returns a new LineMesh object cloned from the current one. * @param name defines the cloned mesh name * @param newParent defines the new mesh parent * @param doNotCloneChildren if set to true, none of the mesh children are cloned (false by default) * @returns the new mesh */ clone(name, newParent = null, doNotCloneChildren) { if (newParent && newParent._addToSceneRootNodes === undefined) { const createOptions = newParent; createOptions.source = this; return new LinesMesh(name, this.getScene(), createOptions.parent, createOptions.source, createOptions.doNotCloneChildren); } return new LinesMesh(name, this.getScene(), newParent, this, doNotCloneChildren); } /** * Creates a new InstancedLinesMesh object from the mesh model. * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/copies/instances * @param name defines the name of the new instance * @returns a new InstancedLinesMesh */ createInstance(name) { const instance = new InstancedLinesMesh(name, this); if (this.instancedBuffers) { instance.instancedBuffers = {}; for (const key in this.instancedBuffers) { instance.instancedBuffers[key] = this.instancedBuffers[key]; } } return instance; } /** * Serializes this ground mesh * @param serializationObject object to write serialization to */ serialize(serializationObject) { super.serialize(serializationObject); serializationObject.color = this.color.asArray(); serializationObject.alpha = this.alpha; } /** * Parses a serialized ground mesh * @param parsedMesh the serialized mesh * @param scene the scene to create the ground mesh in * @returns the created ground mesh */ static Parse(parsedMesh, scene) { const result = new LinesMesh(parsedMesh.name, scene); result.color = Color3.FromArray(parsedMesh.color); result.alpha = parsedMesh.alpha; return result; } } /** * Force all the LineMeshes to compile their default color material to glsl even on WebGPU engines. * False by default. This is mostly meant for backward compatibility. */ LinesMesh.ForceGLSL = false; /** * Creates an instance based on a source LinesMesh */ class InstancedLinesMesh extends InstancedMesh { constructor(name, source) { super(name, source); this.intersectionThreshold = source.intersectionThreshold; } /** * @returns the string "InstancedLinesMesh". */ getClassName() { return "InstancedLinesMesh"; } } LinesMesh.prototype.enableEdgesRendering ??= _MissingSideEffect("LinesMesh", "enableEdgesRendering"); InstancedLinesMesh.prototype.enableEdgesRendering ??= _MissingSideEffect("InstancedLinesMesh", "enableEdgesRendering"); // #endregion GENERATED_SIDE_EFFECT_STUBS /** This file must only contain pure code and pure imports */ /* eslint-disable @typescript-eslint/naming-convention */ /** * Creates the VertexData of the LineSystem * @param options an object used to set the following optional parameters for the LineSystem, required but can be empty * - lines an array of lines, each line being an array of successive Vector3 * - colors an array of line colors, each of the line colors being an array of successive Color4, one per line point * @returns the VertexData of the LineSystem */ function CreateLineSystemVertexData(options) { const indices = []; const positions = []; const lines = options.lines; const colors = options.colors; const vertexColors = []; let idx = 0; for (let l = 0; l < lines.length; l++) { const points = lines[l]; for (let index = 0; index < points.length; index++) { const { x, y, z } = points[index]; positions.push(x, y, z); if (colors) { const color = colors[l]; const { r, g, b, a } = color[index]; vertexColors.push(r, g, b, a); } if (index > 0) { indices.push(idx - 1); indices.push(idx); } idx++; } } const vertexData = new VertexData(); vertexData.indices = indices; vertexData.positions = positions; if (colors) { vertexData.colors = vertexColors; } return vertexData; } /** * Create the VertexData for a DashedLines * @param options an object used to set the following optional parameters for the DashedLines, required but can be empty * - points an array successive Vector3 * - dashSize the size of the dashes relative to the dash number, optional, default 3 * - gapSize the size of the gap between two successive dashes relative to the dash number, optional, default 1 * - dashNb the intended total number of dashes, optional, default 200 * @returns the VertexData for the DashedLines */ function CreateDashedLinesVertexData(options) { const dashSize = options.dashSize || 3; const gapSize = options.gapSize || 1; const dashNb = options.dashNb || 200; const points = options.points; const positions = []; const indices = []; const curvect = Vector3.Zero(); let lg = 0; let nb; let curshft; let idx = 0; let i; for (i = 0; i < points.length - 1; i++) { points[i + 1].subtractToRef(points[i], curvect); lg += curvect.length(); } const shft = lg / dashNb; const dashshft = (dashSize * shft) / (dashSize + gapSize); for (i = 0; i < points.length - 1; i++) { points[i + 1].subtractToRef(points[i], curvect); nb = Math.floor(curvect.length() / shft); curvect.normalize(); for (let j = 0; j < nb; j++) { curshft = shft * j; positions.push(points[i].x + curshft * curvect.x, points[i].y + curshft * curvect.y, points[i].z + curshft * curvect.z); positions.push(points[i].x + (curshft + dashshft) * curvect.x, points[i].y + (curshft + dashshft) * curvect.y, points[i].z + (curshft + dashshft) * curvect.z); indices.push(idx, idx + 1); idx += 2; } } // Result const vertexData = new VertexData(); vertexData.positions = positions; vertexData.indices = indices; return vertexData; } /** * Creates a line system mesh. A line system is a pool of many lines gathered in a single mesh * * A line system mesh is considered as a parametric shape since it has no predefined original shape. Its shape is determined by the passed array of lines as an input parameter * * Like every other parametric shape, it is dynamically updatable by passing an existing instance of LineSystem to this static function * * The parameter `lines` is an array of lines, each line being an array of successive Vector3 * * The optional parameter `instance` is an instance of an existing LineSystem object to be updated with the passed `lines` parameter * * The optional parameter `colors` is an array of line colors, each line colors being an array of successive Color4, one per line point * * The optional parameter `useVertexAlpha` is to be set to `false` (default `true`) when you don't need the alpha blending (faster) * * The optional parameter `material` is the material to use to draw the lines if provided. If not, a default material will be created * * Updating a simple Line mesh, you just need to update every line in the `lines` array : https://doc.babylonjs.com/features/featuresDeepDive/mesh/dynamicMeshMorph#lines-and-dashedlines * * When updating an instance, remember that only line point positions can change, not the number of points, neither the number of lines * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param#line-system * @param name defines the name of the new line system * @param options defines the options used to create the line system * @param scene defines the hosting scene * @returns a new line system mesh */ function CreateLineSystem(name, options, scene = null) { const instance = options.instance; const lines = options.lines; const colors = options.colors; if (instance) { // lines update const positions = instance.getVerticesData(VertexBuffer.PositionKind); let vertexColor; let lineColors; if (colors) { vertexColor = instance.getVerticesData(VertexBuffer.ColorKind); } let i = 0; let c = 0; for (let l = 0; l < lines.length; l++) { const points = lines[l]; for (let p = 0; p < points.length; p++) { positions[i] = points[p].x; positions[i + 1] = points[p].y; positions[i + 2] = points[p].z; if (colors && vertexColor) { lineColors = colors[l]; vertexColor[c] = lineColors[p].r; vertexColor[c + 1] = lineColors[p].g; vertexColor[c + 2] = lineColors[p].b; vertexColor[c + 3] = lineColors[p].a; c += 4; } i += 3; } } instance.updateVerticesData(VertexBuffer.PositionKind, positions, false, false); if (colors && vertexColor) { instance.updateVerticesData(VertexBuffer.ColorKind, vertexColor, false, false); } instance.refreshBoundingInfo(); return instance; } // line system creation const useVertexColor = colors ? true : false; const lineSystem = new LinesMesh(name, scene, null, undefined, undefined, useVertexColor, options.useVertexAlpha, options.material); const vertexData = CreateLineSystemVertexData(options); vertexData.applyToMesh(lineSystem, options.updatable); return lineSystem; } /** * Creates a line mesh * A line mesh is considered as a parametric shape since it has no predefined original shape. Its shape is determined by the passed array of points as an input parameter * * Like every other parametric shape, it is dynamically updatable by passing an existing instance of LineMesh to this static function * * The parameter `points` is an array successive Vector3 * * The optional parameter `instance` is an instance of an existing LineMesh object to be updated with the passed `points` parameter : https://doc.babylonjs.com/features/featuresDeepDive/mesh/dynamicMeshMorph#lines-and-dashedlines * * The optional parameter `colors` is an array of successive Color4, one per line point * * The optional parameter `useVertexAlpha` is to be set to `false` (default `true`) when you don't need alpha blending (faster) * * The optional parameter `material` is the material to use to draw the lines if provided. If not, a default material will be created * * When updating an instance, remember that only point positions can change, not the number of points * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param#lines * @param name defines the name of the new line system * @param options defines the options used to create the line system * @param scene defines the hosting scene * @returns a new line mesh */ function CreateLines(name, options, scene = null) { const colors = options.colors ? [options.colors] : null; const lines = CreateLineSystem(name, { lines: [options.points], updatable: options.updatable, instance: options.instance, colors: colors, useVertexAlpha: options.useVertexAlpha, material: options.material }, scene); return lines; } /** * Creates a dashed line mesh * * A dashed line mesh is considered as a parametric shape since it has no predefined original shape. Its shape is determined by the passed array of points as an input parameter * * Like every other parametric shape, it is dynamically updatable by passing an existing instance of LineMesh to this static function * * The parameter `points` is an array successive Vector3 * * The parameter `dashNb` is the intended total number of dashes (positive integer, default 200) * * The parameter `dashSize` is the size of the dashes relatively the dash number (positive float, default 3) * * The parameter `gapSize` is the size of the gap between two successive dashes relatively the dash number (positive float, default 1) * * The optional parameter `instance` is an instance of an existing LineMesh object to be updated with the passed `points` parameter : https://doc.babylonjs.com/features/featuresDeepDive/mesh/dynamicMeshMorph#lines-and-dashedlines * * The optional parameter `useVertexAlpha` is to be set to `false` (default `true`) when you don't need the alpha blending (faster) * * The optional parameter `material` is the material to use to draw the lines if provided. If not, a default material will be created * * When updating an instance, remember that only point positions can change, not the number of points * * The mesh can be set to updatable with the boolean parameter `updatable` (default false) if its internal geometry is supposed to change once created * @param name defines the name of the mesh * @param options defines the options used to create the mesh * @param scene defines the hosting scene * @returns the dashed line mesh * @see https://doc.babylonjs.com/features/featuresDeepDive/mesh/creation/param#dashed-lines */ function CreateDashedLines(name, options, scene = null) { const points = options.points; const instance = options.instance; const gapSize = options.gapSize || 1; const dashSize = options.dashSize || 3; if (instance) { // dashed lines update const positionFunction = (positions) => { const curvect = Vector3.Zero(); const nbSeg = positions.length / 6; let lg = 0; let nb; let curshft; let p = 0; let i; let j; for (i = 0; i < points.length - 1; i++) { points[i + 1].subtractToRef(points[i], curvect); lg += curvect.length(); } const shft = lg / nbSeg; const dashSize = instance._creationDataStorage.dashSize; const gapSize = instance._creationDataStorage.gapSize; const dashshft = (dashSize * shft) / (dashSize + gapSize); for (i = 0; i < points.length - 1; i++) { points[i + 1].subtractToRef(points[i], curvect); nb = Math.floor(curvect.length() / shft); curvect.normalize(); j = 0; while (j < nb && p < positions.length) { curshft = shft * j; positions[p] = points[i].x + curshft * curvect.x; positions[p + 1] = points[i].y + curshft * curvect.y; positions[p + 2] = points[i].z + curshft * curvect.z; positions[p + 3] = points[i].x + (curshft + dashshft) * curvect.x; positions[p + 4] = points[i].y + (curshft + dashshft) * curvect.y; positions[p + 5] = points[i].z + (curshft + dashshft) * curvect.z; p += 6; j++; } } while (p < positions.length) { positions[p] = points[i].x; positions[p + 1] = points[i].y; positions[p + 2] = points[i].z; p += 3; } }; if (options.dashNb || options.dashSize || options.gapSize || options.useVertexAlpha || options.material) { Logger.Warn("You have used an option other than points with the instance option. Please be aware that these other options will be ignored."); } instance.updateMeshPositions(positionFunction, false); return instance; } // dashed lines creation const dashedLines = new LinesMesh(name, scene, null, undefined, undefined, undefined, options.useVertexAlpha, options.material); const vertexData = CreateDashedLinesVertexData(options); vertexData.applyToMesh(dashedLines, options.updatable); dashedLines._creationDataStorage = new _CreationDataStorage(); dashedLines._creationDataStorage.dashSize = dashSize; dashedLines._creationDataStorage.gapSize = gapSize; return dashedLines; } let _Registered$2 = false; /** * Register side effects for linesBuilder. * Safe to call multiple times; only the first call has an effect. */ function RegisterLinesBuilder() { if (_Registered$2) { return; } _Registered$2 = true; VertexData.CreateLineSystem = CreateLineSystemVertexData; VertexData.CreateDashedLines = CreateDashedLinesVertexData; Mesh.CreateLines = (name, points, scene = null, updatable = false, instance = null) => { const options = { points, updatable, instance, }; return CreateLines(name, options, scene); }; Mesh.CreateDashedLines = (name, points, dashSize, gapSize, dashNb, scene = null, updatable, instance) => { const options = { points, dashSize, gapSize, dashNb, updatable, instance, }; return CreateDashedLines(name, options, scene); }; } /** * Re-exports pure implementation and applies runtime side effects. * Import linesBuilder.pure for tree-shakeable, side-effect-free usage. */ RegisterLinesBuilder(); 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, height: imageData.height }); } 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 coeffs = data.shN.bands ? (data.shN.bands + 1) ** 2 - 1 : data.shN.shape[1] / 3; // 3 components per coeff const shDegree = data.shN.bands !== undefined && data.shN.bands !== null ? data.shN.bands : Math.round(Math.sqrt(coeffs + 1) - 1); 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; const engine = scene.getEngine(); const width = engine.getCaps().maxTextureSize; const height = Math.ceil(splatCount / width); // sh is an array of uint8array that will be used to create sh textures const sh = AllocateShBuffers(textureCount, height * width * 4 * 4); 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, shDegree: shDegree }); }); } 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 */ 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); } function CreateSogTexture(scene, bits, width, height) { const tex = new RawTexture(bits, width, height, Constants.TEXTUREFORMAT_RGBA, scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURETYPE_UNSIGNED_BYTE); tex.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; tex.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; return tex; } function CreateSogTextureFromImage(scene, image) { return CreateSogTexture(scene, image.bits, image.width, image.height); } /** * Loads a SOG attribute image straight onto a GPU texture using `createImageBitmap` (decoded off the main * thread, with `premultiplyAlpha`/`colorSpaceConversion` set to "none" so the raw data bytes are preserved) * and a direct `texImage2D` upload — avoiding the `<img>` + 2D-canvas `getImageData` readback that stalls the * frame. Falls back to the canvas path when `createImageBitmap` is unavailable or fails. Use only for textures * whose pixels are never read back on the CPU (scales, quats, sh0, shN); means_l/means_u still go through the * readback path because their bytes are needed to decode positions for the sort worker. * @param rootUrlOrData base URL (string) or the raw file bytes (Uint8Array) * @param filename file name (used only with a URL) * @param scene hosting scene * @returns a GPU texture holding the raw image bytes */ async function LoadSogTextureDirectAsync(rootUrlOrData, filename, scene) { const engine = scene.getEngine(); if (typeof createImageBitmap === "function") { try { // A typed blob is required: createImageBitmap can fail to decode a typeless blob (and the // content-type is lost when loading via LoadFileAsync), which would force the slow canvas fallback. const mimeType = filename.toLowerCase().endsWith(".png") ? "image/png" : "image/webp"; let blob; if (typeof rootUrlOrData === "string") { const buffer = (await Tools.LoadFileAsync(rootUrlOrData + filename, true)); blob = new Blob([buffer], { type: mimeType }); } else { blob = new Blob([rootUrlOrData], { type: mimeType }); } const bitmap = await createImageBitmap(blob, { premultiplyAlpha: "none", colorSpaceConversion: "none" }); try { const tex = new RawTexture(null, bitmap.width, bitmap.height, Constants.TEXTUREFORMAT_RGBA, scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURETYPE_UNSIGNED_BYTE); tex.wrapU = Constants.TEXTURE_CLAMP_ADDRESSMODE; tex.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; const internal = tex.getInternalTexture(); if (internal) { // invertY=false / premulAlpha=false keep the byte layout identical to the canvas path. engine.updateDynamicTexture(internal, bitmap, false, false); } return tex; } finally { bitmap.close(); } } catch { // Fall through to the canvas readback path below. } } const image = await LoadWebpImageData(rootUrlOrData, filename, engine); return CreateSogTextureFromImage(scene, image); } function DecodeSogPositions(data, meansl, meansu, splatCount) { const unlog = (n) => Math.sign(n) * (Math.exp(Math.abs(n)) - 1); if (!Array.isArray(data.means.mins) || !Array.isArray(data.means.maxs)) { throw new Error("Missing arrays in SOG data."); } // Stride-4 layout (x,y,z,w) expected by the depth-sort worker and the centers texture. const positions = new Float32Array(splatCount * 4); for (let i = 0; i < splatCount; i++) { const index = i * 4; for (let j = 0; j < 3; j++) { const q = (meansu[index + j] << 8) | meansl[index + j]; const n = Scalar.Lerp(data.means.mins[j], data.means.maxs[j], q / 65535); positions[i * 4 + j] = unlog(n); } positions[i * 4 + 3] = 1.0; } return positions; } /** * Parse SOG data and produce a set of GPU textures + dequantization parameters. * The shader will sample these raw RGBA8 textures and reconstruct positions/scales/rotations/colors/SH on the GPU. * @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 * @param computeCpuPositions When true (default), means_l/means_u are read back on the CPU and `pack.positions` * is decoded for the sort worker / bounding box. Pass false when the caller will instead read the decoded * centers back from the GPU work buffer — then every attribute (including means) uses the fast direct * ImageBitmap upload (no `getImageData` readback) and `pack.positions` is left empty. * @param downloadManager Optional download manager that throttles and retries the per-file image downloads * (used by the LOD streamer). When omitted, files are fetched directly. Only applies when loading from a URL. * @param downloadGroupId Optional group tag passed to the download manager so this file's image downloads can * be cancelled together if the streamer no longer needs them. * @returns Parsed splat info with `sogTextures` populated. */ // eslint-disable-next-line @typescript-eslint/no-restricted-types async function ParseSogMetaAsTextures(dataOrFiles, rootUrl, scene, computeCpuPositions = true, downloadManager, downloadGroupId) { 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; } // Attribute textures (scales/quats/sh0/shN) are only sampled on the GPU, so they always upload straight // from a decoded ImageBitmap (no getImageData readback). means_l/means_u additionally need their CPU bytes // when computeCpuPositions is set (to decode positions for the sort worker) — those go through the // <img>+canvas path; otherwise means also use the fast direct upload. All loads run in parallel. const loadMeansImageAsync = async (fileName) => { if (files && files.has(fileName)) { return await LoadWebpImageData(files.get(fileName), fileName, scene.getEngine()); } if (downloadManager) { const bytes = new Uint8Array(await downloadManager.loadFileAsync(rootUrl + fileName, downloadGroupId)); return await LoadWebpImageData(bytes, fileName, scene.getEngine()); } return await LoadWebpImageData(rootUrl, fileName, scene.getEngine()); }; const loadGpuTextureAsync = async (fileName) => { if (files && files.has(fileName)) { return await LoadSogTextureDirectAsync(files.get(fileName), fileName, scene); } if (downloadManager) { const bytes = new Uint8Array(await downloadManager.loadFileAsync(rootUrl + fileName, downloadGroupId)); return await LoadSogTextureDirectAsync(bytes, fileName, scene); } return await LoadSogTextureDirectAsync(rootUrl, fileName, scene); }; const gpuFiles = [...data.scales.files, ...data.quats.files, ...data.sh0.files, ...(data.shN?.files ?? [])]; let meansL; let meansU; let meansWidth; let meansHeight; let meansImages = null; let gpuTextures; if (computeCpuPositions) { const [images, gpu] = await Promise.all([Promise.all(data.means.files.map(loadMeansImageAsync)), Promise.all(gpuFiles.map(loadGpuTextureAsync))]); meansImages = [images[0], images[1]]; gpuTextures = gpu; meansL = CreateSogTextureFromImage(scene, images[0]); meansU = CreateSogTextureFromImage(scene, images[1]); meansWidth = images[0].width; meansHeight = images[0].height; } else { const [meansTex, gpu] = await Promise.all([Promise.all(data.means.files.map(loadGpuTextureAsync)), Promise.all(gpuFiles.map(loadGpuTextureAsync))]); gpuTextures = gpu; meansL = meansTex[0]; meansU = meansTex[1]; const size = meansL.getSize(); meansWidth = size.width; meansHeight = size.height; } const splatCount = data.count ?? data.means.shape[0]; const splatTexelCount = meansWidth * meansHeight; if (splatTexelCount < splatCount) { throw new Error(`SOG texture contains ${splatTexelCount} texels, but metadata references ${splatCount} splats.`); } const scales = gpuTextures[0]; const quats = gpuTextures[1]; const sh0 = gpuTextures[2]; let shCentroids; let shLabels; let shCoeffCount = 0; let shDegree = 0; if (data.shN && gpuTextures.length >= 5) { shCentroids = gpuTextures[3]; shLabels = gpuTextures[4]; shCoeffCount = data.shN.bands ? (data.shN.bands + 1) ** 2 - 1 : data.shN.shape[1] / 3; shDegree = data.shN.bands ?? Math.round(Math.sqrt(shCoeffCount + 1) - 1); } // Optional codebook packed into a 1D R32F texture: [scales(256) | sh0(256) | shN(256)] let codebookTexture; if (data.version === 2) { const codebookSize = 256; const packed = new Float32Array(codebookSize * 3); if (data.scales.codebook) { packed.set(data.scales.codebook.slice(0, codebookSize), 0); } if (data.sh0.codebook) { packed.set(data.sh0.codebook.slice(0, codebookSize), codebookSize); } if (data.shN?.codebook) { packed.set(data.shN.codebook.slice(0, codebookSize), codebookSize * 2); } codebookTexture = new RawTexture(packed, codebookSize * 3, 1, Constants.TEXTUREFORMAT_R, scene, false, false, Constants.TEXTURE_NEAREST_SAMPLINGMODE, Constants.TEXTURETYPE_FLOAT); codebookTexture.wrapU