diff --git a/package-lock.json b/package-lock.json index 996fcde0cee..742bd40b4b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23826,10 +23826,19 @@ "packages/dev/loaders": { "name": "@dev/loaders", "version": "1.0.0", + "dependencies": { + "@adobe/spz": "^0.2.0" + }, "devDependencies": { "@dev/core": "^1.0.0" } }, + "packages/dev/loaders/node_modules/@adobe/spz": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@adobe/spz/-/spz-0.1.9.tgz", + "integrity": "sha512-23MIO9Dat0+nIBXZqvBb8aKjXZ+Ps8CBm0NP5Q+CWDVkPEYZeiXp3ImFtufMEFOXU8e9oevp6BG8rEk/jyFmpg==", + "license": "ISC" + }, "packages/dev/lottiePlayer": { "name": "@dev/lottie-player", "version": "1.0.0", diff --git a/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.ts b/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.ts index 4c1350c2e20..89c1fd4bf75 100644 --- a/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.ts +++ b/packages/dev/core/src/Materials/GaussianSplatting/gaussianSplattingMaterial.ts @@ -178,7 +178,18 @@ export class GaussianSplattingMaterial extends PushMaterial { } protected static _Attribs = [VertexBuffer.PositionKind, "splatIndex0", "splatIndex1", "splatIndex2", "splatIndex3"]; - protected static _Samplers = ["covariancesATexture", "covariancesBTexture", "centersTexture", "colorsTexture", "shTexture0", "shTexture1", "shTexture2", "partIndicesTexture"]; + protected static _Samplers = [ + "covariancesATexture", + "covariancesBTexture", + "centersTexture", + "colorsTexture", + "shTexture0", + "shTexture1", + "shTexture2", + "shTexture3", + "shTexture4", + "partIndicesTexture", + ]; protected static _UniformBuffers = ["Scene", "Mesh"]; protected static _Uniforms = [ "world", @@ -435,7 +446,7 @@ export class GaussianSplattingMaterial extends PushMaterial { effect.setTexture("colorsTexture", gsMesh.colorsTexture); if (gsMesh.shTextures) { - for (let i = 0; i < gsMesh.shTextures?.length; i++) { + for (let i = 0; i < gsMesh.shTextures.length; i++) { effect.setTexture(`shTexture${i}`, gsMesh.shTextures[i]); } } diff --git a/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.ts b/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.ts index 98d73113619..4af6700b8c7 100644 --- a/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.ts +++ b/packages/dev/core/src/Meshes/GaussianSplatting/gaussianSplattingMeshBase.ts @@ -224,6 +224,33 @@ const enum PLYValue { SH_42, SH_43, SH_44, + SH_45, + SH_46, + SH_47, + SH_48, + SH_49, + SH_50, + SH_51, + SH_52, + SH_53, + SH_54, + SH_55, + SH_56, + SH_57, + SH_58, + SH_59, + SH_60, + SH_61, + SH_62, + SH_63, + SH_64, + SH_65, + SH_66, + SH_67, + SH_68, + SH_69, + SH_70, + SH_71, UNDEFINED, } @@ -344,6 +371,7 @@ export class GaussianSplattingMeshBase extends Mesh { private static _PlyConversionBatchSize = 32768; /** @internal */ public _shDegree = 0; + private _maxShDegree = 0; private static readonly _BatchSize = 16; // 16 splats per instance private _cameraViewInfos = new Map(); @@ -391,7 +419,7 @@ export class GaussianSplattingMeshBase extends Mesh { } public set shDegree(value: number) { - const maxDegree = this._shTextures?.length ?? 0; + const maxDegree = this._maxShDegree; const clamped = Math.max(0, Math.min(Math.round(value), maxDegree)); if (this._shDegree === clamped) { return; @@ -404,7 +432,7 @@ export class GaussianSplattingMeshBase extends Mesh { * Maximum SH degree available from the loaded data. */ public get maxShDegree() { - return this._shTextures?.length ?? 0; + return this._maxShDegree; } /** @@ -978,6 +1006,60 @@ export class GaussianSplattingMeshBase extends Mesh { return PLYValue.SH_43; case "f_rest_44": return PLYValue.SH_44; + case "f_rest_45": + return PLYValue.SH_45; + case "f_rest_46": + return PLYValue.SH_46; + case "f_rest_47": + return PLYValue.SH_47; + case "f_rest_48": + return PLYValue.SH_48; + case "f_rest_49": + return PLYValue.SH_49; + case "f_rest_50": + return PLYValue.SH_50; + case "f_rest_51": + return PLYValue.SH_51; + case "f_rest_52": + return PLYValue.SH_52; + case "f_rest_53": + return PLYValue.SH_53; + case "f_rest_54": + return PLYValue.SH_54; + case "f_rest_55": + return PLYValue.SH_55; + case "f_rest_56": + return PLYValue.SH_56; + case "f_rest_57": + return PLYValue.SH_57; + case "f_rest_58": + return PLYValue.SH_58; + case "f_rest_59": + return PLYValue.SH_59; + case "f_rest_60": + return PLYValue.SH_60; + case "f_rest_61": + return PLYValue.SH_61; + case "f_rest_62": + return PLYValue.SH_62; + case "f_rest_63": + return PLYValue.SH_63; + case "f_rest_64": + return PLYValue.SH_64; + case "f_rest_65": + return PLYValue.SH_65; + case "f_rest_66": + return PLYValue.SH_66; + case "f_rest_67": + return PLYValue.SH_67; + case "f_rest_68": + return PLYValue.SH_68; + case "f_rest_69": + return PLYValue.SH_69; + case "f_rest_70": + return PLYValue.SH_70; + case "f_rest_71": + return PLYValue.SH_71; } return PLYValue.UNDEFINED; @@ -1032,10 +1114,12 @@ export class GaussianSplattingMeshBase extends Mesh { const value = GaussianSplattingMeshBase._ValueNameToEnum(name); if (value != PLYValue.UNDEFINED) { - // SH degree 1,2 or 3 for 9, 24 or 45 values - if (value >= PLYValue.SH_44) { - shDegree = 3; - } else if (value >= PLYValue.SH_24) { + // SH degree 1,2,3 or 4 for 9, 24, 45 or 72 values + if (value >= PLYValue.SH_71) { + shDegree = 4; + } else if (value >= PLYValue.SH_44) { + shDegree = Math.max(shDegree, 3); + } else if (value >= PLYValue.SH_23) { shDegree = Math.max(shDegree, 2); } else if (value >= PLYValue.SH_8) { shDegree = Math.max(shDegree, 1); @@ -1323,7 +1407,7 @@ export class GaussianSplattingMeshBase extends Mesh { r3 = value; break; } - if (sh && property.value >= PLYValue.SH_0 && property.value <= PLYValue.SH_44) { + if (sh && property.value >= PLYValue.SH_0 && property.value <= PLYValue.SH_71) { const shIndex = property.value - PLYValue.SH_0; if (property.type == PLYType.UCHAR && header.chunkCount) { // compressed ply. dataView points to beginning of vertex @@ -1341,7 +1425,7 @@ export class GaussianSplattingMeshBase extends Mesh { } if (sh) { - const shDim = header.shDegree == 1 ? 3 : header.shDegree == 2 ? 8 : 15; + const shDim = header.shDegree == 1 ? 3 : header.shDegree == 2 ? 8 : header.shDegree == 3 ? 15 : 24; for (let j = 0; j < shDim; j++) { sh[j * 3 + 0] = plySH[j]; sh[j * 3 + 1] = plySH[j + shDim]; @@ -1417,7 +1501,7 @@ export class GaussianSplattingMeshBase extends Mesh { } } - return { buffer: header.buffer, sh: sh }; + return { buffer: header.buffer, sh: sh, shDegree: header.shDegree }; } /** @@ -1909,7 +1993,8 @@ export class GaussianSplattingMeshBase extends Mesh { isAsync: boolean, sh?: Uint8Array[], partIndices?: Uint8Array, - { flipY = false, previousVertexCount = 0 }: IUpdateOptions = {} + { flipY = false, previousVertexCount = 0 }: IUpdateOptions = {}, + shDegree?: number ): Coroutine { if (!this._covariancesATexture) { this._readyToDisplay = false; @@ -1935,8 +2020,8 @@ export class GaussianSplattingMeshBase extends Mesh { this._updateSplatIndexBuffer(vertexCount); } this._vertexCount = vertexCount; - // degree == 1 for 1 texture (3 terms), 2 for 2 textures (8 terms) and 3 for 3 textures (15 terms) - this._shDegree = sh ? sh.length : 0; + this._maxShDegree = sh ? (shDegree ?? 0) : 0; + this._shDegree = this._maxShDegree; const textureSize = this._getTextureSize(vertexCount); const textureLength = textureSize.x * textureSize.y; @@ -2058,10 +2143,11 @@ export class GaussianSplattingMeshBase extends Mesh { * @param data array buffer containing center, color, orientation and scale of splats * @param sh optional array of uint8 array for SH data * @param partIndices optional array of uint8 for rig node indices + * @param shDegree optional SH degree of the data * @returns a promise */ - public async updateDataAsync(data: ArrayBuffer, sh?: Uint8Array[], partIndices?: Uint8Array): Promise { - return await runCoroutineAsync(this._updateData(data, true, sh, partIndices), createYieldingScheduler()); + public async updateDataAsync(data: ArrayBuffer, sh?: Uint8Array[], partIndices?: Uint8Array, shDegree?: number): Promise { + return await runCoroutineAsync(this._updateData(data, true, sh, partIndices, undefined, shDegree), createYieldingScheduler()); } /** @@ -2071,9 +2157,10 @@ export class GaussianSplattingMeshBase extends Mesh { * @param sh optional array of uint8 array for SH data * @param options optional informations on how to treat data (needs to be 3rd for backward compatibility) * @param partIndices optional array of uint8 for rig node indices + * @param shDegree optional SH degree of the data */ - public updateData(data: ArrayBuffer, sh?: Uint8Array[], options: IUpdateOptions = { flipY: true }, partIndices?: Uint8Array): void { - runCoroutineSync(this._updateData(data, false, sh, partIndices, options)); + public updateData(data: ArrayBuffer, sh?: Uint8Array[], options: IUpdateOptions = { flipY: true }, partIndices?: Uint8Array, shDegree?: number): void { + runCoroutineSync(this._updateData(data, false, sh, partIndices, options, shDegree)); } /** diff --git a/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx b/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx index 6f1e9feaa96..514bd6ccf05 100644 --- a/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx +++ b/packages/dev/core/src/Shaders/ShadersInclude/gaussianSplatting.fx @@ -34,6 +34,10 @@ struct Splat { #if SH_DEGREE > 2 uvec4 sh2; #endif +#if SH_DEGREE > 3 + uvec4 sh3; + uvec4 sh4; +#endif #if IS_COMPOUND uint partIndex; #endif @@ -87,6 +91,10 @@ Splat readSplat(float splatIndex) #if SH_DEGREE > 2 splat.sh2 = texelFetch(shTexture2, splatUVint, 0); #endif +#if SH_DEGREE > 3 + splat.sh3 = texelFetch(shTexture3, splatUVint, 0); + splat.sh4 = texelFetch(shTexture4, splatUVint, 0); +#endif #if IS_COMPOUND splat.partIndex = uint(texture2D(partIndicesTexture, splatUV).r * 255.0 + 0.5); #endif @@ -96,7 +104,7 @@ Splat readSplat(float splatIndex) #if defined(WEBGL2) || defined(WEBGPU) || defined(NATIVE) // no SH for GS and WebGL1 // dir = normalized(splat pos - cam pos) -vec3 computeColorFromSHDegree(vec3 dir, const vec3 sh[16]) +vec3 computeColorFromSHDegree(vec3 dir, const vec3 sh[25]) { const float SH_C0 = 0.28209479; const float SH_C1 = 0.48860251; @@ -116,6 +124,17 @@ vec3 computeColorFromSHDegree(vec3 dir, const vec3 sh[16]) SH_C3[5] = 1.445305721; SH_C3[6] = -0.59004358; + float SH_C4[9]; + SH_C4[0] = 2.5033429418; + SH_C4[1] = -1.7701307698; + SH_C4[2] = 0.9461746958; + SH_C4[3] = -0.6690465436; + SH_C4[4] = 0.1057855469; + SH_C4[5] = -0.6690465436; + SH_C4[6] = 0.4730873479; + SH_C4[7] = -1.7701307698; + SH_C4[8] = 0.6258357354; + vec3 result = /*SH_C0 * */sh[0]; #if SH_DEGREE > 0 @@ -143,6 +162,19 @@ vec3 computeColorFromSHDegree(vec3 dir, const vec3 sh[16]) SH_C3[4] * x * (4.0 * zz - xx - yy) * sh[13] + SH_C3[5] * z * (xx - yy) * sh[14] + SH_C3[6] * x * (xx - 3.0 * yy) * sh[15]; + +#if SH_DEGREE > 3 + result += + SH_C4[0] * x * y * (xx - yy) * sh[16] + + SH_C4[1] * y * z * (3.0 * xx - yy) * sh[17] + + SH_C4[2] * x * y * (7.0 * zz - 1.0) * sh[18] + + SH_C4[3] * y * z * (7.0 * zz - 3.0) * sh[19] + + SH_C4[4] * (zz * (35.0 * zz - 30.0) + 3.0) * sh[20] + + SH_C4[5] * x * z * (7.0 * zz - 3.0) * sh[21] + + SH_C4[6] * (xx - yy) * (7.0 * zz - 1.0) * sh[22] + + SH_C4[7] * x * z * (xx - 3.0 * yy) * sh[23] + + SH_C4[8] * (xx * (xx - 3.0 * yy) - yy * (3.0 * xx - yy)) * sh[24]; +#endif #endif #endif #endif @@ -163,7 +195,7 @@ vec4 decompose(uint value) vec3 computeSH(Splat splat, vec3 dir) { - vec3 sh[16]; + vec3 sh[25]; sh[0] = vec3(0.,0.,0.); #if SH_DEGREE > 0 @@ -200,7 +232,26 @@ vec3 computeSH(Splat splat, vec3 dir) sh[12] = vec3(sh08.y, sh08.z, sh08.w); sh[13] = vec3(sh09.x, sh09.y, sh09.z); sh[14] = vec3(sh09.w, sh10.x, sh10.y); - sh[15] = vec3(sh10.z, sh10.w, sh11.x); + sh[15] = vec3(sh10.z, sh10.w, sh11.x); +#endif +#if SH_DEGREE > 3 + // sh[16] R/G/B are in sh11.y/z/w (j=45,46,47 — last 3 bytes of texture2) + vec4 sh12 = decompose(splat.sh3.x); + vec4 sh13 = decompose(splat.sh3.y); + vec4 sh14 = decompose(splat.sh3.z); + vec4 sh15 = decompose(splat.sh3.w); + vec4 sh16 = decompose(splat.sh4.x); + vec4 sh17 = decompose(splat.sh4.y); + + sh[16] = vec3(sh11.y, sh11.z, sh11.w); + sh[17] = vec3(sh12.x, sh12.y, sh12.z); + sh[18] = vec3(sh12.w, sh13.x, sh13.y); + sh[19] = vec3(sh13.z, sh13.w, sh14.x); + sh[20] = vec3(sh14.y, sh14.z, sh14.w); + sh[21] = vec3(sh15.x, sh15.y, sh15.z); + sh[22] = vec3(sh15.w, sh16.x, sh16.y); + sh[23] = vec3(sh16.z, sh16.w, sh17.x); + sh[24] = vec3(sh17.y, sh17.z, sh17.w); #endif return computeColorFromSHDegree(dir, sh); diff --git a/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx b/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx index 9d6940174c1..b0ed876d14d 100644 --- a/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx +++ b/packages/dev/core/src/Shaders/gaussianSplatting.vertex.fx @@ -37,6 +37,10 @@ uniform highp usampler2D shTexture1; #if SH_DEGREE > 2 uniform highp usampler2D shTexture2; #endif +#if SH_DEGREE > 3 +uniform highp usampler2D shTexture3; +uniform highp usampler2D shTexture4; +#endif #if IS_COMPOUND uniform sampler2D partIndicesTexture; diff --git a/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx b/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx index 31d8e1849f0..fe63ba8d410 100644 --- a/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx +++ b/packages/dev/core/src/ShadersWGSL/ShadersInclude/gaussianSplatting.fx @@ -18,6 +18,10 @@ struct Splat { #if SH_DEGREE > 2 sh2: vec4, #endif +#if SH_DEGREE > 3 + sh3: vec4, + sh4: vec4, +#endif #if IS_COMPOUND partIndex: u32, #endif @@ -131,13 +135,17 @@ fn readSplat(splatIndex: f32, dataTextureSize: vec2f) -> Splat { #if SH_DEGREE > 2 splat.sh2 = textureLoad(shTexture2, splatUVi32, 0); #endif +#if SH_DEGREE > 3 + splat.sh3 = textureLoad(shTexture3, splatUVi32, 0); + splat.sh4 = textureLoad(shTexture4, splatUVi32, 0); +#endif #if IS_COMPOUND splat.partIndex = u32(textureLoad(partIndicesTexture, splatUVi32, 0).r * 255.0 + 0.5); #endif return splat; } -fn computeColorFromSHDegree(dir: vec3f, sh: array, 16>) -> vec3f +fn computeColorFromSHDegree(dir: vec3f, sh: array, 25>) -> vec3f { let SH_C0: f32 = 0.28209479; let SH_C1: f32 = 0.48860251; @@ -159,6 +167,18 @@ fn computeColorFromSHDegree(dir: vec3f, sh: array, 16>) -> vec3f -0.59004358 ); + var SH_C4: array = array( + 2.5033429418, + -1.7701307698, + 0.9461746958, + -0.6690465436, + 0.1057855469, + -0.6690465436, + 0.4730873479, + -1.7701307698, + 0.6258357354 + ); + var result: vec3f = /*SH_C0 * */sh[0]; #if SH_DEGREE > 0 @@ -174,7 +194,7 @@ fn computeColorFromSHDegree(dir: vec3f, sh: array, 16>) -> vec3f let xy: f32 = x * y; let yz: f32 = y * z; let xz: f32 = x * z; - result += + result += SH_C2[0] * xy * sh[4] + SH_C2[1] * yz * sh[5] + SH_C2[2] * (2.0f * zz - xx - yy) * sh[6] + @@ -182,7 +202,7 @@ fn computeColorFromSHDegree(dir: vec3f, sh: array, 16>) -> vec3f SH_C2[4] * (xx - yy) * sh[8]; #if SH_DEGREE > 2 - result += + result += SH_C3[0] * y * (3.0f * xx - yy) * sh[9] + SH_C3[1] * xy * z * sh[10] + SH_C3[2] * y * (4.0f * zz - xx - yy) * sh[11] + @@ -190,6 +210,19 @@ fn computeColorFromSHDegree(dir: vec3f, sh: array, 16>) -> vec3f SH_C3[4] * x * (4.0f * zz - xx - yy) * sh[13] + SH_C3[5] * z * (xx - yy) * sh[14] + SH_C3[6] * x * (xx - 3.0f * yy) * sh[15]; + +#if SH_DEGREE > 3 + result += + SH_C4[0] * x * y * (xx - yy) * sh[16] + + SH_C4[1] * y * z * (3.0f * xx - yy) * sh[17] + + SH_C4[2] * x * y * (7.0f * zz - 1.0f) * sh[18] + + SH_C4[3] * y * z * (7.0f * zz - 3.0f) * sh[19] + + SH_C4[4] * (zz * (35.0f * zz - 30.0f) + 3.0f) * sh[20] + + SH_C4[5] * x * z * (7.0f * zz - 3.0f) * sh[21] + + SH_C4[6] * (xx - yy) * (7.0f * zz - 1.0f) * sh[22] + + SH_C4[7] * x * z * (xx - 3.0f * yy) * sh[23] + + SH_C4[8] * (xx * (xx - 3.0f * yy) - yy * (3.0f * xx - yy)) * sh[24]; +#endif #endif #endif #endif @@ -210,7 +243,7 @@ fn decompose(value: u32) -> vec4f fn computeSH(splat: Splat, dir: vec3f) -> vec3f { - var sh: array, 16>; + var sh: array, 25>; sh[0] = vec3f(0., 0., 0.); @@ -248,7 +281,26 @@ fn computeSH(splat: Splat, dir: vec3f) -> vec3f sh[12] = vec3f(sh08.y, sh08.z, sh08.w); sh[13] = vec3f(sh09.x, sh09.y, sh09.z); sh[14] = vec3f(sh09.w, sh10.x, sh10.y); - sh[15] = vec3f(sh10.z, sh10.w, sh11.x); + sh[15] = vec3f(sh10.z, sh10.w, sh11.x); +#endif +#if SH_DEGREE > 3 + // sh[16] R/G/B are in sh11.y/z/w (j=45,46,47 — last 3 bytes of texture2) + let sh12: vec4f = decompose(splat.sh3.x); + let sh13: vec4f = decompose(splat.sh3.y); + let sh14: vec4f = decompose(splat.sh3.z); + let sh15: vec4f = decompose(splat.sh3.w); + let sh16: vec4f = decompose(splat.sh4.x); + let sh17: vec4f = decompose(splat.sh4.y); + + sh[16] = vec3f(sh11.y, sh11.z, sh11.w); + sh[17] = vec3f(sh12.x, sh12.y, sh12.z); + sh[18] = vec3f(sh12.w, sh13.x, sh13.y); + sh[19] = vec3f(sh13.z, sh13.w, sh14.x); + sh[20] = vec3f(sh14.y, sh14.z, sh14.w); + sh[21] = vec3f(sh15.x, sh15.y, sh15.z); + sh[22] = vec3f(sh15.w, sh16.x, sh16.y); + sh[23] = vec3f(sh16.z, sh16.w, sh17.x); + sh[24] = vec3f(sh17.y, sh17.z, sh17.w); #endif return computeColorFromSHDegree(dir, sh); diff --git a/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx b/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx index 32cf42cf5db..4c57c93a321 100644 --- a/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx +++ b/packages/dev/core/src/ShadersWGSL/gaussianSplatting.vertex.fx @@ -40,6 +40,10 @@ var shTexture1: texture_2d; #if SH_DEGREE > 2 var shTexture2: texture_2d; #endif +#if SH_DEGREE > 3 +var shTexture3: texture_2d; +var shTexture4: texture_2d; +#endif #if IS_COMPOUND var partIndicesTexture: texture_2d; #endif diff --git a/packages/dev/loaders/package.json b/packages/dev/loaders/package.json index 268bbb4af8e..2ba0e2e7a6e 100644 --- a/packages/dev/loaders/package.json +++ b/packages/dev/loaders/package.json @@ -21,5 +21,7 @@ "@dev/core": "^1.0.0" }, "sideEffects": true, - "dependencies": {} + "dependencies": { + "@adobe/spz": "^0.2.0" + } } diff --git a/packages/dev/loaders/src/SPLAT/sog.ts b/packages/dev/loaders/src/SPLAT/sog.ts index 58e00ef737f..3243547c273 100644 --- a/packages/dev/loaders/src/SPLAT/sog.ts +++ b/packages/dev/loaders/src/SPLAT/sog.ts @@ -289,8 +289,8 @@ async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], s // --- SH if (data.shN) { - const coeffCounts = [0, 3, 8, 15]; - const coeffs = data.shN.bands ? coeffCounts[data.shN.bands] : data.shN.shape[1] / 3; // 3 components per coeff + const coeffs = data.shN.bands ? (data.shN.bands + 1) ** 2 - 1 : data.shN.shape[1] / 3; // 3 components per coeff + const shDegree = 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; @@ -358,7 +358,7 @@ async function ParseSogDatas(data: SOGRootData, imageDataArrays: IWebPImage[], s } } return await new Promise((resolve) => { - resolve({ mode: Mode.Splat, data: buffer, hasVertexColors: false, sh: sh }); + resolve({ mode: Mode.Splat, data: buffer, hasVertexColors: false, sh: sh, shDegree: shDegree }); }); } diff --git a/packages/dev/loaders/src/SPLAT/splatDefs.ts b/packages/dev/loaders/src/SPLAT/splatDefs.ts index 04a00310c33..f667957fd0b 100644 --- a/packages/dev/loaders/src/SPLAT/splatDefs.ts +++ b/packages/dev/loaders/src/SPLAT/splatDefs.ts @@ -17,6 +17,7 @@ export interface IParsedSplat { faces?: number[]; hasVertexColors?: boolean; sh?: Uint8Array[]; + shDegree?: number; trainedWithAntialiasing?: boolean; compressed?: boolean; rawSplat?: boolean; diff --git a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts index 2c868cd258b..ae646c73ef1 100644 --- a/packages/dev/loaders/src/SPLAT/splatFileLoader.ts +++ b/packages/dev/loaders/src/SPLAT/splatFileLoader.ts @@ -22,7 +22,7 @@ import { Color4 } from "core/Maths/math.color"; import { VertexData } from "core/Meshes/mesh.vertexData"; import { type SPLATLoadingOptions } from "./splatLoadingOptions"; import { type GaussianSplattingMaterial } from "core/Materials/GaussianSplatting/gaussianSplattingMaterial"; -import { ParseSpz } from "./spz"; +import { ConvertSpzToSplatAsync, GetSpzModule } from "./spz"; import { Mode, type IParsedSplat } from "./splatDefs"; import { ParseSogMeta, type SOGRootData } from "./sog"; import { Tools } from "core/Misc/tools"; @@ -211,7 +211,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu const gaussianSplatting = this._loadingOptions.gaussianSplattingMesh ?? new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam); gaussianSplatting._parentContainer = this._assetContainer; babylonMeshesArray.push(gaussianSplatting); - gaussianSplatting.updateData(parsedSOG.data, parsedSOG.sh, { flipY: false }); + gaussianSplatting.updateData(parsedSOG.data, parsedSOG.sh, { flipY: false }, undefined, parsedSOG.shDegree); gaussianSplatting.scaling.y *= -1; gaussianSplatting.computeWorldMatrix(true); scene._blockEntityCollection = false; @@ -266,7 +266,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu this._loadingOptions.gaussianSplattingMesh ?? new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam); gaussianSplatting._parentContainer = this._assetContainer; babylonMeshesArray.push(gaussianSplatting); - gaussianSplatting.updateData(parsedPLY.data, parsedPLY.sh, { flipY: false }); + gaussianSplatting.updateData(parsedPLY.data, parsedPLY.sh, { flipY: false }, undefined, parsedPLY.shDegree); gaussianSplatting.scaling.y *= -1.0; if (parsedPLY.chirality === "RightHanded") { @@ -318,54 +318,36 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu }); }; - // Check for gzip magic bytes (SPZ format) before attempting decompression + // Check for gzip magic bytes to detect SPZ format if (u8[0] !== 0x1f || u8[1] !== 0x8b) { return new Promise((resolve) => { handlePLY(resolve); }); } - // Use GZip DecompressionStream for SPZ files - const readableStream = new ReadableStream({ - start(controller) { - controller.enqueue(new Uint8Array(data)); - controller.close(); - }, - }); - const decompressionStream = new DecompressionStream("gzip"); - const decompressedStream = readableStream.pipeThrough(decompressionStream); - - return new Promise((resolve) => { - new Response(decompressedStream) - .arrayBuffer() - // eslint-disable-next-line github/no-then - .then((buffer) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises, github/no-then - ParseSpz(buffer, scene, this._loadingOptions).then((parsedSPZ) => { - scene._blockEntityCollection = !!this._assetContainer; - const gaussianSplatting = - this._loadingOptions.gaussianSplattingMesh ?? new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam); - if (parsedSPZ.trainedWithAntialiasing) { - const gsMaterial = gaussianSplatting.material as GaussianSplattingMaterial; - gsMaterial.kernelSize = 0.1; - gsMaterial.compensation = true; - } - gaussianSplatting._parentContainer = this._assetContainer; - babylonMeshesArray.push(gaussianSplatting); - gaussianSplatting.updateData(parsedSPZ.data, parsedSPZ.sh, { flipY: false }); - if (!this._loadingOptions.flipY) { - gaussianSplatting.scaling.y *= -1.0; - gaussianSplatting.computeWorldMatrix(true); - } - scene._blockEntityCollection = false; - this.applyAutoCameraLimits(parsedSPZ, scene); - resolve(babylonMeshesArray); - }); - }) - // eslint-disable-next-line github/no-then - .catch(() => { - handlePLY(resolve); - }); + // eslint-disable-next-line github/no-then + return GetSpzModule().then((spz) => { + const cloud = spz.loadSpzFromBuffer(new Uint8Array(data), { to: spz.CoordinateSystem.RUB }); + // eslint-disable-next-line github/no-then + return ConvertSpzToSplatAsync(cloud, scene).then((parsedSPZ) => { + scene._blockEntityCollection = !!this._assetContainer; + const gaussianSplatting = this._loadingOptions.gaussianSplattingMesh ?? new GaussianSplattingMesh("GaussianSplatting", null, scene, this._loadingOptions.keepInRam); + if (parsedSPZ.trainedWithAntialiasing) { + const gsMaterial = gaussianSplatting.material as GaussianSplattingMaterial; + gsMaterial.kernelSize = 0.1; + gsMaterial.compensation = true; + } + gaussianSplatting._parentContainer = this._assetContainer; + babylonMeshesArray.push(gaussianSplatting); + gaussianSplatting.updateData(parsedSPZ.data, parsedSPZ.sh, { flipY: false }, undefined, parsedSPZ.shDegree); + if (!this._loadingOptions.flipY) { + gaussianSplatting.scaling.y *= -1.0; + gaussianSplatting.computeWorldMatrix(true); + } + scene._blockEntityCollection = false; + this.applyAutoCameraLimits(parsedSPZ, scene); + return babylonMeshesArray; + }); }); } @@ -578,7 +560,16 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu // early exit for chunked/quantized ply if (chunkCount) { return await new Promise((resolve) => { - resolve({ mode: Mode.Splat, data: splatsData.buffer, sh: splatsData.sh, faces: faces, hasVertexColors: false, compressed: true, rawSplat: false }); + resolve({ + mode: Mode.Splat, + data: splatsData.buffer, + sh: splatsData.sh, + shDegree: splatsData.shDegree, + faces: faces, + hasVertexColors: false, + compressed: true, + rawSplat: false, + }); }); } // count available properties. if all necessary are present then it's a splat. Otherwise, it's a point cloud @@ -605,6 +596,7 @@ export class SPLATFileLoader implements ISceneLoaderPluginAsync, ISceneLoaderPlu mode: currentMode, data: splatsData.buffer, sh: splatsData.sh, + shDegree: splatsData.shDegree, faces: faces, hasVertexColors: !!propertyColorCount, compressed: false, diff --git a/packages/dev/loaders/src/SPLAT/spz.ts b/packages/dev/loaders/src/SPLAT/spz.ts index 93c52291cad..d55507815bd 100644 --- a/packages/dev/loaders/src/SPLAT/spz.ts +++ b/packages/dev/loaders/src/SPLAT/spz.ts @@ -1,201 +1,179 @@ -/* eslint-disable @typescript-eslint/promise-function-async */ -import { Scalar } from "core/Maths/math.scalar"; import { type Scene } from "core/scene"; -import { type SPLATLoadingOptions } from "./splatLoadingOptions"; +import { runCoroutineAsync, createYieldingScheduler, type Coroutine } from "core/Misc/coroutine"; import { Mode, type IParsedSplat } from "./splatDefs"; +const _SpzConversionBatchSize = 32768; +const _SH_C0 = 0.28209479177387814; + +// Cached WASM module promise — initialized once, reused across all SPZ loads. +let _SpzModulePromise: Promise | null = null; + /** - * Parses SPZ data and returns a promise resolving to an IParsedPLY object. - * @param data The ArrayBuffer containing SPZ data. - * @param scene The Babylon.js scene. - * @param loadingOptions Options for loading Gaussian Splatting files. - * @returns A promise resolving to the parsed SPZ data. + * Returns the initialized \@adobe/spz WASM module, loading it on first call. + * @returns A promise resolving to the initialized spz WASM module */ -export function ParseSpz(data: ArrayBuffer, scene: Scene, loadingOptions: SPLATLoadingOptions): Promise { - const ubuf = new Uint8Array(data); - const ubufu32 = new Uint32Array(data.slice(0, 12)); // Only need ubufu32[0] to [2] - // debug infos - const splatCount = ubufu32[2]; - - const shDegree = ubuf[12]; - const fractionalBits = ubuf[13]; - const flags = ubuf[14]; - const reserved = ubuf[15]; - const version = ubufu32[1]; - - // check magic and version - if (reserved || ubufu32[0] != 0x5053474e || (version != 2 && version != 3)) { - // reserved must be 0 - return new Promise((resolve) => { - resolve({ mode: Mode.Reject, data: buffer, hasVertexColors: false }); - }); +export async function GetSpzModule(): Promise { + if (!_SpzModulePromise) { + _SpzModulePromise = (async () => { + const { default: createSpzModule } = await import("@adobe/spz"); + return await createSpzModule(); + })(); } + return await _SpzModulePromise; +} - const rowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // 32 +/** + * Converts a GaussianCloud object (from \@adobe/spz) into the packed 32-byte-per-splat + * ArrayBuffer and SH texture arrays expected by GaussianSplattingMeshBase.updateData. + * + * Packed layout per splat (32 bytes): + * [0-11] position xyz (float32 x3) + * [12-23] scale xyz (float32 x3) + * [24-27] color RGBA (uint8 x4, colors in [0,255], alpha in [0,255]) + * [28-31] quaternion wxyz (uint8 x4, encoded as q * 127.5 + 127.5) + * + * SH coefficients from the cloud (Float32, range ~[-1,1]) are encoded to bytes + * using the same convention as the PLY converter: byte = coeff * 127.5 + 127.5. + * + * @param cloud The GaussianCloud returned by spz.loadSpzFromBuffer + * @param scene The Babylon.js scene (used to query maxTextureSize for SH textures) + * @param useCoroutine If true, yields periodically to avoid blocking the main thread + * @returns A coroutine returning an IParsedSplat ready to be passed to updateData + */ +export function* ConvertSpzToSplat(cloud: any, scene: Scene, useCoroutine = false): Coroutine { + const splatCount: number = cloud.numPoints; + const rowOutputLength = 3 * 4 + 3 * 4 + 4 + 4; // 32 bytes const buffer = new ArrayBuffer(rowOutputLength * splatCount); + const fBuffer = new Float32Array(buffer); + const uBuffer = new Uint8Array(buffer); + + const positions: Float32Array = cloud.positions; + const scales: Float32Array = cloud.scales; + const colors: Float32Array = cloud.colors; + const alphas: Float32Array = cloud.alphas; + const rotations: Float32Array = cloud.rotations; + + // Build SH texture arrays upfront so both main and SH data can be written in a single pass + let sh: Uint8Array[] | null = null; + const shDegree: number = cloud.shDegree; + let cloudSh: Float32Array | null = null; + let shComponentCount = 0; + let chunkStarts: Int32Array | null = null; + let chunkEnds: Int32Array | null = null; + let shArrays: Uint8Array[] | null = null; + + if (shDegree > 0 && cloud.sh.length > 0) { + const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1; + shComponentCount = shVectorCount * 3; + const textureCount = Math.ceil(shComponentCount / 16); - const positionScale = 1.0 / (1 << fractionalBits); - - const int32View = new Int32Array(1); - const uint8View = new Uint8Array(int32View.buffer); - const read24bComponent = function (u8: Uint8Array, offset: number) { - uint8View[0] = u8[offset + 0]; - uint8View[1] = u8[offset + 1]; - uint8View[2] = u8[offset + 2]; - uint8View[3] = u8[offset + 2] & 0x80 ? 0xff : 0x00; - return int32View[0] * positionScale; - }; - - let byteOffset = 16; - - const position = new Float32Array(buffer); - const scale = new Float32Array(buffer); - const rgba = new Uint8ClampedArray(buffer); - const rot = new Uint8ClampedArray(buffer); - - // positions - for (let i = 0; i < splatCount; i++) { - position[i * 8 + 0] = read24bComponent(ubuf, byteOffset + 0); - position[i * 8 + 1] = read24bComponent(ubuf, byteOffset + 3); - position[i * 8 + 2] = read24bComponent(ubuf, byteOffset + 6); - byteOffset += 9; - } - - // colors - const shC0 = 0.282; - for (let i = 0; i < splatCount; i++) { - for (let component = 0; component < 3; component++) { - const byteValue = ubuf[byteOffset + splatCount + i * 3 + component]; - // 0.15 is hard coded value from spz - // Scale factor for DC color components. To convert to RGB, we should multiply by 0.282, but it can - // be useful to represent base colors that are out of range if the higher spherical harmonics bands - // bring them back into range so we multiply by a smaller value. - const value = (byteValue - 127.5) / (0.15 * 255); - rgba[i * 32 + 24 + component] = Scalar.Clamp((0.5 + shC0 * value) * 255, 0, 255); + const engine = scene.getEngine(); + const width = engine.getCaps().maxTextureSize; + const height = Math.ceil(splatCount / width); + sh = []; + for (let t = 0; t < textureCount; t++) { + sh.push(new Uint8Array(height * width * 4 * 4)); } - rgba[i * 32 + 24 + 3] = ubuf[byteOffset + i]; + // Precompute chunk start/end and hoist texture references out of the per-splat loop + chunkStarts = new Int32Array(textureCount); + chunkEnds = new Int32Array(textureCount); + for (let t = 0; t < textureCount; t++) { + chunkStarts[t] = t * 16; + chunkEnds[t] = Math.min((t + 1) * 16, shComponentCount); + } + shArrays = sh; + cloudSh = cloud.sh; } - byteOffset += splatCount * 4; - // scales + // Single pass: write packed splat data and SH textures together to halve iteration count for (let i = 0; i < splatCount; i++) { - scale[i * 8 + 3 + 0] = Math.exp(ubuf[byteOffset + 0] / 16.0 - 10.0); - scale[i * 8 + 3 + 1] = Math.exp(ubuf[byteOffset + 1] / 16.0 - 10.0); - scale[i * 8 + 3 + 2] = Math.exp(ubuf[byteOffset + 2] / 16.0 - 10.0); - byteOffset += 3; - } - - // convert quaternion - if (version >= 3) { - /* - In version 3, rotations are represented as the smallest three components of the normalized rotation quaternion, for optimal rotation accuracy. - The largest component can be derived from the others and is not stored. Its index is stored on 2 bits - and each of the smallest three components is encoded as a 10-bit signed integer. - */ - const sqrt12 = Math.SQRT1_2; - for (let i = 0; i < splatCount; i++) { - const r = [ubuf[byteOffset + 0], ubuf[byteOffset + 1], ubuf[byteOffset + 2], ubuf[byteOffset + 3]]; - - const comp = r[0] + (r[1] << 8) + (r[2] << 16) + (r[3] << 24); - - const cmask = (1 << 9) - 1; - const rotation = []; - const iLargest = comp >>> 30; - let remaining = comp; - let sumSquares = 0; - - for (let i = 3; i >= 0; --i) { - if (i !== iLargest) { - const mag = remaining & cmask; - const negbit = (remaining >>> 9) & 0x1; - remaining = remaining >>> 10; - - rotation[i] = sqrt12 * (mag / cmask); - if (negbit === 1) { - rotation[i] = -rotation[i]; - } - - // accumulate the sum of squares - sumSquares += rotation[i] * rotation[i]; + const fBase = i * 8; + const uBase = i * 32; + const p = i * 3; + const r = i * 4; + + // Position (float32 x3, bytes 0-11) + fBuffer[fBase + 0] = positions[p + 0]; + fBuffer[fBase + 1] = positions[p + 1]; + fBuffer[fBase + 2] = positions[p + 2]; + + // Scale (float32 x3, bytes 12-23) — cloud scales are in log space, convert to linear + fBuffer[fBase + 3] = Math.exp(scales[p + 0]); + fBuffer[fBase + 4] = Math.exp(scales[p + 1]); + fBuffer[fBase + 5] = Math.exp(scales[p + 2]); + + // Color RGB: cloud gives raw SH DC coefficients, convert to [0,255] display value + const c0 = (0.5 + _SH_C0 * colors[p + 0]) * 255; + const c1 = (0.5 + _SH_C0 * colors[p + 1]) * 255; + const c2 = (0.5 + _SH_C0 * colors[p + 2]) * 255; + uBuffer[uBase + 24] = c0 <= 0 ? 0 : c0 >= 255 ? 255 : (c0 + 0.5) | 0; + uBuffer[uBase + 25] = c1 <= 0 ? 0 : c1 >= 255 ? 255 : (c1 + 0.5) | 0; + uBuffer[uBase + 26] = c2 <= 0 ? 0 : c2 >= 255 ? 255 : (c2 + 0.5) | 0; + // Alpha: cloud gives raw logit opacity, apply sigmoid to get [0,255] + uBuffer[uBase + 27] = ((1.0 / (1.0 + Math.exp(-alphas[i]))) * 255 + 0.5) | 0; + + // Rotation: cloud is xyzw, packed buffer expects wxyz + const rw = rotations[r + 3] * 127.5 + 127.5; + const rx = rotations[r + 0] * 127.5 + 127.5; + const ry = rotations[r + 1] * 127.5 + 127.5; + const rz = rotations[r + 2] * 127.5 + 127.5; + uBuffer[uBase + 28] = rw <= 0 ? 0 : rw >= 255 ? 255 : (rw + 0.5) | 0; // w + uBuffer[uBase + 29] = rx <= 0 ? 0 : rx >= 255 ? 255 : (rx + 0.5) | 0; // x + uBuffer[uBase + 30] = ry <= 0 ? 0 : ry >= 255 ? 255 : (ry + 0.5) | 0; // y + uBuffer[uBase + 31] = rz <= 0 ? 0 : rz >= 255 ? 255 : (rz + 0.5) | 0; // z + + // SH: process all texture chunks for this splat in the same iteration + if (cloudSh && shArrays && chunkStarts && chunkEnds) { + const shSplatBase = i * shComponentCount; + const offsetPerSplat = i * 16; + for (let t = 0; t < shArrays.length; t++) { + const shT = shArrays[t]; + const chunkStart = chunkStarts[t]; + const chunkEnd = chunkEnds[t]; + for (let j = chunkStart; j < chunkEnd; j++) { + const v = cloudSh[shSplatBase + j] * 127.5 + 127.5; + shT[offsetPerSplat + j - chunkStart] = v <= 0 ? 0 : v >= 255 ? 255 : (v + 0.5) | 0; } } - - const square = 1 - sumSquares; - rotation[iLargest] = Math.sqrt(Math.max(square, 0)); - - const shuffle = [3, 0, 1, 2]; // shuffle to match the order of the quaternion components in the splat file - for (let j = 0; j < 4; j++) { - rot[i * 32 + 28 + j] = Math.round(127.5 + rotation[shuffle[j]] * 127.5); - } - - byteOffset += 4; } - } else { - /* - In version 2, rotations are represented as the `(x, y, z)` components of the normalized rotation quaternion. The - `w` component can be derived from the others and is not stored. Each component is encoded as an - 8-bit signed integer. - */ - for (let i = 0; i < splatCount; i++) { - const x = ubuf[byteOffset + 0]; - const y = ubuf[byteOffset + 1]; - const z = ubuf[byteOffset + 2]; - const nx = x / 127.5 - 1; - const ny = y / 127.5 - 1; - const nz = z / 127.5 - 1; - rot[i * 32 + 28 + 1] = x; - rot[i * 32 + 28 + 2] = y; - rot[i * 32 + 28 + 3] = z; - const v = 1 - (nx * nx + ny * ny + nz * nz); - rot[i * 32 + 28 + 0] = 127.5 + Math.sqrt(v < 0 ? 0 : v) * 127.5; - - byteOffset += 3; - } - } - //SH - if (shDegree) { - // shVectorCount is : 3 for dim = 1, 8 for dim = 2 and 15 for dim = 3 - // number of vec3 vector needed per splat - const shVectorCount = (shDegree + 1) * (shDegree + 1) - 1; // minus 1 because sh0 is color - // number of component values : 3 per vector3 (45) - const shComponentCount = shVectorCount * 3; - - const textureCount = Math.ceil(shComponentCount / 16); // 4 components can be stored per texture, 4 sh per component - let shIndexRead = byteOffset; - - // sh is an array of uint8array that will be used to create sh textures - const sh: Uint8Array[] = []; - - const engine = scene.getEngine(); - const width = engine.getCaps().maxTextureSize; - const height = Math.ceil(splatCount / width); - // create array for the number of textures needed. - for (let textureIndex = 0; textureIndex < textureCount; textureIndex++) { - const texture = new Uint8Array(height * width * 4 * 4); // 4 components per texture, 4 sh per component - sh.push(texture); + if (i % _SpzConversionBatchSize === 0 && useCoroutine) { + yield; } + } - for (let i = 0; i < splatCount; i++) { - for (let shIndexWrite = 0; shIndexWrite < shComponentCount; shIndexWrite++) { - const shValue = ubuf[shIndexRead++]; - - 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. - shArray[byteIndexInTexture + offsetPerSplat] = shValue; + // Extract safe-orbit-camera extension if present + let safeOrbitCameraRadiusMin: number | undefined; + let safeOrbitCameraElevationMinMax: [number, number] | undefined; + if (cloud.extensions) { + for (const ext of cloud.extensions) { + if (ext.safeOrbitRadiusMin !== undefined) { + safeOrbitCameraRadiusMin = ext.safeOrbitRadiusMin; + safeOrbitCameraElevationMinMax = [ext.safeOrbitElevationMin, ext.safeOrbitElevationMax]; + break; } } - - return new Promise((resolve) => { - resolve({ mode: Mode.Splat, data: buffer, hasVertexColors: false, sh: sh, trainedWithAntialiasing: !!flags }); - }); } - return new Promise((resolve) => { - resolve({ mode: Mode.Splat, data: buffer, hasVertexColors: false, trainedWithAntialiasing: !!flags }); - }); + return { + mode: Mode.Splat, + data: buffer, + hasVertexColors: false, + sh: sh ?? undefined, + shDegree: shDegree > 0 ? shDegree : undefined, + trainedWithAntialiasing: !!cloud.antialiased, + safeOrbitCameraRadiusMin, + safeOrbitCameraElevationMinMax, + }; +} + +/** + * Async version of ConvertSpzToSplat that yields periodically to avoid blocking the main thread. + * @param cloud The GaussianCloud returned by spz.loadSpzFromBuffer + * @param scene The Babylon.js scene + * @returns A promise resolving to an IParsedSplat + */ +export async function ConvertSpzToSplatAsync(cloud: any, scene: Scene): Promise { + return await runCoroutineAsync(ConvertSpzToSplat(cloud, scene, true), createYieldingScheduler()); } diff --git a/packages/tools/tests/test/visualization/ReferenceImages/gsplat-ply-sh-order-4.png b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-ply-sh-order-4.png new file mode 100644 index 00000000000..fa7ec0bc76e Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-ply-sh-order-4.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-detailed-splat.png b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-detailed-splat.png new file mode 100644 index 00000000000..47c19134619 Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-detailed-splat.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-extensions.png b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-extensions.png new file mode 100644 index 00000000000..9ca6c004dc0 Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-extensions.png differ diff --git a/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-sh-order-4.png b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-sh-order-4.png new file mode 100644 index 00000000000..d3749803076 Binary files /dev/null and b/packages/tools/tests/test/visualization/ReferenceImages/gsplat-spz-sh-order-4.png differ diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index 657a34b1448..e6d11a8046d 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -47,6 +47,30 @@ "renderCount": 15, "referenceImage": "gsplat-spz-sh.png" }, + { + "title": "Gaussian Splatting SPZ Extensions", + "playgroundId": "#XSNFXP#4", + "renderCount": 15, + "referenceImage": "gsplat-spz-extensions.png" + }, + { + "title": "Gaussian Splatting SPZ SH Order 4", + "playgroundId": "#XSNFXP#5", + "renderCount": 15, + "referenceImage": "gsplat-spz-sh-order-4.png" + }, + { + "title": "Gaussian Splatting SPZ Detailed Splat", + "playgroundId": "#XSNFXP#6", + "renderCount": 15, + "referenceImage": "gsplat-spz-detailed-splat.png" + }, + { + "title": "Gaussian Splatting PLY SH Order 4", + "playgroundId": "#XSNFXP#7", + "renderCount": 15, + "referenceImage": "gsplat-ply-sh-order-4.png" + }, { "title": "Gaussian Splatting Update Data", "playgroundId": "#Q0LBM8#2",