@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
JavaScript
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