diff --git a/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.ts b/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.ts index 8fad8331cfa..2bb5c86a00e 100644 --- a/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.ts +++ b/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.pure.ts @@ -87,6 +87,10 @@ class GaussianSplattingMaterialDefines extends MaterialDefines { public IS_COMPOUND = false; /** Defines the maximum number of parts (computed from engine caps at runtime) */ public MAX_PART_COUNT = GaussianSplattingMaxPartCount; + /** Defines whether SOG raw-texture in-shader dequantization is enabled */ + public USE_SOG = false; + /** Defines whether SOG v2 (codebook) dequantization is enabled */ + public USE_SOG_V2 = false; /** * Constructor of the defines. @@ -184,6 +188,10 @@ export class GaussianSplattingMaterial extends PushMaterial { "shTexture3", "shTexture4", "partIndicesTexture", + "sogQuatsTexture", + "sogShNCentroidsTexture", + "sogShNLabelsTexture", + "sogCodebookTexture", ]; protected static _UniformBuffers = ["Scene", "Mesh"]; protected static _VoxelUniforms = [ @@ -216,6 +224,15 @@ export class GaussianSplattingMaterial extends PushMaterial { "depthValues", "partWorld", "partVisibility", + "sogMeansMin", + "sogMeansMax", + "sogScalesMin", + "sogScalesMax", + "sogSh0Min", + "sogSh0Max", + "sogShnMin", + "sogShnMax", + "sogShCoeffCount", ]; private _sourceMesh: GaussianSplattingMesh | null = null; /** @@ -304,6 +321,8 @@ export class GaussianSplattingMaterial extends PushMaterial { defines["IS_COMPOUND"] = gsMesh.isCompound; defines["MAX_PART_COUNT"] = GetGaussianSplattingMaxPartCount(engine); + defines["USE_SOG"] = gsMesh.useSog; + defines["USE_SOG_V2"] = gsMesh.useSog && gsMesh.sogParams?.version === 2; // Compensation const splatMaterial = gsMesh.material as GaussianSplattingMaterial; @@ -462,7 +481,9 @@ export class GaussianSplattingMaterial extends PushMaterial { effect.setTexture("centersTexture", gsMesh.centersTexture); effect.setTexture("colorsTexture", gsMesh.colorsTexture); - if (gsMesh.shTextures) { + if (gsMesh.useSog) { + GaussianSplattingMaterial._BindSogUniforms(gsMesh, effect); + } else if (gsMesh.shTextures) { for (let i = 0; i < gsMesh.shTextures.length; i++) { effect.setTexture(`shTexture${i}`, gsMesh.shTextures[i]); } @@ -472,6 +493,38 @@ export class GaussianSplattingMaterial extends PushMaterial { gsMesh.bindExtraEffectUniforms(effect); } } + + /** + * Bind SOG dequantization uniforms + raw textures. + * @internal + */ + protected static _BindSogUniforms(gsMesh: GaussianSplattingMesh, effect: Effect): void { + const p = gsMesh.sogParams; + if (!p) { + return; + } + effect.setTexture("sogQuatsTexture", gsMesh.rotationsATexture); + if (gsMesh.shTextures && gsMesh.shTextures.length >= 2) { + effect.setTexture("sogShNCentroidsTexture", gsMesh.shTextures[0]); + effect.setTexture("sogShNLabelsTexture", gsMesh.shTextures[1]); + } + if (p.codebookTexture) { + effect.setTexture("sogCodebookTexture", p.codebookTexture); + } + effect.setFloat3("sogMeansMin", p.meansMin[0], p.meansMin[1], p.meansMin[2]); + effect.setFloat3("sogMeansMax", p.meansMax[0], p.meansMax[1], p.meansMax[2]); + if (p.scalesMin && p.scalesMax) { + effect.setFloat3("sogScalesMin", p.scalesMin[0], p.scalesMin[1], p.scalesMin[2]); + effect.setFloat3("sogScalesMax", p.scalesMax[0], p.scalesMax[1], p.scalesMax[2]); + } + if (p.sh0Min && p.sh0Max) { + effect.setFloat4("sogSh0Min", p.sh0Min[0], p.sh0Min[1], p.sh0Min[2], p.sh0Min[3]); + effect.setFloat4("sogSh0Max", p.sh0Max[0], p.sh0Max[1], p.sh0Max[2], p.sh0Max[3]); + } + effect.setFloat("sogShnMin", p.shnMin ?? 0); + effect.setFloat("sogShnMax", p.shnMax ?? 0); + effect.setFloat("sogShCoeffCount", p.shCoeffCount ?? 0); + } /** * Binds the submesh to this material by preparing the effect and shader to draw * @param world defines the world transformation matrix diff --git a/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.ts b/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.ts index 5656df06407..8d5737d5728 100644 --- a/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.ts +++ b/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.pure.ts @@ -27,6 +27,31 @@ import { type INative } from "core/Engines/Native/nativeInterfaces"; // eslint-disable-next-line @typescript-eslint/naming-convention declare const _native: INative; +/** Internal mirror of ISogTexturePack (defined in loaders) — avoids a circular import. */ +interface ISogPackInternal { + version: 1 | 2; + splatCount: number; + shDegree: number; + meansTextureL: BaseTexture; + meansTextureU: BaseTexture; + scalesTexture: BaseTexture; + quatsTexture: BaseTexture; + sh0Texture: BaseTexture; + shCentroidsTexture?: BaseTexture; + shLabelsTexture?: BaseTexture; + codebookTexture?: BaseTexture; + meansMin: [number, number, number]; + meansMax: [number, number, number]; + scalesMin?: [number, number, number]; + scalesMax?: [number, number, number]; + sh0Min?: [number, number, number, number]; + sh0Max?: [number, number, number, number]; + shnMin?: number; + shnMax?: number; + shCoeffCount: number; + positions: Float32Array; +} + const IsNative = typeof _native !== "undefined"; const Native = IsNative ? _native : null; interface IDelayedTextureUpdate { @@ -394,6 +419,8 @@ export class GaussianSplattingMeshBase extends Mesh { private _delayedTextureUpdate: Nullable = null; protected _useRGBACovariants = false; + protected _useSog = false; + protected _sogParams: Nullable = null; private _material: Nullable = null; private _tmpCovariances = [0, 0, 0, 0, 0, 0]; @@ -632,6 +659,107 @@ export class GaussianSplattingMeshBase extends Mesh { return this._shTextures; } + /** + * True when this mesh holds raw SOG webp textures (dequantized in-shader) rather than the + * pre-decoded covariance/center/color textures produced by the standard splat loader. + */ + public get useSog(): boolean { + return this._useSog; + } + + /** + * SOG dequantization parameters paired with the raw textures. + * Set by the splat loader when `useSogTextures: true`. Null otherwise. + */ + public get sogParams(): Nullable { + return this._sogParams; + } + + /** + * Install a set of raw SOG webp textures and bind the mesh to the in-shader dequantization path. + * @param pack SOG texture pack produced by ParseSogMetaAsTextures. + * @internal + */ + public setSogTextureData(pack: ISogPackInternal): void { + this._useSog = true; + this._sogParams?.codebookTexture?.dispose(); + this._sogParams = pack; + this._vertexCount = pack.splatCount; + this._shDegree = pack.shDegree ?? 0; + this._maxShDegree = this._shDegree; + + // Stride-4 (xyz + 1) — required by the depth-sort worker and the centers texture path. + this._splatPositions = pack.positions; + + // Reuse existing texture slots for SOG textures (the shader, under USE_SOG, samples them as RGBA8). + this._covariancesATexture?.dispose(); + this._covariancesBTexture?.dispose(); + this._centersTexture?.dispose(); + this._colorsTexture?.dispose(); + this._rotationsATexture?.dispose(); + if (this._shTextures) { + for (const t of this._shTextures) { + t.dispose(); + } + } + + this._centersTexture = pack.meansTextureL; + this._covariancesATexture = pack.meansTextureU; + this._covariancesBTexture = pack.scalesTexture; + this._rotationsATexture = pack.quatsTexture; + this._colorsTexture = pack.sh0Texture; + + const shTextures: BaseTexture[] = []; + if (pack.shCentroidsTexture) { + shTextures.push(pack.shCentroidsTexture); + } + if (pack.shLabelsTexture) { + shTextures.push(pack.shLabelsTexture); + } + this._shTextures = shTextures.length ? shTextures : null; + + // Force pipeline rebuild so the USE_SOG define and extra samplers are picked up. + this._material?.resetDrawCache(); + + const size = pack.meansTextureL.getSize(); + this._textureSize.x = size.width; + this._textureSize.y = size.height; + + this._updateSplatIndexBuffer(this._vertexCount); + this._instantiateWorker(); + + // Compute bounds from the CPU-decoded positions (stride-4) so the mesh is not frustum-culled. + const positions = pack.positions as Float32Array; + const minimum = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); + const maximum = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); + for (let i = 0; i < this._vertexCount; i++) { + const x = positions[i * 4 + 0]; + const y = positions[i * 4 + 1]; + const z = positions[i * 4 + 2]; + if (x < minimum.x) { + minimum.x = x; + } + if (y < minimum.y) { + minimum.y = y; + } + if (z < minimum.z) { + minimum.z = z; + } + if (x > maximum.x) { + maximum.x = x; + } + if (y > maximum.y) { + maximum.y = y; + } + if (z > maximum.z) { + maximum.z = z; + } + } + this.getBoundingInfo().reConstruct(minimum, maximum, this.getWorldMatrix()); + this.setEnabled(true); + this._sortIsDirty = true; + } + /** * Gets the kernel size * Documentation and mathematical explanations here: @@ -1855,6 +1983,7 @@ export class GaussianSplattingMeshBase extends Mesh { this._rotationsATexture?.dispose(); this._rotationsBTexture?.dispose(); this._rotationScaleTexture?.dispose(); + this._sogParams?.codebookTexture?.dispose(); this._rotationsATexture = null; this._rotationsBTexture = null; this._rotationScaleTexture = null; diff --git a/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx b/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx index facfd831941..39b9af1cdaf 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx @@ -46,6 +46,9 @@ struct Splat { vec4 rotationB; vec4 rotationScale; #endif +#ifdef USE_SOG + float splatIndex; +#endif }; float getSplatIndex(int localIndex) @@ -80,25 +83,98 @@ Splat readSplat(float splatIndex) { Splat splat; vec2 splatUV = getDataUV(splatIndex, dataTextureSize); +#ifdef USE_SOG + // --- SOG raw-texture path. All samplers are RGBA8 normalized with nearest filtering. + ivec2 sogUVi = ivec2(int(splatUV.x * dataTextureSize.x), int(splatUV.y * dataTextureSize.y)); + vec4 mL = texelFetch(centersTexture, sogUVi, 0); // means_l + vec4 mU = texelFetch(covariancesATexture, sogUVi, 0); // means_u + vec4 sRaw = texelFetch(covariancesBTexture, sogUVi, 0); // scales (3 bytes valid) + vec4 qRaw = texelFetch(sogQuatsTexture, sogUVi, 0); // quaternion 3+mode + vec4 c0 = texelFetch(colorsTexture, sogUVi, 0); // sh0 + + // Reconstruct position: q = (u<<8)|l; n = lerp(min,max,q/65535); pos = sign(n)*(exp(|n|)-1) + vec3 q16 = (mU.xyz * 256.0 + mL.xyz) * (255.0 / 65535.0); + vec3 nPos = mix(sogMeansMin, sogMeansMax, q16); + vec3 center = sign(nPos) * (exp(abs(nPos)) - vec3(1.0)); + splat.center = vec4(center, 1.0); + + // Reconstruct scale (v1: lerp+exp ; v2: codebook lookup) +#ifdef USE_SOG_V2 + // codebook layout: [scales 0..255 | sh0 256..511 | shN 512..767], width=768 + vec3 sIdx = floor(sRaw.xyz * 255.0 + 0.5); + vec3 splatScale; + splatScale.x = exp(texelFetch(sogCodebookTexture, ivec2(int(sIdx.x), 0), 0).r); + splatScale.y = exp(texelFetch(sogCodebookTexture, ivec2(int(sIdx.y), 0), 0).r); + splatScale.z = exp(texelFetch(sogCodebookTexture, ivec2(int(sIdx.z), 0), 0).r); +#else + vec3 splatScale = exp(mix(sogScalesMin, sogScalesMax, sRaw.xyz)); +#endif + + // Reconstruct quaternion (largest-omitted, mode in alpha as 252+omitted-index) + const float invSqrt2 = 0.70710678118; + vec3 qabc = (qRaw.xyz - vec3(0.5)) * 2.0 * invSqrt2; + int qMode = int(qRaw.w * 255.0 + 0.5) - 252; + float qd = sqrt(max(0.0, 1.0 - dot(qabc, qabc))); + vec4 quat; + if (qMode == 0) quat = vec4(qd, qabc.x, qabc.y, qabc.z); + else if (qMode == 1) quat = vec4(qabc.x, qd, qabc.y, qabc.z); + else if (qMode == 2) quat = vec4(qabc.x, qabc.y, qd, qabc.z); + else quat = vec4(qabc.x, qabc.y, qabc.z, qd); + + // Build rotation matrix from quaternion (w,x,y,z) + float qw = quat.x, qx = quat.y, qy = quat.z, qz = quat.w; + mat3 R = mat3( + 1.0 - 2.0*(qy*qy + qz*qz), 2.0*(qx*qy + qw*qz), 2.0*(qx*qz - qw*qy), + 2.0*(qx*qy - qw*qz), 1.0 - 2.0*(qx*qx + qz*qz), 2.0*(qy*qz + qw*qx), + 2.0*(qx*qz + qw*qy), 2.0*(qy*qz - qw*qx), 1.0 - 2.0*(qx*qx + qy*qy) + ); + + // Covariance = R * diag(2s)^2 * R^T to match the CPU path (which scales by 2x before squaring). + mat3 S2 = mat3(4.0*splatScale.x*splatScale.x, 0.0, 0.0, + 0.0, 4.0*splatScale.y*splatScale.y, 0.0, + 0.0, 0.0, 4.0*splatScale.z*splatScale.z); + mat3 Sigma = R * S2 * transpose(R); + splat.covA = vec4(Sigma[0][0], Sigma[0][1], Sigma[0][2], Sigma[1][1]); + splat.covB = vec4(Sigma[1][2], Sigma[2][2], 0.0, 0.0); + + // Color (sh0): RGB -> 0.5 + c*SH_C0 ; alpha -> sigmoid(c) (v1) or codebook[a] (v2) + const float SH_C0 = 0.28209479177387814; +#ifdef USE_SOG_V2 + vec3 c3; + c3.x = texelFetch(sogCodebookTexture, ivec2(256 + int(c0.x * 255.0 + 0.5), 0), 0).r; + c3.y = texelFetch(sogCodebookTexture, ivec2(256 + int(c0.y * 255.0 + 0.5), 0), 0).r; + c3.z = texelFetch(sogCodebookTexture, ivec2(256 + int(c0.z * 255.0 + 0.5), 0), 0).r; + vec3 colRgb = vec3(0.5) + c3 * SH_C0; + float colA = c0.w; // already 0..1 +#else + vec4 cLerp = mix(sogSh0Min, sogSh0Max, c0); + vec3 colRgb = vec3(0.5) + cLerp.xyz * SH_C0; + float colA = 1.0 / (1.0 + exp(-cLerp.w)); +#endif + splat.color = vec4(colRgb, colA); + splat.splatIndex = splatIndex; +#else splat.center = texture2D(centersTexture, splatUV); splat.color = texture2D(colorsTexture, splatUV); #if !defined(IS_FOR_VOXELIZATION) splat.covA = texture2D(covariancesATexture, splatUV) * splat.center.w; splat.covB = texture2D(covariancesBTexture, splatUV) * splat.center.w; #endif +#endif + #if SH_DEGREE > 0 || IS_COMPOUND ivec2 splatUVint = getDataUVint(splatIndex, dataTextureSize); #endif -#if SH_DEGREE > 0 +#if SH_DEGREE > 0 && !defined(USE_SOG) splat.sh0 = texelFetch(shTexture0, splatUVint, 0); #endif -#if SH_DEGREE > 1 +#if SH_DEGREE > 1 && !defined(USE_SOG) splat.sh1 = texelFetch(shTexture1, splatUVint, 0); #endif -#if SH_DEGREE > 2 +#if SH_DEGREE > 2 && !defined(USE_SOG) splat.sh2 = texelFetch(shTexture2, splatUVint, 0); #endif -#if SH_DEGREE > 3 +#if SH_DEGREE > 3 && !defined(USE_SOG) splat.sh3 = texelFetch(shTexture3, splatUVint, 0); splat.sh4 = texelFetch(shTexture4, splatUVint, 0); #endif @@ -205,6 +281,48 @@ vec4 decompose(uint value) return components * vec4(2./255.) - vec4(1.); } +#ifdef USE_SOG +vec3 computeSH(Splat splat, vec3 dir) +{ +#if SH_DEGREE > 0 + vec3 sh[25]; + sh[0] = vec3(0., 0., 0.); + + // Read 16-bit label for this splat from the labels texture (LSB in r, MSB in g). + ivec2 labelSize = textureSize(sogShNLabelsTexture, 0); + int idx = int(splat.splatIndex + 0.5); + int lx = idx - (idx / labelSize.x) * labelSize.x; + int ly = idx / labelSize.x; + vec4 labelRaw = texelFetch(sogShNLabelsTexture, ivec2(lx, ly), 0); + int n = int(labelRaw.r * 255.0 + 0.5) + int(labelRaw.g * 255.0 + 0.5) * 256; + + int coeffs = int(sogShCoeffCount + 0.5); + int u = (n - (n / 64) * 64) * coeffs; + int v = n / 64; + + for (int k = 0; k < 24; k++) { + if (k >= coeffs) break; + vec4 centroidRaw = texelFetch(sogShNCentroidsTexture, ivec2(u + k, v), 0); + vec3 shCoeff; +#ifdef USE_SOG_V2 + int rIdx = int(centroidRaw.r * 255.0 + 0.5); + int gIdx = int(centroidRaw.g * 255.0 + 0.5); + int bIdx = int(centroidRaw.b * 255.0 + 0.5); + shCoeff.r = texelFetch(sogCodebookTexture, ivec2(512 + rIdx, 0), 0).r; + shCoeff.g = texelFetch(sogCodebookTexture, ivec2(512 + gIdx, 0), 0).r; + shCoeff.b = texelFetch(sogCodebookTexture, ivec2(512 + bIdx, 0), 0).r; +#else + shCoeff = mix(vec3(sogShnMin), vec3(sogShnMax), centroidRaw.rgb); +#endif + sh[k + 1] = shCoeff; + } + + return computeColorFromSHDegree(dir, sh, 1., 1., 1., 1.); +#else + return vec3(0., 0., 0.); +#endif +} +#else vec3 computeSHWeighted(Splat splat, vec3 dir, float _so1, float _so2, float _so3, float _so4) { vec3 sh[25]; @@ -292,6 +410,7 @@ vec3 computeSH(Splat splat, vec3 dir) #endif return computeSHWeighted(splat, dir, _w1, _w2, _w3, _w4); } +#endif #else vec3 computeSH(Splat splat, vec3 dir) { diff --git a/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx b/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx index 42dbbb047bd..1d66127ac49 100644 --- a/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx +++ b/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx @@ -28,16 +28,37 @@ uniform sampler2D covariancesBTexture; uniform sampler2D centersTexture; uniform sampler2D colorsTexture; +#ifdef USE_SOG +uniform sampler2D sogQuatsTexture; +uniform vec3 sogMeansMin; +uniform vec3 sogMeansMax; +#ifdef USE_SOG_V2 +uniform sampler2D sogCodebookTexture; // 768x1 R32F, packed [scales|sh0|shN] +#else +uniform vec3 sogScalesMin; +uniform vec3 sogScalesMax; +uniform vec4 sogSh0Min; +uniform vec4 sogSh0Max; +uniform float sogShnMin; +uniform float sogShnMax; +#endif #if SH_DEGREE > 0 +uniform sampler2D sogShNCentroidsTexture; +uniform sampler2D sogShNLabelsTexture; +uniform float sogShCoeffCount; +#endif +#endif + +#if SH_DEGREE > 0 && !defined(USE_SOG) uniform highp usampler2D shTexture0; #endif -#if SH_DEGREE > 1 +#if SH_DEGREE > 1 && !defined(USE_SOG) uniform highp usampler2D shTexture1; #endif -#if SH_DEGREE > 2 +#if SH_DEGREE > 2 && !defined(USE_SOG) uniform highp usampler2D shTexture2; #endif -#if SH_DEGREE > 3 +#if SH_DEGREE > 3 && !defined(USE_SOG) uniform highp usampler2D shTexture3; uniform highp usampler2D shTexture4; #endif diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx index aa36fb1e164..d5a770a8fae 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx @@ -30,6 +30,9 @@ struct Splat { rotationB: vec4f, rotationScale: vec4f, #endif +#ifdef USE_SOG + splatIndex: f32, +#endif }; fn getSplatIndex(localIndex: i32, splatIndex0: vec4f, splatIndex1: vec4f, splatIndex2: vec4f, splatIndex3: vec4f) -> f32 { @@ -127,22 +130,86 @@ fn readSplat(splatIndex: f32, dataTextureSize: vec2f) -> Splat { var splat: Splat; let splatUV = getDataUV(splatIndex, dataTextureSize); let splatUVi32 = vec2(i32(splatUV.x), i32(splatUV.y)); +#ifdef USE_SOG + let mL = textureLoad(centersTexture, splatUVi32, 0); + let mU = textureLoad(covariancesATexture, splatUVi32, 0); + let sRaw = textureLoad(covariancesBTexture, splatUVi32, 0); + let qRaw = textureLoad(sogQuatsTexture, splatUVi32, 0); + let c0 = textureLoad(colorsTexture, splatUVi32, 0); + + let q16 = (mU.xyz * 256.0 + mL.xyz) * (255.0 / 65535.0); + let nPos = mix(uniforms.sogMeansMin, uniforms.sogMeansMax, q16); + let center3 = sign(nPos) * (exp(abs(nPos)) - vec3f(1.0)); + splat.center = vec4f(center3, 1.0); + +#ifdef USE_SOG_V2 + let sIdx = floor(sRaw.xyz * 255.0 + 0.5); + var splatScale: vec3f; + splatScale.x = exp(textureLoad(sogCodebookTexture, vec2(i32(sIdx.x), 0), 0).r); + splatScale.y = exp(textureLoad(sogCodebookTexture, vec2(i32(sIdx.y), 0), 0).r); + splatScale.z = exp(textureLoad(sogCodebookTexture, vec2(i32(sIdx.z), 0), 0).r); +#else + let splatScale = exp(mix(uniforms.sogScalesMin, uniforms.sogScalesMax, sRaw.xyz)); +#endif + + let invSqrt2: f32 = 0.70710678118; + let qabc = (qRaw.xyz - vec3f(0.5)) * 2.0 * invSqrt2; + let qMode = i32(qRaw.w * 255.0 + 0.5) - 252; + let qd = sqrt(max(0.0, 1.0 - dot(qabc, qabc))); + var quat: vec4f; + if (qMode == 0) { quat = vec4f(qd, qabc.x, qabc.y, qabc.z); } + else if (qMode == 1) { quat = vec4f(qabc.x, qd, qabc.y, qabc.z); } + else if (qMode == 2) { quat = vec4f(qabc.x, qabc.y, qd, qabc.z); } + else { quat = vec4f(qabc.x, qabc.y, qabc.z, qd); } + + let qw = quat.x; let qx = quat.y; let qy = quat.z; let qz = quat.w; + let R = mat3x3( + 1.0 - 2.0*(qy*qy + qz*qz), 2.0*(qx*qy + qw*qz), 2.0*(qx*qz - qw*qy), + 2.0*(qx*qy - qw*qz), 1.0 - 2.0*(qx*qx + qz*qz), 2.0*(qy*qz + qw*qx), + 2.0*(qx*qz + qw*qy), 2.0*(qy*qz - qw*qx), 1.0 - 2.0*(qx*qx + qy*qy) + ); + let S2 = mat3x3( + 4.0*splatScale.x*splatScale.x, 0.0, 0.0, + 0.0, 4.0*splatScale.y*splatScale.y, 0.0, + 0.0, 0.0, 4.0*splatScale.z*splatScale.z + ); + let Sigma = R * S2 * transpose(R); + splat.covA = vec4f(Sigma[0][0], Sigma[0][1], Sigma[0][2], Sigma[1][1]); + splat.covB = vec4f(Sigma[1][2], Sigma[2][2], 0.0, 0.0); + + let SH_C0_SOG: f32 = 0.28209479177387814; +#ifdef USE_SOG_V2 + var c3: vec3f; + c3.x = textureLoad(sogCodebookTexture, vec2(256 + i32(c0.x * 255.0 + 0.5), 0), 0).r; + c3.y = textureLoad(sogCodebookTexture, vec2(256 + i32(c0.y * 255.0 + 0.5), 0), 0).r; + c3.z = textureLoad(sogCodebookTexture, vec2(256 + i32(c0.z * 255.0 + 0.5), 0), 0).r; + let colRgb = vec3f(0.5) + c3 * SH_C0_SOG; + let colA = c0.w; +#else + let cLerp = mix(uniforms.sogSh0Min, uniforms.sogSh0Max, c0); + let colRgb = vec3f(0.5) + cLerp.xyz * SH_C0_SOG; + let colA = 1.0 / (1.0 + exp(-cLerp.w)); +#endif + splat.color = vec4f(colRgb, colA); + splat.splatIndex = splatIndex; +#else splat.center = textureLoad(centersTexture, splatUVi32, 0); splat.color = textureLoad(colorsTexture, splatUVi32, 0); #if !defined(IS_FOR_VOXELIZATION) splat.covA = textureLoad(covariancesATexture, splatUVi32, 0) * splat.center.w; splat.covB = textureLoad(covariancesBTexture, splatUVi32, 0) * splat.center.w; #endif -#if SH_DEGREE > 0 +#endif +#if SH_DEGREE > 0 && !defined(USE_SOG) splat.sh0 = textureLoad(shTexture0, splatUVi32, 0); #endif -#if SH_DEGREE > 1 +#if SH_DEGREE > 1 && !defined(USE_SOG) splat.sh1 = textureLoad(shTexture1, splatUVi32, 0); #endif -#if SH_DEGREE > 2 +#if SH_DEGREE > 2 && !defined(USE_SOG) splat.sh2 = textureLoad(shTexture2, splatUVi32, 0); #endif -#if SH_DEGREE > 3 +#if SH_DEGREE > 3 && !defined(USE_SOG) splat.sh3 = textureLoad(shTexture3, splatUVi32, 0); splat.sh4 = textureLoad(shTexture4, splatUVi32, 0); #endif @@ -253,6 +320,48 @@ fn decompose(value: u32) -> vec4f return components * vec4f(2./255.) - vec4f(1.); } +#ifdef USE_SOG +fn computeSH(splat: Splat, dir: vec3f) -> vec3f +{ +#if SH_DEGREE > 0 + var sh: array, 25>; + sh[0] = vec3f(0., 0., 0.); + + let labelSize = textureDimensions(sogShNLabelsTexture, 0); + let idx = i32(splat.splatIndex + 0.5); + let lw = i32(labelSize.x); + let lx = idx - (idx / lw) * lw; + let ly = idx / lw; + let labelRaw = textureLoad(sogShNLabelsTexture, vec2(lx, ly), 0); + let n = i32(labelRaw.r * 255.0 + 0.5) + i32(labelRaw.g * 255.0 + 0.5) * 256; + + let coeffs = i32(uniforms.sogShCoeffCount + 0.5); + let u = (n - (n / 64) * 64) * coeffs; + let v = n / 64; + + for (var k: i32 = 0; k < 24; k = k + 1) { + if (k >= coeffs) { break; } + let centroidRaw = textureLoad(sogShNCentroidsTexture, vec2(u + k, v), 0); + var shCoeff: vec3f; +#ifdef USE_SOG_V2 + let rIdx = i32(centroidRaw.r * 255.0 + 0.5); + let gIdx = i32(centroidRaw.g * 255.0 + 0.5); + let bIdx = i32(centroidRaw.b * 255.0 + 0.5); + shCoeff.r = textureLoad(sogCodebookTexture, vec2(512 + rIdx, 0), 0).r; + shCoeff.g = textureLoad(sogCodebookTexture, vec2(512 + gIdx, 0), 0).r; + shCoeff.b = textureLoad(sogCodebookTexture, vec2(512 + bIdx, 0), 0).r; +#else + shCoeff = mix(vec3f(uniforms.sogShnMin), vec3f(uniforms.sogShnMax), centroidRaw.rgb); +#endif + sh[k + 1] = shCoeff; + } + + return computeColorFromSHDegree(dir, sh, 1., 1., 1., 1.); +#else + return vec3f(0., 0., 0.); +#endif +} +#else fn computeSHWeighted(splat: Splat, dir: vec3f, _so1: f32, _so2: f32, _so3: f32, _so4: f32) -> vec3f { var sh: array, 25>; @@ -341,6 +450,7 @@ fn computeSH(splat: Splat, dir: vec3f) -> vec3f #endif return computeSHWeighted(splat, dir, _w1, _w2, _w3, _w4); } +#endif fn gaussianSplatting( meshPos: vec2, diff --git a/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx b/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx index a203db4d695..80f31239997 100644 --- a/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx @@ -31,16 +31,38 @@ var covariancesATexture: texture_2d; var covariancesBTexture: texture_2d; var centersTexture: texture_2d; var colorsTexture: texture_2d; + +#ifdef USE_SOG +var sogQuatsTexture: texture_2d; +uniform sogMeansMin: vec3f; +uniform sogMeansMax: vec3f; +#ifdef USE_SOG_V2 +var sogCodebookTexture: texture_2d; +#else +uniform sogScalesMin: vec3f; +uniform sogScalesMax: vec3f; +uniform sogSh0Min: vec4f; +uniform sogSh0Max: vec4f; +uniform sogShnMin: f32; +uniform sogShnMax: f32; +#endif #if SH_DEGREE > 0 +var sogShNCentroidsTexture: texture_2d; +var sogShNLabelsTexture: texture_2d; +uniform sogShCoeffCount: f32; +#endif +#endif + +#if SH_DEGREE > 0 && !defined(USE_SOG) var shTexture0: texture_2d; #endif -#if SH_DEGREE > 1 +#if SH_DEGREE > 1 && !defined(USE_SOG) var shTexture1: texture_2d; #endif -#if SH_DEGREE > 2 +#if SH_DEGREE > 2 && !defined(USE_SOG) var shTexture2: texture_2d; #endif -#if SH_DEGREE > 3 +#if SH_DEGREE > 3 && !defined(USE_SOG) var shTexture3: texture_2d; var shTexture4: texture_2d; #endif diff --git a/packages/dev/loaders/src/SPLAT/sog.ts b/packages/dev/loaders/src/SPLAT/sog.ts index ed1dc5c16b0..e3b1c62178b 100644 --- a/packages/dev/loaders/src/SPLAT/sog.ts +++ b/packages/dev/loaders/src/SPLAT/sog.ts @@ -1,7 +1,9 @@ import { type Scene } from "core/scene"; -import { type IParsedSplat, Mode } from "./splatDefs"; +import { type IParsedSplat, type ISogTexturePack, Mode } from "./splatDefs"; import { Scalar } from "core/Maths/math.scalar"; import { type AbstractEngine } from "core/Engines"; +import { RawTexture } from "core/Materials/Textures/rawTexture"; +import { Constants } from "core/Engines/constants"; /** * Definition of a SOG data file @@ -413,3 +415,155 @@ export async function ParseSogMeta(dataOrFiles: SOGRootData | Map 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 + * @returns Parsed splat info with `sogTextures` populated. + */ +// eslint-disable-next-line @typescript-eslint/no-restricted-types +export async function ParseSogMetaAsTextures(dataOrFiles: SOGRootData | Map, rootUrl: string, scene: Scene): Promise { + let data: SOGRootData; + let files: Map | undefined; + + 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)) as SOGRootData; + } else { + data = dataOrFiles; + } + + const urls = [...data.means.files, ...data.scales.files, ...data.quats.files, ...data.sh0.files]; + if (data.shN) { + urls.push(...data.shN.files); + } + + const images: IWebPImage[] = await Promise.all( + urls.map(async (fileName) => { + if (files && files.has(fileName)) { + return await LoadWebpImageData(files.get(fileName)!, fileName, scene.getEngine()); + } + return await LoadWebpImageData(rootUrl, fileName, scene.getEngine()); + }) + ); + + const splatCount = data.count ?? data.means.shape[0]; + const engine = scene.getEngine(); + const splatTextureWidth = Math.min(splatCount, engine.getCaps().maxTextureSize); + const splatTextureHeight = Math.ceil(splatCount / splatTextureWidth); + + // means_l, means_u, scales, quats, sh0 share the same (w,h) + const meansL = CreateSogTexture(scene, images[0].bits, splatTextureWidth, splatTextureHeight); + const meansU = CreateSogTexture(scene, images[1].bits, splatTextureWidth, splatTextureHeight); + const scales = CreateSogTexture(scene, images[2].bits, splatTextureWidth, splatTextureHeight); + const quats = CreateSogTexture(scene, images[3].bits, splatTextureWidth, splatTextureHeight); + const sh0 = CreateSogTexture(scene, images[4].bits, splatTextureWidth, splatTextureHeight); + + let shCentroids: RawTexture | undefined; + let shLabels: RawTexture | undefined; + let shCoeffCount = 0; + let shDegree = 0; + + if (data.shN && images.length >= 7) { + const centroidsImage = images[5]; + const labelsImage = images[6]; + const centroidsHeight = centroidsImage.bits.length / 4 / centroidsImage.width; + shCentroids = CreateSogTexture(scene, centroidsImage.bits, centroidsImage.width, centroidsHeight); + const labelsHeight = labelsImage.bits.length / 4 / labelsImage.width; + shLabels = CreateSogTexture(scene, labelsImage.bits, labelsImage.width, labelsHeight); + + 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: RawTexture | undefined; + 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 = Constants.TEXTURE_CLAMP_ADDRESSMODE; + codebookTexture.wrapV = Constants.TEXTURE_CLAMP_ADDRESSMODE; + } + + const meansMins = data.means.mins as number[]; + const meansMaxs = data.means.maxs as number[]; + + const pack: ISogTexturePack = { + version: (data.version === 2 ? 2 : 1) as 1 | 2, + splatCount, + shDegree, + meansTextureL: meansL, + meansTextureU: meansU, + scalesTexture: scales, + quatsTexture: quats, + sh0Texture: sh0, + shCentroidsTexture: shCentroids, + shLabelsTexture: shLabels, + codebookTexture, + meansMin: [meansMins[0], meansMins[1], meansMins[2]], + meansMax: [meansMaxs[0], meansMaxs[1], meansMaxs[2]], + scalesMin: Array.isArray(data.scales.mins) ? [data.scales.mins[0], data.scales.mins[1], data.scales.mins[2]] : undefined, + scalesMax: Array.isArray(data.scales.maxs) ? [data.scales.maxs[0], data.scales.maxs[1], data.scales.maxs[2]] : undefined, + sh0Min: Array.isArray(data.sh0.mins) ? [data.sh0.mins[0], data.sh0.mins[1], data.sh0.mins[2], data.sh0.mins[3]] : undefined, + sh0Max: Array.isArray(data.sh0.maxs) ? [data.sh0.maxs[0], data.sh0.maxs[1], data.sh0.maxs[2], data.sh0.maxs[3]] : undefined, + shnMin: typeof data.shN?.mins === "number" ? data.shN.mins : undefined, + shnMax: typeof data.shN?.maxs === "number" ? data.shN.maxs : undefined, + shCoeffCount, + positions: DecodeSogPositions(data, images[0].bits, images[1].bits, splatCount), + }; + + return { mode: Mode.Splat, data: new ArrayBuffer(0), hasVertexColors: false, shDegree, sogTextures: pack }; +} diff --git a/packages/dev/loaders/src/SPLAT/splatDefs.ts b/packages/dev/loaders/src/SPLAT/splatDefs.ts index f667957fd0b..60245c2594f 100644 --- a/packages/dev/loaders/src/SPLAT/splatDefs.ts +++ b/packages/dev/loaders/src/SPLAT/splatDefs.ts @@ -1,28 +1,78 @@ -/** - * Indicator of the parsed ply buffer. A standard ready to use splat or an array of positions for a point cloud - */ -export const enum Mode { - Splat = 0, - PointCloud = 1, - Mesh = 2, - Reject = 3, -} - -/** - * A parsed buffer and how to use it - */ -export interface IParsedSplat { - data: ArrayBuffer; - mode: Mode; - faces?: number[]; - hasVertexColors?: boolean; - sh?: Uint8Array[]; - shDegree?: number; - trainedWithAntialiasing?: boolean; - compressed?: boolean; - rawSplat?: boolean; - safeOrbitCameraRadiusMin?: number; - safeOrbitCameraElevationMinMax?: [number, number]; - upAxis?: "X" | "Y" | "Z"; - chirality?: "LeftHanded" | "RightHanded"; -} +import { type BaseTexture } from "core/Materials/Textures/baseTexture.pure"; + +/** + * Indicator of the parsed ply buffer. A standard ready to use splat or an array of positions for a point cloud + */ +export const enum Mode { + Splat = 0, + PointCloud = 1, + Mesh = 2, + Reject = 3, +} + +/** + * SOG (Self-Organized Gaussians) raw texture set + decoding parameters. + * Used when SOG webp textures are fed directly to the GPU and dequantized in the shader. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface ISogTexturePack { + /** SOG file version (1 or 2). */ + version: 1 | 2; + /** Number of splats. */ + splatCount: number; + /** SH degree (0..3+). */ + shDegree: number; + + /** Raw webp textures, all RGBA8 with nearest sampling. */ + meansTextureL: BaseTexture; + meansTextureU: BaseTexture; + scalesTexture: BaseTexture; + quatsTexture: BaseTexture; + sh0Texture: BaseTexture; + shCentroidsTexture?: BaseTexture; + shLabelsTexture?: BaseTexture; + + /** Optional codebook (v2) packed into a 1D R32F texture. Encoding: + * - texels [0..255] : scales codebook + * - texels [256..511] : sh0 codebook + * - texels [512..767] : shN codebook + */ + codebookTexture?: BaseTexture; + + /** Mins/maxs (v1) used as uniforms. */ + meansMin: [number, number, number]; + meansMax: [number, number, number]; + scalesMin?: [number, number, number]; + scalesMax?: [number, number, number]; + sh0Min?: [number, number, number, number]; + sh0Max?: [number, number, number, number]; + shnMin?: number; + shnMax?: number; + + /** SH layout info. */ + shCoeffCount: number; + + /** CPU-side decoded positions for the depth-sort worker. */ + positions: Float32Array; +} + +/** + * A parsed buffer and how to use it + */ +export interface IParsedSplat { + data: ArrayBuffer; + mode: Mode; + faces?: number[]; + hasVertexColors?: boolean; + sh?: Uint8Array[]; + shDegree?: number; + trainedWithAntialiasing?: boolean; + compressed?: boolean; + rawSplat?: boolean; + safeOrbitCameraRadiusMin?: number; + safeOrbitCameraElevationMinMax?: [number, number]; + upAxis?: "X" | "Y" | "Z"; + chirality?: "LeftHanded" | "RightHanded"; + /** When set, the splats are to be uploaded as raw SOG textures and dequantized in the shader. */ + sogTextures?: ISogTexturePack; +} diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index 0bdac7d191f..c5a56860e8a 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -24,7 +24,7 @@ import { type SPLATLoadingOptions } from "./splatLoadingOptions"; import { type GaussianSplattingMaterial } from "core/Materials/GaussianSplatting/gaussianSplattingMaterial"; import { ConvertSpzToSplatAsync, GetSpzModule, ParseSpz } from "./spz"; import { Mode, type IParsedSplat } from "./splatDefs"; -import { ParseSogMeta, type SOGRootData } from "./sog"; +import { ParseSogMeta, ParseSogMetaAsTextures, type SOGRootData } from "./sog"; import { Tools } from "core/Misc/tools"; import { type ArcRotateCamera } from "core/Cameras/arcRotateCamera"; @@ -215,26 +215,38 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam, this._loadingOptions.needsRotationScaleTextures); gaussianSplatting._parentContainer = this._assetContainer; babylonMeshesArray.push(gaussianSplatting); - gaussianSplatting.updateData(parsedSOG.data, parsedSOG.sh, { flipY: false }, undefined, parsedSOG.shDegree); + if (parsedSOG.sogTextures) { + gaussianSplatting.setSogTextureData(parsedSOG.sogTextures); + } else { + gaussianSplatting.updateData(parsedSOG.data, parsedSOG.sh, { flipY: false }, undefined, parsedSOG.shDegree); + } gaussianSplatting.scaling.y *= -1; gaussianSplatting.computeWorldMatrix(true); scene._blockEntityCollection = false; }; + const engine = scene.getEngine(); + let useSogTextures = this._loadingOptions.useSogTextures; + if (useSogTextures && !engine.isWebGPU && engine.version < 2) { + Logger.Warn("SPLATFileLoader: useSogTextures requires WebGL2 or WebGPU. Falling back to CPU path."); + useSogTextures = false; + } + const sogParser = useSogTextures ? ParseSogMetaAsTextures : ParseSogMeta; + // check if data is json string if (typeof data === "string") { const dataSOG = JSON.parse(data) as SOGRootData; if (dataSOG && dataSOG.means && dataSOG.scales && dataSOG.quats && dataSOG.sh0) { - return new Promise((resolve) => { - ParseSogMeta(dataSOG, rootUrl, scene) + return new Promise((resolve, reject) => { + sogParser(dataSOG, rootUrl, scene) // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then .then((parsedSOG) => { makeGSFromParsedSOG(parsedSOG); resolve(babylonMeshesArray); }) // eslint-disable-next-line github/no-then - .catch(() => { - throw new Error("Failed to parse SOG data."); + .catch((e) => { + reject(new Error("Failed to parse SOG data.", { cause: e })); }); }); } @@ -243,17 +255,17 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu const u8 = data instanceof ArrayBuffer ? new Uint8Array(data) : data; // ZIP signature check for SOG if (u8[0] === 0x50 && u8[1] === 0x4b) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then this._unzipWithFFlateAsync(u8).then((files) => { - ParseSogMeta(files, rootUrl, scene) + sogParser(files, rootUrl, scene) // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then .then((parsedSOG) => { makeGSFromParsedSOG(parsedSOG); resolve(babylonMeshesArray); }) // eslint-disable-next-line github/no-then - .catch(() => { - throw new Error("Failed to parse SOG zip data."); + .catch((e) => { + reject(new Error("Failed to parse SOG zip data.", { cause: e })); }); }); }); diff --git a/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts b/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts index 8eb1c55fdd1..6a3af2b3789 100644 --- a/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts +++ b/packages/dev/loaders/src/SPLAT/splatLoadingOptions.ts @@ -40,6 +40,13 @@ export type SPLATLoadingOptions = { */ needsRotationScaleTextures?: boolean; + /** + * Load SOG files as raw GPU textures and dequantize in the shader. + * Skips the CPU decode pass and yields much faster load times. + * Requires WebGL2 / WebGPU. Defaults to false (CPU decode). + */ + useSogTextures?: boolean; + /** * URL to load the spz WASM ES module from (e.g. the \@adobe/spz package). * When provided, the WASM-based SPZ loader is used, which supports extra features diff --git a/packages/tools/tests/test/visualization/ReferenceImages/gsplat-sogs-sh-nolookup.png b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-sogs-sh-nolookup.png new file mode 100644 index 00000000000..6af8a3bf6bb Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-sogs-sh-nolookup.png differ diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index b228e43dbfd..f20ea7f5aa9 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -193,6 +193,14 @@ "dependsOn": ["Loaders"], "excludeFromPerformance": true }, + { + "title": "SOGS with SH no texture lookup", + "playgroundId": "#QA2662#24", + "errorRatio": 5, + "referenceImage": "gsplat-sogs-sh-nolookup.png", + "dependsOn": ["Loaders"], + "excludeFromPerformance": true + }, { "title": "Gaussian Splatting Part Test", "playgroundId": "#BTS11N#0",