Generate gaussian splat chunks during PLY import

This commit is contained in:
2026-04-11 07:13:32 +08:00
parent 92d5cc61cf
commit ff4e3f639a
3 changed files with 208 additions and 16 deletions

View File

@@ -450,22 +450,185 @@ bool IsFinite(const Math::Quaternion& value) {
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w); return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
} }
Math::Vector3 MinVector3(const Math::Vector3& lhs, const Math::Vector3& rhs) {
return Math::Vector3(
std::min(lhs.x, rhs.x),
std::min(lhs.y, rhs.y),
std::min(lhs.z, rhs.z));
}
Math::Vector3 MaxVector3(const Math::Vector3& lhs, const Math::Vector3& rhs) {
return Math::Vector3(
std::max(lhs.x, rhs.x),
std::max(lhs.y, rhs.y),
std::max(lhs.z, rhs.z));
}
Math::Vector4 MinVector4(const Math::Vector4& lhs, const Math::Vector4& rhs) {
return Math::Vector4(
std::min(lhs.x, rhs.x),
std::min(lhs.y, rhs.y),
std::min(lhs.z, rhs.z),
std::min(lhs.w, rhs.w));
}
Math::Vector4 MaxVector4(const Math::Vector4& lhs, const Math::Vector4& rhs) {
return Math::Vector4(
std::max(lhs.x, rhs.x),
std::max(lhs.y, rhs.y),
std::max(lhs.z, rhs.z),
std::max(lhs.w, rhs.w));
}
Core::uint16 FloatToHalfBits(float value) {
std::uint32_t bits = 0u;
std::memcpy(&bits, &value, sizeof(bits));
const std::uint32_t sign = (bits >> 16u) & 0x8000u;
std::uint32_t mantissa = bits & 0x007fffffu;
std::int32_t exponent = static_cast<std::int32_t>((bits >> 23u) & 0xffu) - 127 + 15;
if (exponent <= 0) {
if (exponent < -10) {
return static_cast<Core::uint16>(sign);
}
mantissa = (mantissa | 0x00800000u) >> static_cast<std::uint32_t>(1 - exponent);
if ((mantissa & 0x00001000u) != 0u) {
mantissa += 0x00002000u;
}
return static_cast<Core::uint16>(sign | (mantissa >> 13u));
}
if (exponent >= 31) {
return static_cast<Core::uint16>(sign | 0x7c00u);
}
if ((mantissa & 0x00001000u) != 0u) {
mantissa += 0x00002000u;
if ((mantissa & 0x00800000u) != 0u) {
mantissa = 0u;
++exponent;
if (exponent >= 31) {
return static_cast<Core::uint16>(sign | 0x7c00u);
}
}
}
return static_cast<Core::uint16>(
sign |
(static_cast<std::uint32_t>(exponent) << 10u) |
(mantissa >> 13u));
}
Core::uint32 PackHalfRange(float minValue, float maxValue) {
return static_cast<Core::uint32>(FloatToHalfBits(minValue)) |
(static_cast<Core::uint32>(FloatToHalfBits(maxValue)) << 16u);
}
Core::uint32 ComputeChunkCount(Core::uint32 splatCount) {
return splatCount == 0u
? 0u
: ((splatCount + kGaussianSplatChunkSize - 1u) / kGaussianSplatChunkSize);
}
void BuildChunkRecords(Core::uint32 vertexCount,
const GaussianSplatPositionRecord* positions,
const GaussianSplatOtherRecord* other,
const GaussianSplatColorRecord* colors,
const GaussianSplatSHRecord* shRecords,
GaussianSplatChunkRecord* chunks,
Core::uint32 chunkCount) {
constexpr Core::uint32 kSHCoefficientsPerChannel =
kGaussianSplatSHCoefficientCount / kGaussianSplatSHColorChannelCount;
for (Core::uint32 chunkIndex = 0u; chunkIndex < chunkCount; ++chunkIndex) {
const Core::uint32 startIndex = chunkIndex * kGaussianSplatChunkSize;
const Core::uint32 endIndex = std::min(startIndex + kGaussianSplatChunkSize, vertexCount);
Math::Vector3 minPosition(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
Math::Vector3 maxPosition(-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max());
Math::Vector3 minScale(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
Math::Vector3 maxScale(-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max());
Math::Vector4 minColor(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
Math::Vector4 maxColor(-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max());
Math::Vector3 minSh(std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max());
Math::Vector3 maxSh(-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max(),
-std::numeric_limits<float>::max());
for (Core::uint32 vertexIndex = startIndex; vertexIndex < endIndex; ++vertexIndex) {
minPosition = MinVector3(minPosition, positions[vertexIndex].position);
maxPosition = MaxVector3(maxPosition, positions[vertexIndex].position);
minScale = MinVector3(minScale, other[vertexIndex].scale);
maxScale = MaxVector3(maxScale, other[vertexIndex].scale);
minColor = MinVector4(minColor, colors[vertexIndex].colorOpacity);
maxColor = MaxVector4(maxColor, colors[vertexIndex].colorOpacity);
const GaussianSplatSHRecord& shRecord = shRecords[vertexIndex];
for (Core::uint32 coefficientIndex = 0u; coefficientIndex < kSHCoefficientsPerChannel; ++coefficientIndex) {
minSh.x = std::min(minSh.x, shRecord.coefficients[coefficientIndex]);
maxSh.x = std::max(maxSh.x, shRecord.coefficients[coefficientIndex]);
minSh.y = std::min(minSh.y, shRecord.coefficients[coefficientIndex + kSHCoefficientsPerChannel]);
maxSh.y = std::max(maxSh.y, shRecord.coefficients[coefficientIndex + kSHCoefficientsPerChannel]);
minSh.z = std::min(minSh.z, shRecord.coefficients[coefficientIndex + (kSHCoefficientsPerChannel * 2u)]);
maxSh.z = std::max(maxSh.z, shRecord.coefficients[coefficientIndex + (kSHCoefficientsPerChannel * 2u)]);
}
}
GaussianSplatChunkRecord chunk = {};
chunk.posX = Math::Vector2(minPosition.x, maxPosition.x);
chunk.posY = Math::Vector2(minPosition.y, maxPosition.y);
chunk.posZ = Math::Vector2(minPosition.z, maxPosition.z);
chunk.sclX = PackHalfRange(minScale.x, maxScale.x);
chunk.sclY = PackHalfRange(minScale.y, maxScale.y);
chunk.sclZ = PackHalfRange(minScale.z, maxScale.z);
chunk.colR = PackHalfRange(minColor.x, maxColor.x);
chunk.colG = PackHalfRange(minColor.y, maxColor.y);
chunk.colB = PackHalfRange(minColor.z, maxColor.z);
chunk.colA = PackHalfRange(minColor.w, maxColor.w);
chunk.shR = PackHalfRange(minSh.x, maxSh.x);
chunk.shG = PackHalfRange(minSh.y, maxSh.y);
chunk.shB = PackHalfRange(minSh.z, maxSh.z);
chunks[chunkIndex] = chunk;
}
}
void BuildSections(Core::uint32 vertexCount, void BuildSections(Core::uint32 vertexCount,
Core::uint32 chunkCount,
Containers::Array<GaussianSplatSection>& outSections, Containers::Array<GaussianSplatSection>& outSections,
size_t& outPayloadSize) { size_t& outPayloadSize) {
outSections.Clear(); outSections.Clear();
outSections.Reserve(4u); outSections.Reserve(chunkCount > 0u ? 5u : 4u);
size_t payloadOffset = 0u; size_t payloadOffset = 0u;
auto appendSection = [&](GaussianSplatSectionType type, auto appendSection = [&](GaussianSplatSectionType type,
GaussianSplatSectionFormat format, GaussianSplatSectionFormat format,
size_t elementStride) { size_t elementStride,
Core::uint32 elementCount) {
GaussianSplatSection section; GaussianSplatSection section;
section.type = type; section.type = type;
section.format = format; section.format = format;
section.dataOffset = payloadOffset; section.dataOffset = payloadOffset;
section.dataSize = elementStride * static_cast<size_t>(vertexCount); section.dataSize = elementStride * static_cast<size_t>(elementCount);
section.elementCount = vertexCount; section.elementCount = elementCount;
section.elementStride = static_cast<Core::uint32>(elementStride); section.elementStride = static_cast<Core::uint32>(elementStride);
outSections.PushBack(section); outSections.PushBack(section);
payloadOffset += section.dataSize; payloadOffset += section.dataSize;
@@ -474,19 +637,30 @@ void BuildSections(Core::uint32 vertexCount,
appendSection( appendSection(
GaussianSplatSectionType::Positions, GaussianSplatSectionType::Positions,
GaussianSplatSectionFormat::VectorFloat32, GaussianSplatSectionFormat::VectorFloat32,
sizeof(GaussianSplatPositionRecord)); sizeof(GaussianSplatPositionRecord),
vertexCount);
appendSection( appendSection(
GaussianSplatSectionType::Other, GaussianSplatSectionType::Other,
GaussianSplatSectionFormat::OtherFloat32, GaussianSplatSectionFormat::OtherFloat32,
sizeof(GaussianSplatOtherRecord)); sizeof(GaussianSplatOtherRecord),
vertexCount);
appendSection( appendSection(
GaussianSplatSectionType::Color, GaussianSplatSectionType::Color,
GaussianSplatSectionFormat::ColorRGBA32F, GaussianSplatSectionFormat::ColorRGBA32F,
sizeof(GaussianSplatColorRecord)); sizeof(GaussianSplatColorRecord),
vertexCount);
appendSection( appendSection(
GaussianSplatSectionType::SH, GaussianSplatSectionType::SH,
GaussianSplatSectionFormat::SHFloat32, GaussianSplatSectionFormat::SHFloat32,
sizeof(GaussianSplatSHRecord)); sizeof(GaussianSplatSHRecord),
vertexCount);
if (chunkCount > 0u) {
appendSection(
GaussianSplatSectionType::Chunks,
GaussianSplatSectionFormat::ChunkFloat32,
sizeof(GaussianSplatChunkRecord),
chunkCount);
}
outPayloadSize = payloadOffset; outPayloadSize = payloadOffset;
} }
@@ -506,9 +680,10 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
return LoadResult(Containers::String("Failed to parse GaussianSplat PLY header: ") + path + " - " + headerError); return LoadResult(Containers::String("Failed to parse GaussianSplat PLY header: ") + path + " - " + headerError);
} }
const Core::uint32 chunkCount = ComputeChunkCount(header.vertexCount);
Containers::Array<GaussianSplatSection> sections; Containers::Array<GaussianSplatSection> sections;
size_t payloadSize = 0u; size_t payloadSize = 0u;
BuildSections(header.vertexCount, sections, payloadSize); BuildSections(header.vertexCount, chunkCount, sections, payloadSize);
Containers::Array<Core::uint8> payload; Containers::Array<Core::uint8> payload;
payload.Resize(payloadSize); payload.Resize(payloadSize);
@@ -521,6 +696,10 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
payload.Data() + static_cast<size_t>(sections[2].dataOffset)); payload.Data() + static_cast<size_t>(sections[2].dataOffset));
auto* shRecords = reinterpret_cast<GaussianSplatSHRecord*>( auto* shRecords = reinterpret_cast<GaussianSplatSHRecord*>(
payload.Data() + static_cast<size_t>(sections[3].dataOffset)); payload.Data() + static_cast<size_t>(sections[3].dataOffset));
auto* chunks = chunkCount > 0u
? reinterpret_cast<GaussianSplatChunkRecord*>(
payload.Data() + static_cast<size_t>(sections[4].dataOffset))
: nullptr;
Math::Bounds bounds; Math::Bounds bounds;
bool hasBounds = false; bool hasBounds = false;
@@ -600,17 +779,21 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
} }
} }
if (chunkCount > 0u && chunks != nullptr) {
BuildChunkRecords(header.vertexCount, positions, other, colors, shRecords, chunks, chunkCount);
}
GaussianSplatMetadata metadata; GaussianSplatMetadata metadata;
metadata.contentVersion = 1u; metadata.contentVersion = 1u;
metadata.splatCount = header.vertexCount; metadata.splatCount = header.vertexCount;
metadata.chunkCount = 0u; metadata.chunkCount = chunkCount;
metadata.cameraCount = 0u; metadata.cameraCount = 0u;
metadata.bounds = bounds; metadata.bounds = bounds;
metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32; metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32; metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F; metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
metadata.shFormat = GaussianSplatSectionFormat::SHFloat32; metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
metadata.chunkFormat = GaussianSplatSectionFormat::Unknown; metadata.chunkFormat = chunkCount > 0u ? GaussianSplatSectionFormat::ChunkFloat32 : GaussianSplatSectionFormat::Unknown;
metadata.cameraFormat = GaussianSplatSectionFormat::Unknown; metadata.cameraFormat = GaussianSplatSectionFormat::Unknown;
auto* gaussianSplat = new GaussianSplat(); auto* gaussianSplat = new GaussianSplat();

View File

@@ -554,16 +554,18 @@ TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsSourceAssetImportP
ASSERT_NE(cached, nullptr); ASSERT_NE(cached, nullptr);
EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady); EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady);
EXPECT_EQ(cached->splatCount, 2u); EXPECT_EQ(cached->splatCount, 2u);
EXPECT_EQ(state->createBufferCalls, 4); EXPECT_EQ(cached->chunkCount, 1u);
EXPECT_EQ(state->createShaderViewCalls, 4); EXPECT_EQ(cached->chunks.elementStride, sizeof(GaussianSplatChunkRecord));
EXPECT_EQ(state->createBufferCalls, 5);
EXPECT_EQ(state->createShaderViewCalls, 5);
const auto handleAgain = manager.Load<GaussianSplat>("Assets/sample.ply"); const auto handleAgain = manager.Load<GaussianSplat>("Assets/sample.ply");
ASSERT_TRUE(handleAgain.IsValid()); ASSERT_TRUE(handleAgain.IsValid());
const RenderResourceCache::CachedGaussianSplat* cachedAgain = const RenderResourceCache::CachedGaussianSplat* cachedAgain =
cache.GetOrCreateGaussianSplat(&device, handleAgain.Get()); cache.GetOrCreateGaussianSplat(&device, handleAgain.Get());
EXPECT_EQ(cachedAgain, cached); EXPECT_EQ(cachedAgain, cached);
EXPECT_EQ(state->createBufferCalls, 4); EXPECT_EQ(state->createBufferCalls, 5);
EXPECT_EQ(state->createShaderViewCalls, 4); EXPECT_EQ(state->createShaderViewCalls, 5);
} }
manager.UnloadAll(); manager.UnloadAll();

View File

@@ -456,7 +456,7 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource); auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
ASSERT_NE(gaussianSplat, nullptr); ASSERT_NE(gaussianSplat, nullptr);
EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u); EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u);
EXPECT_EQ(gaussianSplat->GetChunkCount(), 0u); EXPECT_EQ(gaussianSplat->GetChunkCount(), 1u);
EXPECT_EQ(gaussianSplat->GetCameraCount(), 0u); EXPECT_EQ(gaussianSplat->GetCameraCount(), 0u);
ExpectVector3Near(gaussianSplat->GetBounds().GetMin(), Vector3(-4.0f, -5.0f, -6.0f)); ExpectVector3Near(gaussianSplat->GetBounds().GetMin(), Vector3(-4.0f, -5.0f, -6.0f));
ExpectVector3Near(gaussianSplat->GetBounds().GetMax(), Vector3(1.0f, 2.0f, 3.0f)); ExpectVector3Near(gaussianSplat->GetBounds().GetMax(), Vector3(1.0f, 2.0f, 3.0f));
@@ -465,6 +465,7 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
ASSERT_NE(gaussianSplat->GetOtherRecords(), nullptr); ASSERT_NE(gaussianSplat->GetOtherRecords(), nullptr);
ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr); ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr);
ASSERT_NE(gaussianSplat->GetSHRecords(), nullptr); ASSERT_NE(gaussianSplat->GetSHRecords(), nullptr);
ASSERT_NE(gaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr);
ExpectVector3Near(gaussianSplat->GetPositionRecords()[0].position, vertices[0].position); ExpectVector3Near(gaussianSplat->GetPositionRecords()[0].position, vertices[0].position);
ExpectVector3Near(gaussianSplat->GetPositionRecords()[1].position, vertices[1].position); ExpectVector3Near(gaussianSplat->GetPositionRecords()[1].position, vertices[1].position);
@@ -497,6 +498,8 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) {
const XCEngine::Core::uint32 expectedVertexCount = ReadPlyVertexCount(fixturePath); const XCEngine::Core::uint32 expectedVertexCount = ReadPlyVertexCount(fixturePath);
ASSERT_GT(expectedVertexCount, 0u); ASSERT_GT(expectedVertexCount, 0u);
const XCEngine::Core::uint32 expectedChunkCount =
(expectedVertexCount + kGaussianSplatChunkSize - 1u) / kGaussianSplatChunkSize;
const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_room_import"); const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_room_import");
const fs::path assetsDir = projectRoot / "Assets"; const fs::path assetsDir = projectRoot / "Assets";
@@ -514,10 +517,12 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) {
const auto firstHandle = manager.Load<GaussianSplat>("Assets/room.ply"); const auto firstHandle = manager.Load<GaussianSplat>("Assets/room.ply");
ASSERT_TRUE(firstHandle.IsValid()); ASSERT_TRUE(firstHandle.IsValid());
EXPECT_EQ(firstHandle->GetSplatCount(), expectedVertexCount); EXPECT_EQ(firstHandle->GetSplatCount(), expectedVertexCount);
EXPECT_EQ(firstHandle->GetChunkCount(), expectedChunkCount);
EXPECT_EQ(firstHandle->GetPositionFormat(), GaussianSplatSectionFormat::VectorFloat32); EXPECT_EQ(firstHandle->GetPositionFormat(), GaussianSplatSectionFormat::VectorFloat32);
EXPECT_EQ(firstHandle->GetOtherFormat(), GaussianSplatSectionFormat::OtherFloat32); EXPECT_EQ(firstHandle->GetOtherFormat(), GaussianSplatSectionFormat::OtherFloat32);
EXPECT_EQ(firstHandle->GetColorFormat(), GaussianSplatSectionFormat::ColorRGBA32F); EXPECT_EQ(firstHandle->GetColorFormat(), GaussianSplatSectionFormat::ColorRGBA32F);
EXPECT_EQ(firstHandle->GetSHFormat(), GaussianSplatSectionFormat::SHFloat32); EXPECT_EQ(firstHandle->GetSHFormat(), GaussianSplatSectionFormat::SHFloat32);
EXPECT_EQ(firstHandle->GetChunkFormat(), GaussianSplatSectionFormat::ChunkFloat32);
AssetRef assetRef; AssetRef assetRef;
ASSERT_TRUE(manager.TryGetAssetRef("Assets/room.ply", ResourceType::GaussianSplat, assetRef)); ASSERT_TRUE(manager.TryGetAssetRef("Assets/room.ply", ResourceType::GaussianSplat, assetRef));
@@ -547,7 +552,9 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) {
const auto secondHandle = manager.Load<GaussianSplat>(assetRef); const auto secondHandle = manager.Load<GaussianSplat>(assetRef);
ASSERT_TRUE(secondHandle.IsValid()); ASSERT_TRUE(secondHandle.IsValid());
EXPECT_EQ(secondHandle->GetSplatCount(), expectedVertexCount); EXPECT_EQ(secondHandle->GetSplatCount(), expectedVertexCount);
EXPECT_EQ(secondHandle->GetChunkCount(), expectedChunkCount);
ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::SH), nullptr); ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::SH), nullptr);
ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::Chunks), nullptr);
} }
manager.SetResourceRoot(""); manager.SetResourceRoot("");