Generate gaussian splat chunks during PLY import
This commit is contained in:
@@ -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<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,
|
||||
Core::uint32 chunkCount,
|
||||
Containers::Array<GaussianSplatSection>& 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<size_t>(vertexCount);
|
||||
section.elementCount = vertexCount;
|
||||
section.dataSize = elementStride * static_cast<size_t>(elementCount);
|
||||
section.elementCount = elementCount;
|
||||
section.elementStride = static_cast<Core::uint32>(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<GaussianSplatSection> sections;
|
||||
size_t payloadSize = 0u;
|
||||
BuildSections(header.vertexCount, sections, payloadSize);
|
||||
BuildSections(header.vertexCount, chunkCount, sections, payloadSize);
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
payload.Resize(payloadSize);
|
||||
@@ -521,6 +696,10 @@ LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
|
||||
payload.Data() + static_cast<size_t>(sections[2].dataOffset));
|
||||
auto* shRecords = reinterpret_cast<GaussianSplatSHRecord*>(
|
||||
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;
|
||||
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();
|
||||
|
||||
@@ -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<GaussianSplat>("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();
|
||||
|
||||
@@ -456,7 +456,7 @@ TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
|
||||
auto* gaussianSplat = static_cast<GaussianSplat*>(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<GaussianSplat>("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<GaussianSplat>(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("");
|
||||
|
||||
Reference in New Issue
Block a user