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.

209 lines (205 loc) 11 kB
import { bq as GaussianSplattingMesh, br as AllocateShBuffers, bo as unregisterGLTFExtension, bp as registerGLTFExtension } from './index-MZPybX0H.esm.js'; import { b as GLTFLoader, A as ArrayItem } from './glTFLoader.pure-BaIedO7s.esm.js'; import './morphTargetManager-CK7B3gaT.esm.js'; import './bone.pure-CjsCww39.esm.js'; import './skeleton-D7Sw58cv.esm.js'; import './assetContainer-Bm0vsreJ.esm.js'; import './objectModelMapping-De5EKNEZ.esm.js'; import './spotLight.pure-CRdZC6Ci.esm.js'; const NAME = "KHR_gaussian_splatting"; // Zeroth-order spherical harmonics coefficient used to reconstruct a base color from the SH DC term. const ShC0 = 0.28209479177387814; // Attribute semantics defined by the KHR_gaussian_splatting ellipse kernel. const RotationAttribute = "KHR_gaussian_splatting:ROTATION"; const ScaleAttribute = "KHR_gaussian_splatting:SCALE"; const OpacityAttribute = "KHR_gaussian_splatting:OPACITY"; const ShDegree0Attribute = "KHR_gaussian_splatting:SH_DEGREE_0_COEF_0"; // Number of spherical harmonics coefficients (VEC3) for each degree beyond the DC term. const ShCoefficientCountPerDegree = [0, 3, 5, 7]; function Clamp255(value) { return value <= 0 ? 0 : value >= 255 ? 255 : (value + 0.5) | 0; } /** * [Specification](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_gaussian_splatting/README.md) * Loads a mesh primitive tagged with KHR_gaussian_splatting as a {@link GaussianSplattingMesh}. */ // eslint-disable-next-line @typescript-eslint/naming-convention class KHR_gaussian_splatting { /** * @internal */ constructor(loader) { /** * The name of this extension. */ this.name = NAME; this._loader = loader; this.enabled = this._loader.isExtensionUsed(NAME); } /** @internal */ dispose() { this._loader = null; } /** * @internal */ // eslint-disable-next-line no-restricted-syntax _loadMeshPrimitiveAsync(context, name, node, mesh, primitive, assign) { return GLTFLoader.LoadExtensionAsync(context, primitive, this.name, async (extensionContext) => { if (primitive.mode != undefined && primitive.mode !== 0 /* MeshPrimitiveMode.POINTS */) { throw new Error(`${extensionContext}: Gaussian splatting primitives must use POINTS mode`); } const loader = this._loader; const loadAttribute = (attributeName) => { const accessorIndex = primitive.attributes[attributeName]; if (accessorIndex == undefined) { return null; } const accessor = ArrayItem.Get(`${context}/attributes/${attributeName}`, loader.gltf.accessors, accessorIndex); return loader._loadFloatAccessorAsync(`/accessors/${accessor.index}`, accessor); }; const positionsPromise = loadAttribute("POSITION"); if (!positionsPromise) { throw new Error(`${extensionContext}: Gaussian splatting primitive is missing the POSITION attribute`); } // Determine which spherical harmonics degrees are present (all lower degrees must exist per spec). let shDegree = 0; const shAttributeNames = []; for (let degree = 1; degree <= 3; degree++) { if (primitive.attributes[`KHR_gaussian_splatting:SH_DEGREE_${degree}_COEF_0`] == undefined) { break; } shDegree = degree; for (let coef = 0; coef < ShCoefficientCountPerDegree[degree]; coef++) { shAttributeNames.push(`KHR_gaussian_splatting:SH_DEGREE_${degree}_COEF_${coef}`); } } // Create the Gaussian Splatting mesh and assign it to the node synchronously (before awaiting the // attribute data). The base loader wires the node's transform node from this assign call // synchronously, so it must happen before the first await. The splat data is uploaded afterwards. const scene = loader.babylonScene; scene._blockEntityCollection = !!loader._assetContainer; const gaussianSplattingMesh = new GaussianSplattingMesh(name, null, scene); gaussianSplattingMesh._parentContainer = loader._assetContainer; scene._blockEntityCollection = false; GLTFLoader.AddPointerMetadata(gaussianSplattingMesh, context); loader.parent.onMeshLoadedObservable.notifyObservers(gaussianSplattingMesh); assign(gaussianSplattingMesh); const engineCaps = scene.getEngine().getCaps(); const [positions, rotations, scales, opacities, shDegree0, colors, ...shHigherDegrees] = await Promise.all([ positionsPromise, loadAttribute(RotationAttribute), loadAttribute(ScaleAttribute), loadAttribute(OpacityAttribute), loadAttribute(ShDegree0Attribute), loadAttribute("COLOR_0"), ...shAttributeNames.map((attributeName) => loadAttribute(attributeName)), ]); const splatCount = positions.length / 3; const colorStride = colors ? colors.length / splatCount : 0; const rowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // 32 bytes: position(12) scale(12) color RGBA(4) quaternion wxyz(4) const buffer = new ArrayBuffer(rowOutputLength * splatCount); const floatView = new Float32Array(buffer); const byteView = new Uint8Array(buffer); // Build spherical harmonics texture buffers for degrees above 0. let shBuffers = null; if (shDegree > 0) { const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1; const shComponentCount = shVectorCount * 3; const textureCount = Math.ceil(shComponentCount / 16); const width = engineCaps.maxTextureSize; const height = Math.ceil(splatCount / width); shBuffers = AllocateShBuffers(textureCount, height * width * 4 * 4); } for (let i = 0; i < splatCount; i++) { const floatBase = i * 8; const byteBase = i * 32; const p = i * 3; // Position (float32 x3, bytes 0-11) floatView[floatBase + 0] = positions[p + 0]; floatView[floatBase + 1] = positions[p + 1]; floatView[floatBase + 2] = positions[p + 2]; // Scale (float32 x3, bytes 12-23) — glTF stores linear scale directly. floatView[floatBase + 3] = scales ? scales[p + 0] : 1; floatView[floatBase + 4] = scales ? scales[p + 1] : 1; floatView[floatBase + 5] = scales ? scales[p + 2] : 1; // Color RGB (uint8 x3, bytes 24-26) — reconstructed from the SH DC term, or COLOR_0 as a fallback. if (shDegree0) { byteView[byteBase + 24] = Clamp255((0.5 + ShC0 * shDegree0[p + 0]) * 255); byteView[byteBase + 25] = Clamp255((0.5 + ShC0 * shDegree0[p + 1]) * 255); byteView[byteBase + 26] = Clamp255((0.5 + ShC0 * shDegree0[p + 2]) * 255); } else if (colors) { const c = i * colorStride; byteView[byteBase + 24] = Clamp255(colors[c + 0] * 255); byteView[byteBase + 25] = Clamp255(colors[c + 1] * 255); byteView[byteBase + 26] = Clamp255(colors[c + 2] * 255); } else { byteView[byteBase + 24] = 255; byteView[byteBase + 25] = 255; byteView[byteBase + 26] = 255; } // Alpha (uint8, byte 27) — opacity is a normalized linear value per spec. if (opacities) { byteView[byteBase + 27] = Clamp255(opacities[i] * 255); } else if (colors && colorStride >= 4) { byteView[byteBase + 27] = Clamp255(colors[i * colorStride + 3] * 255); } else { byteView[byteBase + 27] = 255; } // Quaternion (uint8 x4, bytes 28-31) stored as wxyz encoded as q * 127.5 + 127.5. glTF stores xyzw. const r = i * 4; const qx = rotations ? rotations[r + 0] : 0; const qy = rotations ? rotations[r + 1] : 0; const qz = rotations ? rotations[r + 2] : 0; const qw = rotations ? rotations[r + 3] : 1; byteView[byteBase + 28] = Clamp255(qw * 127.5 + 127.5); byteView[byteBase + 29] = Clamp255(qx * 127.5 + 127.5); byteView[byteBase + 30] = Clamp255(qy * 127.5 + 127.5); byteView[byteBase + 31] = Clamp255(qz * 127.5 + 127.5); // Spherical harmonics (coefficient-major, channel-minor) encoded as coeff * 128 + 128. if (shBuffers) { const offsetPerSplat = i * 16; for (let coef = 0; coef < shHigherDegrees.length; coef++) { const coefData = shHigherDegrees[coef]; if (!coefData) { continue; } for (let channel = 0; channel < 3; channel++) { const flatIndex = coef * 3 + channel; const textureIndex = flatIndex >> 4; // Math.floor(flatIndex / 16) const byteIndexInTexture = flatIndex & 15; // flatIndex % 16 shBuffers[textureIndex][offsetPerSplat + byteIndexInTexture] = Clamp255(coefData[p + channel] * 128 + 128); } } } } await gaussianSplattingMesh.updateDataAsync(buffer, shBuffers ?? undefined, undefined, shDegree || undefined); return gaussianSplattingMesh; }); } } let _Registered = false; /** * Registers the KHR_gaussian_splatting glTF loader extension. * Safe to call multiple times; only the first call has an effect. */ // eslint-disable-next-line @typescript-eslint/naming-convention function RegisterKHR_gaussian_splatting() { if (_Registered) { return; } _Registered = true; unregisterGLTFExtension(NAME); registerGLTFExtension(NAME, true, (loader) => new KHR_gaussian_splatting(loader)); } /** * Re-exports the pure implementation and applies the runtime registration side effect. * Import "./KHR_gaussian_splatting.pure" for tree-shakeable, side-effect-free usage. */ RegisterKHR_gaussian_splatting(); export { KHR_gaussian_splatting, RegisterKHR_gaussian_splatting }; //# sourceMappingURL=KHR_gaussian_splatting-CmmwnzOi.esm.js.map