diff --git a/.gitmodules b/.gitmodules index fe07c99..3d3b9d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "vcpkg"] path = vcpkg url = https://github.com/microsoft/vcpkg +[submodule "third_party/spz"] + path = third_party/spz + url = https://github.com/nianticlabs/spz.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 24d875a..0b3f3b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,12 +53,22 @@ else() set(TRACY_LIBRARIES, "") endif() +# zlib (for spz format support) +find_package(ZLIB REQUIRED) + # openxr-loader find_package(OpenXR CONFIG REQUIRED) +# spz (Niantic .spz gaussian splat format) +set(SPZ_DIR ${CMAKE_SOURCE_DIR}/third_party/spz/src/cc) + # src include_directories(src) +include_directories(${SPZ_DIR}) add_executable(${PROJECT_NAME} + ${SPZ_DIR}/load-spz.cc + ${SPZ_DIR}/splat-types.cc + ${SPZ_DIR}/splat-c-types.cc src/core/binaryattribute.cpp src/core/debugrenderer.cpp src/core/framebuffer.cpp @@ -102,6 +112,7 @@ if(WIN32) GLEW::GLEW glm::glm PNG::PNG + ZLIB::ZLIB nlohmann_json::nlohmann_json Eigen3::Eigen OpenXR::headers @@ -115,6 +126,7 @@ if(WIN32) GLEW::GLEW glm::glm PNG::PNG + ZLIB::ZLIB nlohmann_json::nlohmann_json Eigen3::Eigen Tracy::TracyClient diff --git a/src/app.cpp b/src/app.cpp index c8178a8..6be4fde 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -14,6 +14,7 @@ #include #endif +#include #include #include @@ -234,7 +235,22 @@ static std::shared_ptr LoadGaussianCloud(const std::string& plyFi options.exportFullSH = true; #endif auto gaussianCloud = std::make_shared(options); - if (!gaussianCloud->ImportPly(plyFilename)) + + // Route based on file extension + std::string ext = plyFilename.substr(plyFilename.find_last_of('.') + 1); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + bool loaded = false; + if (ext == "spz") + { + loaded = gaussianCloud->ImportSpz(plyFilename); + } + else + { + loaded = gaussianCloud->ImportPly(plyFilename); + } + + if (!loaded) { Log::E("Error loading GaussianCloud!\n"); return nullptr; diff --git a/src/gaussiancloud.cpp b/src/gaussiancloud.cpp index 58eac0b..81b066e 100644 --- a/src/gaussiancloud.cpp +++ b/src/gaussiancloud.cpp @@ -28,6 +28,7 @@ #include "core/util.h" #include "ply.h" +#include "load-spz.h" struct BaseGaussianData { @@ -364,6 +365,166 @@ bool GaussianCloud::ImportPly(const std::string& plyFilename) return true; } +bool GaussianCloud::ImportSpz(const std::string& spzFilename) +{ + ZoneScopedNC("GC::ImportSpz", tracy::Color::Red4); + + // Load SPZ file using Niantic's spz library. + // 3dgsconverter stores data in PLY coordinate space as-is ("no-flip"), + // so we use UNSPECIFIED to avoid any coordinate re-mapping. + spz::UnpackOptions unpackOpts; + unpackOpts.to = spz::CoordinateSystem::UNSPECIFIED; + + spz::GaussianCloud cloud = spz::loadSpz(spzFilename, unpackOpts); + if (cloud.numPoints <= 0) + { + Log::E("Error loading SPZ file \"%s\"\n", spzFilename.c_str()); + return false; + } + + // Determine SH degree support + int32_t shDim = 0; // coefficients per color channel + if (cloud.shDegree >= 3) shDim = 15; + else if (cloud.shDegree >= 2) shDim = 8; + else if (cloud.shDegree >= 1) shDim = 3; + + hasFullSH = opt.importFullSH && (shDim == 15); + + InitAttribs(); + + numGaussians = cloud.numPoints; + if (hasFullSH) + { + gaussianSize = sizeof(FullGaussianData); + FullGaussianData* fullPtr = new FullGaussianData[numGaussians]; + data.reset(fullPtr); + } + else + { + gaussianSize = sizeof(BaseGaussianData); + BaseGaussianData* basePtr = new BaseGaussianData[numGaussians]; + data.reset(basePtr); + } + + uint8_t* rawPtr = (uint8_t*)data.get(); + for (size_t i = 0; i < numGaussians; i++) + { + BaseGaussianData* basePtr = reinterpret_cast(rawPtr); + + // Position + alpha (sigmoid of stored alpha) + basePtr->posWithAlpha[0] = cloud.positions[i * 3 + 0]; + basePtr->posWithAlpha[1] = cloud.positions[i * 3 + 1]; + basePtr->posWithAlpha[2] = cloud.positions[i * 3 + 2]; + basePtr->posWithAlpha[3] = ComputeAlphaFromOpacity(cloud.alphas[i]); + + // Color (SH DC component) — spz stores as SH DC, same as PLY f_dc_0/1/2 + float f_dc_0 = cloud.colors[i * 3 + 0]; + float f_dc_1 = cloud.colors[i * 3 + 1]; + float f_dc_2 = cloud.colors[i * 3 + 2]; + + if (hasFullSH) + { + FullGaussianData* fullPtr = reinterpret_cast(rawPtr); + // SPZ SH layout: [shDim * 3] per point, with color as inner axis: [coeff0_r, coeff0_g, coeff0_b, coeff1_r, ...] + // PLY f_rest layout: all red coefficients, then all green, then all blue (per splat) + // SPZ returns SH in [N, S, C] order after unpack, where S=coeff, C=channel + + // Red channel: DC + 15 f_rest coefficients + fullPtr->r_sh0[0] = f_dc_0; + fullPtr->g_sh0[0] = f_dc_1; + fullPtr->b_sh0[0] = f_dc_2; + + size_t shBase = i * shDim * 3; + for (int j = 0; j < 15; j++) + { + float rVal = 0.0f, gVal = 0.0f, bVal = 0.0f; + if (j < shDim) + { + rVal = cloud.sh[shBase + j * 3 + 0]; + gVal = cloud.sh[shBase + j * 3 + 1]; + bVal = cloud.sh[shBase + j * 3 + 2]; + } + // Map to the same layout as PLY import + int block = j / 4; // 0..3 + int idx = j % 4; // index within block + // r_sh0[1..3] then r_sh1[0..3] then r_sh2[0..3] then r_sh3[0..3] + if (j < 3) + { + fullPtr->r_sh0[1 + j] = rVal; + fullPtr->g_sh0[1 + j] = gVal; + fullPtr->b_sh0[1 + j] = bVal; + } + else if (j < 7) + { + fullPtr->r_sh1[j - 3] = rVal; + fullPtr->g_sh1[j - 3] = gVal; + fullPtr->b_sh1[j - 3] = bVal; + } + else if (j < 11) + { + fullPtr->r_sh2[j - 7] = rVal; + fullPtr->g_sh2[j - 7] = gVal; + fullPtr->b_sh2[j - 7] = bVal; + } + else + { + fullPtr->r_sh3[j - 11] = rVal; + fullPtr->g_sh3[j - 11] = gVal; + fullPtr->b_sh3[j - 11] = bVal; + } + } + } + else + { + basePtr->r_sh0[0] = f_dc_0; + basePtr->r_sh0[1] = 0.0f; + basePtr->r_sh0[2] = 0.0f; + basePtr->r_sh0[3] = 0.0f; + + basePtr->g_sh0[0] = f_dc_1; + basePtr->g_sh0[1] = 0.0f; + basePtr->g_sh0[2] = 0.0f; + basePtr->g_sh0[3] = 0.0f; + + basePtr->b_sh0[0] = f_dc_2; + basePtr->b_sh0[1] = 0.0f; + basePtr->b_sh0[2] = 0.0f; + basePtr->b_sh0[3] = 0.0f; + } + + // Scale (spz stores in log scale, same as PLY) and rotation + float scale[3] = + { + expf(cloud.scales[i * 3 + 0]), + expf(cloud.scales[i * 3 + 1]), + expf(cloud.scales[i * 3 + 2]) + }; + // spz rotation: [x, y, z, w], splatapult needs [w, x, y, z] + float rot[4] = + { + cloud.rotations[i * 4 + 3], // w + cloud.rotations[i * 4 + 0], // x + cloud.rotations[i * 4 + 1], // y + cloud.rotations[i * 4 + 2] // z + }; + + glm::mat3 V = ComputeCovMatFromRotScale(rot, scale); + basePtr->cov3_col0[0] = V[0][0]; + basePtr->cov3_col0[1] = V[0][1]; + basePtr->cov3_col0[2] = V[0][2]; + basePtr->cov3_col1[0] = V[1][0]; + basePtr->cov3_col1[1] = V[1][1]; + basePtr->cov3_col1[2] = V[1][2]; + basePtr->cov3_col2[0] = V[2][0]; + basePtr->cov3_col2[1] = V[2][1]; + basePtr->cov3_col2[2] = V[2][2]; + + rawPtr += gaussianSize; + } + + return true; +} + bool GaussianCloud::ExportPly(const std::string& plyFilename) const { std::ofstream plyFile(plyFilename, std::ios::binary); diff --git a/src/gaussiancloud.h b/src/gaussiancloud.h index 54643af..75b676d 100644 --- a/src/gaussiancloud.h +++ b/src/gaussiancloud.h @@ -26,6 +26,7 @@ class GaussianCloud GaussianCloud(const Options& options); bool ImportPly(const std::string& plyFilename); + bool ImportSpz(const std::string& spzFilename); bool ExportPly(const std::string& plyFilename) const; void InitDebugCloud(); diff --git a/third_party/spz b/third_party/spz new file mode 160000 index 0000000..ef094fd --- /dev/null +++ b/third_party/spz @@ -0,0 +1 @@ +Subproject commit ef094fd1a96ca6ff414d72d7904ee4f4f6d97be9 diff --git a/vcpkg.json b/vcpkg.json index 14b4f6d..68981a3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -9,6 +9,7 @@ "nlohmann-json", "eigen3", "tracy", - "openxr-loader" + "openxr-loader", + "zlib" ] }