From ff4e3f639a512d146999a614d2551b8d1cdff551 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 11 Apr 2026 07:13:32 +0800 Subject: [PATCH] Generate gaussian splat chunks during PLY import --- .../Internal/GaussianSplatPlyImporter.cpp | 205 +++++++++++++++++- .../unit/test_render_resource_cache.cpp | 10 +- .../test_gaussian_splat_loader.cpp | 9 +- 3 files changed, 208 insertions(+), 16 deletions(-) diff --git a/engine/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp b/engine/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp index 87fd6b44..3ffb0705 100644 --- a/engine/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp +++ b/engine/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp @@ -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); } +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((bits >> 23u) & 0xffu) - 127 + 15; + + if (exponent <= 0) { + if (exponent < -10) { + return static_cast(sign); + } + + mantissa = (mantissa | 0x00800000u) >> static_cast(1 - exponent); + if ((mantissa & 0x00001000u) != 0u) { + mantissa += 0x00002000u; + } + + return static_cast(sign | (mantissa >> 13u)); + } + + if (exponent >= 31) { + return static_cast(sign | 0x7c00u); + } + + if ((mantissa & 0x00001000u) != 0u) { + mantissa += 0x00002000u; + if ((mantissa & 0x00800000u) != 0u) { + mantissa = 0u; + ++exponent; + if (exponent >= 31) { + return static_cast(sign | 0x7c00u); + } + } + } + + return static_cast( + sign | + (static_cast(exponent) << 10u) | + (mantissa >> 13u)); +} + +Core::uint32 PackHalfRange(float minValue, float maxValue) { + return static_cast(FloatToHalfBits(minValue)) | + (static_cast(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::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + Math::Vector3 maxPosition(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); + Math::Vector3 minScale(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + Math::Vector3 maxScale(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); + Math::Vector4 minColor(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + Math::Vector4 maxColor(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); + Math::Vector3 minSh(std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + Math::Vector3 maxSh(-std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::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, + Core::uint32 chunkCount, Containers::Array& outSections, size_t& outPayloadSize) { outSections.Clear(); - outSections.Reserve(4u); + outSections.Reserve(chunkCount > 0u ? 5u : 4u); size_t payloadOffset = 0u; auto appendSection = [&](GaussianSplatSectionType type, GaussianSplatSectionFormat format, - size_t elementStride) { + size_t elementStride, + Core::uint32 elementCount) { GaussianSplatSection section; section.type = type; section.format = format; section.dataOffset = payloadOffset; - section.dataSize = elementStride * static_cast(vertexCount); - section.elementCount = vertexCount; + section.dataSize = elementStride * static_cast(elementCount); + section.elementCount = elementCount; section.elementStride = static_cast(elementStride); outSections.PushBack(section); payloadOffset += section.dataSize; @@ -474,19 +637,30 @@ void BuildSections(Core::uint32 vertexCount, appendSection( GaussianSplatSectionType::Positions, GaussianSplatSectionFormat::VectorFloat32, - sizeof(GaussianSplatPositionRecord)); + sizeof(GaussianSplatPositionRecord), + vertexCount); appendSection( GaussianSplatSectionType::Other, GaussianSplatSectionFormat::OtherFloat32, - sizeof(GaussianSplatOtherRecord)); + sizeof(GaussianSplatOtherRecord), + vertexCount); appendSection( GaussianSplatSectionType::Color, GaussianSplatSectionFormat::ColorRGBA32F, - sizeof(GaussianSplatColorRecord)); + sizeof(GaussianSplatColorRecord), + vertexCount); appendSection( GaussianSplatSectionType::SH, GaussianSplatSectionFormat::SHFloat32, - sizeof(GaussianSplatSHRecord)); + sizeof(GaussianSplatSHRecord), + vertexCount); + if (chunkCount > 0u) { + appendSection( + GaussianSplatSectionType::Chunks, + GaussianSplatSectionFormat::ChunkFloat32, + sizeof(GaussianSplatChunkRecord), + chunkCount); + } outPayloadSize = payloadOffset; } @@ -506,9 +680,10 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) { return LoadResult(Containers::String("Failed to parse GaussianSplat PLY header: ") + path + " - " + headerError); } + const Core::uint32 chunkCount = ComputeChunkCount(header.vertexCount); Containers::Array sections; size_t payloadSize = 0u; - BuildSections(header.vertexCount, sections, payloadSize); + BuildSections(header.vertexCount, chunkCount, sections, payloadSize); Containers::Array payload; payload.Resize(payloadSize); @@ -521,6 +696,10 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) { payload.Data() + static_cast(sections[2].dataOffset)); auto* shRecords = reinterpret_cast( payload.Data() + static_cast(sections[3].dataOffset)); + auto* chunks = chunkCount > 0u + ? reinterpret_cast( + payload.Data() + static_cast(sections[4].dataOffset)) + : nullptr; Math::Bounds bounds; 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; metadata.contentVersion = 1u; metadata.splatCount = header.vertexCount; - metadata.chunkCount = 0u; + metadata.chunkCount = chunkCount; metadata.cameraCount = 0u; metadata.bounds = bounds; metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32; metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32; metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F; metadata.shFormat = GaussianSplatSectionFormat::SHFloat32; - metadata.chunkFormat = GaussianSplatSectionFormat::Unknown; + metadata.chunkFormat = chunkCount > 0u ? GaussianSplatSectionFormat::ChunkFloat32 : GaussianSplatSectionFormat::Unknown; metadata.cameraFormat = GaussianSplatSectionFormat::Unknown; auto* gaussianSplat = new GaussianSplat(); diff --git a/tests/Rendering/unit/test_render_resource_cache.cpp b/tests/Rendering/unit/test_render_resource_cache.cpp index 4512b813..e624abfa 100644 --- a/tests/Rendering/unit/test_render_resource_cache.cpp +++ b/tests/Rendering/unit/test_render_resource_cache.cpp @@ -554,16 +554,18 @@ TEST(RenderResourceCacheTest, GetOrCreateGaussianSplatSupportsSourceAssetImportP ASSERT_NE(cached, nullptr); EXPECT_EQ(cached->residencyState, RenderResourceCache::GaussianSplatResidencyState::GpuReady); EXPECT_EQ(cached->splatCount, 2u); - EXPECT_EQ(state->createBufferCalls, 4); - EXPECT_EQ(state->createShaderViewCalls, 4); + EXPECT_EQ(cached->chunkCount, 1u); + EXPECT_EQ(cached->chunks.elementStride, sizeof(GaussianSplatChunkRecord)); + EXPECT_EQ(state->createBufferCalls, 5); + EXPECT_EQ(state->createShaderViewCalls, 5); const auto handleAgain = manager.Load("Assets/sample.ply"); ASSERT_TRUE(handleAgain.IsValid()); const RenderResourceCache::CachedGaussianSplat* cachedAgain = cache.GetOrCreateGaussianSplat(&device, handleAgain.Get()); EXPECT_EQ(cachedAgain, cached); - EXPECT_EQ(state->createBufferCalls, 4); - EXPECT_EQ(state->createShaderViewCalls, 4); + EXPECT_EQ(state->createBufferCalls, 5); + EXPECT_EQ(state->createShaderViewCalls, 5); } manager.UnloadAll(); diff --git a/tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp b/tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp index 64e5c4c0..6752ae7f 100644 --- a/tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp +++ b/tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp @@ -456,7 +456,7 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) { auto* gaussianSplat = static_cast(result.resource); ASSERT_NE(gaussianSplat, nullptr); EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u); - EXPECT_EQ(gaussianSplat->GetChunkCount(), 0u); + EXPECT_EQ(gaussianSplat->GetChunkCount(), 1u); EXPECT_EQ(gaussianSplat->GetCameraCount(), 0u); ExpectVector3Near(gaussianSplat->GetBounds().GetMin(), Vector3(-4.0f, -5.0f, -6.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->GetColorRecords(), nullptr); ASSERT_NE(gaussianSplat->GetSHRecords(), nullptr); + ASSERT_NE(gaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr); ExpectVector3Near(gaussianSplat->GetPositionRecords()[0].position, vertices[0].position); ExpectVector3Near(gaussianSplat->GetPositionRecords()[1].position, vertices[1].position); @@ -497,6 +498,8 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) { const XCEngine::Core::uint32 expectedVertexCount = ReadPlyVertexCount(fixturePath); 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 assetsDir = projectRoot / "Assets"; @@ -514,10 +517,12 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) { const auto firstHandle = manager.Load("Assets/room.ply"); ASSERT_TRUE(firstHandle.IsValid()); EXPECT_EQ(firstHandle->GetSplatCount(), expectedVertexCount); + EXPECT_EQ(firstHandle->GetChunkCount(), expectedChunkCount); EXPECT_EQ(firstHandle->GetPositionFormat(), GaussianSplatSectionFormat::VectorFloat32); EXPECT_EQ(firstHandle->GetOtherFormat(), GaussianSplatSectionFormat::OtherFloat32); EXPECT_EQ(firstHandle->GetColorFormat(), GaussianSplatSectionFormat::ColorRGBA32F); EXPECT_EQ(firstHandle->GetSHFormat(), GaussianSplatSectionFormat::SHFloat32); + EXPECT_EQ(firstHandle->GetChunkFormat(), GaussianSplatSectionFormat::ChunkFloat32); AssetRef assetRef; ASSERT_TRUE(manager.TryGetAssetRef("Assets/room.ply", ResourceType::GaussianSplat, assetRef)); @@ -547,7 +552,9 @@ TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) { const auto secondHandle = manager.Load(assetRef); ASSERT_TRUE(secondHandle.IsValid()); EXPECT_EQ(secondHandle->GetSplatCount(), expectedVertexCount); + EXPECT_EQ(secondHandle->GetChunkCount(), expectedChunkCount); ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::SH), nullptr); + ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::Chunks), nullptr); } manager.SetResourceRoot("");