Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -102,6 +112,7 @@ if(WIN32)
GLEW::GLEW
glm::glm
PNG::PNG
ZLIB::ZLIB
nlohmann_json::nlohmann_json
Eigen3::Eigen
OpenXR::headers
Expand All @@ -115,6 +126,7 @@ if(WIN32)
GLEW::GLEW
glm::glm
PNG::PNG
ZLIB::ZLIB
nlohmann_json::nlohmann_json
Eigen3::Eigen
Tracy::TracyClient
Expand Down
18 changes: 17 additions & 1 deletion src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <SDL2/SDL.h>
#endif

#include <algorithm>
#include <filesystem>
#include <thread>

Expand Down Expand Up @@ -234,7 +235,22 @@ static std::shared_ptr<GaussianCloud> LoadGaussianCloud(const std::string& plyFi
options.exportFullSH = true;
#endif
auto gaussianCloud = std::make_shared<GaussianCloud>(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;
Expand Down
161 changes: 161 additions & 0 deletions src/gaussiancloud.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "core/util.h"

#include "ply.h"
#include "load-spz.h"

struct BaseGaussianData
{
Expand Down Expand Up @@ -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<BaseGaussianData*>(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<FullGaussianData*>(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);
Expand Down
1 change: 1 addition & 0 deletions src/gaussiancloud.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions third_party/spz
Submodule spz added at ef094f
3 changes: 2 additions & 1 deletion vcpkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"nlohmann-json",
"eigen3",
"tracy",
"openxr-loader"
"openxr-loader",
"zlib"
]
}