diff --git a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader index ab3ffb18..591a6776 100644 --- a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader +++ b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader @@ -107,6 +107,102 @@ Shader "Builtin Gaussian Splat Utilities" return rawValue ^ mask; } + float2 UnpackHalfRange(uint packedValue) + { + return float2( + f16tof32(packedValue & 0xffffu), + f16tof32(packedValue >> 16u)); + } + + float4 TransformLocalPointToClip( + float3 localPosition, + float4x4 modelMatrix, + float4x4 viewMatrix, + float4x4 projectionMatrix) + { + const float3 worldPosition = mul(modelMatrix, float4(localPosition, 1.0)).xyz; + const float3 viewPosition = mul(viewMatrix, float4(worldPosition, 1.0)).xyz; + return mul(projectionMatrix, float4(viewPosition, 1.0)); + } + + bool IsChunkDefinitelyOutsideFrustum( + GaussianSplatChunkData chunkData, + uint chunkCount, + uint chunkIndex, + float4x4 modelMatrix, + float4x4 viewMatrix, + float4x4 projectionMatrix) + { + if (chunkCount == 0u || chunkIndex >= chunkCount) + { + return false; + } + + float3 localMin = float3(chunkData.posX.x, chunkData.posY.x, chunkData.posZ.x); + float3 localMax = float3(chunkData.posX.y, chunkData.posY.y, chunkData.posZ.y); + if (localMin.x > localMax.x || localMin.y > localMax.y || localMin.z > localMax.z) + { + return true; + } + + // Inflate by a conservative 3-sigma envelope derived from the chunk's maximum splat scale. + const float3 maxScale = float3( + UnpackHalfRange(chunkData.sclX).y, + UnpackHalfRange(chunkData.sclY).y, + UnpackHalfRange(chunkData.sclZ).y); + const float radius = max(maxScale.x, max(maxScale.y, maxScale.z)) * 3.0; + localMin -= radius.xxx; + localMax += radius.xxx; + + bool allBehind = true; + bool anyBehind = false; + bool outsideLeft = true; + bool outsideRight = true; + bool outsideBottom = true; + bool outsideTop = true; + bool outsideNear = true; + bool outsideFar = true; + + [unroll] + for (uint cornerIndex = 0u; cornerIndex < 8u; ++cornerIndex) + { + const float3 localCorner = float3( + (cornerIndex & 1u) != 0u ? localMax.x : localMin.x, + (cornerIndex & 2u) != 0u ? localMax.y : localMin.y, + (cornerIndex & 4u) != 0u ? localMax.z : localMin.z); + const float4 clipCorner = TransformLocalPointToClip( + localCorner, + modelMatrix, + viewMatrix, + projectionMatrix); + const bool behind = clipCorner.w <= 0.0; + allBehind = allBehind && behind; + anyBehind = anyBehind || behind; + + if (!behind) + { + outsideLeft = outsideLeft && (clipCorner.x < -clipCorner.w); + outsideRight = outsideRight && (clipCorner.x > clipCorner.w); + outsideBottom = outsideBottom && (clipCorner.y < -clipCorner.w); + outsideTop = outsideTop && (clipCorner.y > clipCorner.w); + outsideNear = outsideNear && (clipCorner.z < 0.0); + outsideFar = outsideFar && (clipCorner.z > clipCorner.w); + } + } + + if (allBehind) + { + return true; + } + + if (anyBehind) + { + return false; + } + + return outsideLeft || outsideRight || outsideBottom || outsideTop || outsideNear || outsideFar; + } + float3x3 CalcMatrixFromRotationScale(float4 rotation, float3 scale) { const float x = rotation.x; @@ -260,7 +356,9 @@ Shader "Builtin Gaussian Splat Utilities" const GaussianSplatOtherData otherData = GaussianSplatOther[index]; const float4 colorOpacity = GaussianSplatColor[index]; const GaussianSplatSHData shData = GaussianSplatSH[index]; - const GaussianSplatChunkData chunkData = GaussianSplatChunks[index / GAUSSIAN_SPLAT_CHUNK_SIZE]; + const uint chunkIndex = index / GAUSSIAN_SPLAT_CHUNK_SIZE; + const uint chunkCount = (uint)gSplatParams.w; + const GaussianSplatChunkData chunkData = GaussianSplatChunks[chunkIndex]; const uint shOrder = min((uint)gSplatParams.z, 3u); if (chunkData.posX.x > chunkData.posX.y || chunkData.posY.x > chunkData.posY.y || @@ -270,6 +368,18 @@ Shader "Builtin Gaussian Splat Utilities" GaussianSplatViewDataBuffer[index] = viewData; return; } + if (IsChunkDefinitelyOutsideFrustum( + chunkData, + chunkCount, + chunkIndex, + gModelMatrix, + gViewMatrix, + gProjectionMatrix)) + { + GaussianSplatSortDistances[index] = 0xffffffffu; + GaussianSplatViewDataBuffer[index] = viewData; + return; + } const float3 worldCenter = mul(gModelMatrix, float4(localCenter, 1.0)).xyz; const float3 viewCenter = mul(gViewMatrix, float4(worldCenter, 1.0)).xyz; diff --git a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp index 4d94899b..369b8b1a 100644 --- a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp @@ -1106,7 +1106,7 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat( static_cast(cachedGaussianSplat->splatCount), static_cast(workingSet->sortCapacity), shOrder, - 0.0f) + static_cast(cachedGaussianSplat->chunkCount)) }; if (passLayout->descriptorSetCount > 0u) { diff --git a/tests/Rendering/integration/gaussian_splat_scene/main.cpp b/tests/Rendering/integration/gaussian_splat_scene/main.cpp index f770b214..844562a8 100644 --- a/tests/Rendering/integration/gaussian_splat_scene/main.cpp +++ b/tests/Rendering/integration/gaussian_splat_scene/main.cpp @@ -50,6 +50,53 @@ constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; constexpr uint32_t kBaselineSubsetSplatCount = 65536u; +XCEngine::Core::uint16 FloatToHalfBits(float value) { + uint32_t bits = 0u; + std::memcpy(&bits, &value, sizeof(bits)); + + const uint32_t sign = (bits >> 16u) & 0x8000u; + uint32_t mantissa = bits & 0x007fffffu; + 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)); +} + +uint32_t PackHalfRange(float minValue, float maxValue) { + return static_cast(FloatToHalfBits(minValue)) | + (static_cast(FloatToHalfBits(maxValue)) << 16u); +} + std::filesystem::path GetRoomPlyPath() { return std::filesystem::path(XCENGINE_TEST_ROOM_PLY_PATH); } @@ -153,21 +200,39 @@ GaussianSplat* CreateGaussianSplatSubset( -std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::max()); + Vector3 minScale( + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max()); + Vector3 maxScale( + -std::numeric_limits::max(), + -std::numeric_limits::max(), + -std::numeric_limits::max()); for (uint32_t subsetIndex = startIndex; subsetIndex < endIndex; ++subsetIndex) { const Vector3& position = subsetPositions[subsetIndex].position; + const Vector3& scale = subsetOther[subsetIndex].scale; minPosition.x = std::min(minPosition.x, position.x); minPosition.y = std::min(minPosition.y, position.y); minPosition.z = std::min(minPosition.z, position.z); maxPosition.x = std::max(maxPosition.x, position.x); maxPosition.y = std::max(maxPosition.y, position.y); maxPosition.z = std::max(maxPosition.z, position.z); + minScale.x = std::min(minScale.x, scale.x); + minScale.y = std::min(minScale.y, scale.y); + minScale.z = std::min(minScale.z, scale.z); + maxScale.x = std::max(maxScale.x, scale.x); + maxScale.y = std::max(maxScale.y, scale.y); + maxScale.z = std::max(maxScale.z, scale.z); } GaussianSplatChunkRecord chunk = {}; chunk.posX = Vector2(minPosition.x, maxPosition.x); chunk.posY = Vector2(minPosition.y, maxPosition.y); chunk.posZ = 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); subsetChunks[chunkIndex] = chunk; }