diff --git a/Config/Game.ini b/Config/Game.ini new file mode 100644 index 000000000..b686c325a --- /dev/null +++ b/Config/Game.ini @@ -0,0 +1,2 @@ +[/Script/UnrealEd.ProjectPackagingSettings] ++DirectoriesToAlwaysCook=(Path="/CesiumForUnreal/GaussianSplatting") diff --git a/Content/GaussianSplatting/GaussianSplatEffectType.uasset b/Content/GaussianSplatting/GaussianSplatEffectType.uasset new file mode 100644 index 000000000..4dcac8c3b Binary files /dev/null and b/Content/GaussianSplatting/GaussianSplatEffectType.uasset differ diff --git a/Content/GaussianSplatting/GaussianSplatSystem.uasset b/Content/GaussianSplatting/GaussianSplatSystem.uasset new file mode 100644 index 000000000..510a3516e Binary files /dev/null and b/Content/GaussianSplatting/GaussianSplatSystem.uasset differ diff --git a/Content/GaussianSplatting/M_CesiumSplatMaterial.uasset b/Content/GaussianSplatting/M_CesiumSplatMaterial.uasset new file mode 100644 index 000000000..1ca62abe7 Binary files /dev/null and b/Content/GaussianSplatting/M_CesiumSplatMaterial.uasset differ diff --git a/Source/CesiumRuntime/CesiumRuntime.Build.cs b/Source/CesiumRuntime/CesiumRuntime.Build.cs index cca9bfb4e..4786a5860 100644 --- a/Source/CesiumRuntime/CesiumRuntime.Build.cs +++ b/Source/CesiumRuntime/CesiumRuntime.Build.cs @@ -103,7 +103,8 @@ public CesiumRuntime(ReadOnlyTargetRules Target) : base(Target) "Json", "JsonUtilities", "Slate", - "SlateCore" + "SlateCore", + "Niagara" } ); diff --git a/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.cpp b/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.cpp new file mode 100644 index 000000000..d88c69526 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.cpp @@ -0,0 +1,829 @@ +// Copyright 2020-2025 Bentley Systems, Inc. and Contributors + +#include "CesiumGaussianSplatDataInterface.h" + +#include "CesiumGaussianSplatSubsystem.h" +#include "CesiumGltfGaussianSplatComponent.h" +#include "CesiumRuntime.h" +#include "Containers/Map.h" +#include "Engine/Engine.h" +#include "HLSLTypeAliases.h" +#include "NiagaraCommon.h" +#include "NiagaraCompileHashVisitor.h" +#include "NiagaraComponent.h" +#include "NiagaraDataInterface.h" +#include "NiagaraRenderer.h" +#include "NiagaraShaderParametersBuilder.h" +#include "RHICommandList.h" + +#include +#include +#include + +const FString GetPositionFunctionName = TEXT("GetSplat_Position"); +const FString GetScaleFunctionName = TEXT("GetSplat_Scale"); +const FString GetOrientationFunctionName = TEXT("GetSplat_Orientation"); +const FString GetVisibilityFunctionName = TEXT("GetSplat_Visibility"); +const FString GetTransformRotationFunctionName = + TEXT("GetSplat_Transform_Rotation"); +const FString GetColorFunctionName = TEXT("GetSplat_Color"); +const FString GetSHContributionFunctionName = TEXT("GetSplat_SHContribution"); +const FString GetNumSplatsFunctionName = TEXT("GetNumSplats"); + +namespace { +void UploadSplatMatrices( + FRHICommandListImmediate& RHICmdList, + TArray& Components, + FReadBuffer& Buffer) { + if (Buffer.NumBytes > 0) { + Buffer.Release(); + } + + if (Components.IsEmpty()) { + // Will crash if we try to allocate a buffer of size 0 + return; + } + + const int32 Stride = 7; + + Buffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_SplatMatricesBuffer"), + sizeof(FVector4f), + Components.Num() * Stride, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + + float* BufferData = static_cast(RHICmdList.LockBuffer( + Buffer.Buffer, + 0, + Components.Num() * sizeof(FVector4f) * Stride, + EResourceLockMode::RLM_WriteOnly)); + for (int32 i = 0; i < Components.Num(); i++) { + check(Components[i]); + glm::mat4x4 mat = Components[i]->GetMatrix(); + const int32 Offset = i * (sizeof(FVector4f) / sizeof(float)) * Stride; + BufferData[Offset] = (float)mat[0].x; + BufferData[Offset + 1] = (float)mat[0].y; + BufferData[Offset + 2] = (float)mat[0].z; + BufferData[Offset + 3] = (float)mat[0].w; + BufferData[Offset + 4] = (float)mat[1].x; + BufferData[Offset + 5] = (float)mat[1].y; + BufferData[Offset + 6] = (float)mat[1].z; + BufferData[Offset + 7] = (float)mat[1].w; + BufferData[Offset + 8] = (float)mat[2].x; + BufferData[Offset + 9] = (float)mat[2].y; + BufferData[Offset + 10] = (float)mat[2].z; + BufferData[Offset + 11] = (float)mat[2].w; + BufferData[Offset + 12] = (float)mat[3].x; + BufferData[Offset + 13] = (float)mat[3].y; + BufferData[Offset + 14] = (float)mat[3].z; + BufferData[Offset + 15] = (float)mat[3].w; + const FTransform& ComponentToWorld = Components[i]->GetComponentToWorld(); + // Previously these four held location information, but we don't use it (we + // just use the matrix for that). So, instead, the first component holds + // visibility and the other three are currently unused. + BufferData[Offset + 16] = Components[i]->IsVisible() ? 1.0f : 0.0f; + BufferData[Offset + 17] = 0.0f; + BufferData[Offset + 18] = 0.0f; + BufferData[Offset + 19] = 0.0f; + FVector Scale = ComponentToWorld.GetScale3D(); + BufferData[Offset + 20] = (float)Scale.X; + BufferData[Offset + 21] = (float)Scale.Y; + BufferData[Offset + 22] = (float)Scale.Z; + BufferData[Offset + 23] = (float)1.0; + FQuat Rotation = ComponentToWorld.GetRotation(); + BufferData[Offset + 24] = (float)Rotation.X; + BufferData[Offset + 25] = (float)Rotation.Y; + BufferData[Offset + 26] = (float)Rotation.Z; + BufferData[Offset + 27] = (float)Rotation.W; + } + + RHICmdList.UnlockBuffer(Buffer.Buffer); +} +} // namespace + +FNDIGaussianSplatProxy::FNDIGaussianSplatProxy( + UCesiumGaussianSplatDataInterface* InOwner) + : Owner(InOwner) {} + +void FNDIGaussianSplatProxy::UploadToGPU( + UCesiumGaussianSplatSubsystem* SplatSystem) { + if (this->Owner == nullptr || !IsValid(SplatSystem)) { + return; + } + + if (this->bMatricesNeedUpdate) { + this->bMatricesNeedUpdate = false; + + ENQUEUE_RENDER_COMMAND(FUpdateGaussianSplatMatrices) + ([this, SplatSystem](FRHICommandListImmediate& RHICmdList) { + FScopeLock ScopeLock(&this->BufferLock); + UploadSplatMatrices( + RHICmdList, + SplatSystem->SplatComponents, + this->SplatMatricesBuffer); + }); + } + + if (!this->bNeedsUpdate) { + return; + } + + this->bNeedsUpdate = false; + + ENQUEUE_RENDER_COMMAND(FUpdateGaussianSplatBuffers) + ([this, SplatSystem](FRHICommandListImmediate& RHICmdList) { + FScopeLock ScopeLock(&this->BufferLock); + const int32 NumSplats = SplatSystem->GetNumSplats(); + + int32 TotalCoeffsCount = 0; + for (const UCesiumGltfGaussianSplatComponent* SplatComponent : + SplatSystem->SplatComponents) { + check(SplatComponent); + TotalCoeffsCount += + SplatComponent->NumCoefficients * SplatComponent->NumSplats; + } + + const int32 ExpectedPosBytes = NumSplats * 4 * sizeof(float); + if (this->PositionsBuffer.NumBytes != ExpectedPosBytes) { + if (this->PositionsBuffer.NumBytes > 0) { + this->PositionsBuffer.Release(); + this->ScalesBuffer.Release(); + this->OrientationsBuffer.Release(); + this->ColorsBuffer.Release(); + this->SHNonZeroCoeffsBuffer.Release(); + this->SplatSHDegreesBuffer.Release(); + this->SplatIndicesBuffer.Release(); + } + + if (SplatSystem->SplatComponents.IsEmpty()) { + return; + } + + if (ExpectedPosBytes > 0) { + this->PositionsBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_Positions"), + sizeof(FVector4f), + NumSplats, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + this->ScalesBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_Scales"), + sizeof(FVector4f), + NumSplats, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + this->OrientationsBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_Orientations"), + sizeof(FVector4f), + NumSplats, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + this->ColorsBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_Colors"), + sizeof(FVector4f), + NumSplats, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + if (TotalCoeffsCount > 0) { + this->SHNonZeroCoeffsBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_SHNonZeroCoeffsBuffer"), + sizeof(FVector4f), + TotalCoeffsCount, + EPixelFormat::PF_A32B32G32R32F, + BUF_Static); + } + this->SplatIndicesBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_SplatIndicesBuffer"), + sizeof(uint32), + NumSplats, + EPixelFormat::PF_R32_UINT, + BUF_Static); + this->SplatSHDegreesBuffer.Initialize( + RHICmdList, + TEXT("FNDIGaussianSplatProxy_SplatSHDegrees"), + sizeof(uint32), + SplatSystem->SplatComponents.Num() * 3, + EPixelFormat::PF_R32_UINT, + BUF_Static); + } + } + + if (ExpectedPosBytes > 0) { + { + float* PositionsBuffer = static_cast(RHICmdList.LockBuffer( + this->PositionsBuffer.Buffer, + 0, + ExpectedPosBytes, + EResourceLockMode::RLM_WriteOnly)); + float* ScalesBuffer = static_cast(RHICmdList.LockBuffer( + this->ScalesBuffer.Buffer, + 0, + ExpectedPosBytes, + EResourceLockMode::RLM_WriteOnly)); + float* OrientationsBuffer = static_cast(RHICmdList.LockBuffer( + this->OrientationsBuffer.Buffer, + 0, + NumSplats * 4 * sizeof(float), + EResourceLockMode::RLM_WriteOnly)); + float* ColorsBuffer = static_cast(RHICmdList.LockBuffer( + this->ColorsBuffer.Buffer, + 0, + NumSplats * 4 * sizeof(float), + EResourceLockMode::RLM_WriteOnly)); + float* SHNonZeroCoeffsBuffer = + TotalCoeffsCount > 0 ? static_cast(RHICmdList.LockBuffer( + this->SHNonZeroCoeffsBuffer.Buffer, + 0, + TotalCoeffsCount * 4 * sizeof(float), + EResourceLockMode::RLM_WriteOnly)) + : nullptr; + uint32* IndexBuffer = static_cast(RHICmdList.LockBuffer( + this->SplatIndicesBuffer.Buffer, + 0, + NumSplats * sizeof(uint32), + EResourceLockMode::RLM_WriteOnly)); + uint32* SHDegreesBuffer = static_cast(RHICmdList.LockBuffer( + this->SplatSHDegreesBuffer.Buffer, + 0, + SplatSystem->SplatComponents.Num() * sizeof(uint32) * 3, + EResourceLockMode::RLM_WriteOnly)); + + int32 CoeffCountWritten = 0; + int32 SplatCountWritten = 0; + for (int32 i = 0; i < SplatSystem->SplatComponents.Num(); i++) { + UCesiumGltfGaussianSplatComponent* Component = + SplatSystem->SplatComponents[i]; + check(Component); + + FPlatformMemory::Memcpy( + reinterpret_cast(PositionsBuffer + SplatCountWritten * 4), + Component->Positions.GetData(), + Component->Positions.Num() * sizeof(float)); + FPlatformMemory::Memcpy( + reinterpret_cast(ScalesBuffer + SplatCountWritten * 4), + Component->Scales.GetData(), + Component->Scales.Num() * sizeof(float)); + FPlatformMemory::Memcpy( + reinterpret_cast( + OrientationsBuffer + SplatCountWritten * 4), + Component->Orientations.GetData(), + Component->Orientations.Num() * sizeof(float)); + FPlatformMemory::Memcpy( + reinterpret_cast(ColorsBuffer + SplatCountWritten * 4), + Component->Colors.GetData(), + Component->Colors.Num() * sizeof(float)); + if (TotalCoeffsCount > 0) { + FPlatformMemory::Memcpy( + reinterpret_cast( + SHNonZeroCoeffsBuffer + CoeffCountWritten * 4), + Component->SphericalHarmonics.GetData(), + Component->SphericalHarmonics.Num() * sizeof(float)); + } + for (int32 j = 0; j < Component->NumSplats; j++) { + IndexBuffer[SplatCountWritten + j] = static_cast(i); + } + + SHDegreesBuffer[i * 3] = + static_cast(Component->NumCoefficients); + SHDegreesBuffer[i * 3 + 1] = static_cast(CoeffCountWritten); + SHDegreesBuffer[i * 3 + 2] = static_cast(SplatCountWritten); + + SplatCountWritten += Component->NumSplats; + CoeffCountWritten += + Component->NumSplats * Component->NumCoefficients; + } + + RHICmdList.UnlockBuffer(this->PositionsBuffer.Buffer); + RHICmdList.UnlockBuffer(this->ScalesBuffer.Buffer); + RHICmdList.UnlockBuffer(this->OrientationsBuffer.Buffer); + RHICmdList.UnlockBuffer(this->ColorsBuffer.Buffer); + if (TotalCoeffsCount > 0) { + RHICmdList.UnlockBuffer(this->SHNonZeroCoeffsBuffer.Buffer); + } + RHICmdList.UnlockBuffer(this->SplatIndicesBuffer.Buffer); + RHICmdList.UnlockBuffer(this->SplatSHDegreesBuffer.Buffer); + } + } + }); +} + +UCesiumGaussianSplatDataInterface::UCesiumGaussianSplatDataInterface( + const FObjectInitializer& Initializer) + : UNiagaraDataInterface(Initializer) { + this->Proxy = MakeUnique(this); +} + +#if WITH_EDITORONLY_DATA +void UCesiumGaussianSplatDataInterface::GetParameterDefinitionHLSL( + const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, + FString& OutHLSL) { + UNiagaraDataInterface::GetParameterDefinitionHLSL(ParamInfo, OutHLSL); + + OutHLSL.Appendf( + TEXT("int %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_SplatsCount")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_SplatIndices")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_SplatMatrices")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_Positions")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_Scales")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_Orientations")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_Colors")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_SplatSHDegrees")); + OutHLSL.Appendf( + TEXT("Buffer %s%s;\n"), + *ParamInfo.DataInterfaceHLSLSymbol, + TEXT("_SHNonZeroCoeffs")); +} + +bool UCesiumGaussianSplatDataInterface::GetFunctionHLSL( + const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, + const FNiagaraDataInterfaceGeneratedFunction& FunctionInfo, + int FunctionInstanceIndex, + FString& OutHLSL) { + if (UNiagaraDataInterface::GetFunctionHLSL( + ParamInfo, + FunctionInfo, + FunctionInstanceIndex, + OutHLSL)) { + return true; + } + + static const TCHAR* MatrixMathFormatBounds = TEXT(R"( + int SplatIndex = {IndicesBuffer}[Index]; + float4 c0 = {MatrixBuffer}[SplatIndex * 7]; + float4 c1 = {MatrixBuffer}[SplatIndex * 7 + 1]; + float4 c2 = {MatrixBuffer}[SplatIndex * 7 + 2]; + float4 c3 = {MatrixBuffer}[SplatIndex * 7 + 3]; + float4x4 SplatMatrix = float4x4( + c0.x, c1.x, c2.x, c3.x, + c0.y, c1.y, c2.y, c3.y, + c0.z, c1.z, c2.z, c3.z, + c0.w, c1.w, c2.w, c3.w + ); + )"); + + const TMap MatrixMathArgsBounds = { + {TEXT("IndicesBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("MatrixBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatMatrices"))}}; + + if (FunctionInfo.DefinitionName == *GetPositionFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float3 OutPosition) + { + {MatrixMath} + OutPosition = mul(SplatMatrix, float4({Buffer}[Index].xyz, 1.0)).xyz; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("MatrixMath"), + FStringFormatArg( + FString::Format(MatrixMathFormatBounds, MatrixMathArgsBounds))}, + {TEXT("Buffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_Positions"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetScaleFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float3 OutScale) + { + int SplatIndex = {IndicesBuffer}[Index]; + float4 SScale = {MatrixBuffer}[SplatIndex * 7 + 5]; + OutScale = SScale * {Buffer}[Index].xyz; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("Buffer"), + FStringFormatArg(ParamInfo.DataInterfaceHLSLSymbol + TEXT("_Scales"))}, + {TEXT("IndicesBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("MatrixBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatMatrices"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetOrientationFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float4 OutOrientation) + { + float4 q2 = {Buffer}[Index]; + OutOrientation = q2; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("Buffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_Orientations"))}, + {TEXT("IndicesBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("MatrixBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatMatrices"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetVisibilityFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float OutVisibility) + { + int SplatIndex = {IndicesBuffer}[Index]; + OutVisibility = {MatrixBuffer}[SplatIndex * 7 + 4].x; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("IndicesBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("MatrixBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatMatrices"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetTransformRotationFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float4 OutRotation) + { + int SplatIndex = {IndicesBuffer}[Index]; + OutRotation = {MatrixBuffer}[SplatIndex * 7 + 6]; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("IndicesBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("MatrixBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatMatrices"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetColorFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out float4 OutColor) + { + OutColor = {Buffer}[Index]; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("Buffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_Colors"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetSHContributionFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(float3 WorldViewDir, int Index, out float3 OutColor) + { + const float SH_C1 = 0.4886025119029199f; + const float SH_C2[] = {1.0925484, -1.0925484, 0.3153916, -1.0925484, 0.5462742}; + const float SH_C3[] = {-0.5900435899266435f, 2.890611442640554f, -0.4570457994644658f, 0.3731763325901154f, + -0.4570457994644658f, 1.445305721320277f, -0.5900435899266435f}; + + int SplatIndex = {SplatIndices}[Index]; + int SHCount = {SHDegrees}[SplatIndex * 3]; + int SHOffset = {SHDegrees}[SplatIndex * 3 + 1] + (Index - {SHDegrees}[SplatIndex * 3 + 2]) * SHCount; + + const float x = WorldViewDir.x; + const float y = WorldViewDir.y; + const float z = WorldViewDir.z; + const float xx = x * x; + const float yy = y * y; + const float zz = z * z; + const float xy = x * y; + const float yz = y * z; + const float xz = x * z; + const float xyz = x * y * z; + + float3 Color = float3(0.0, 0.0, 0.0); + if(SHCount >= 3) { + float3 shd1_0 = {Buffer}[SHOffset].xyz; + float3 shd1_1 = {Buffer}[SHOffset + 1].xyz; + float3 shd1_2 = {Buffer}[SHOffset + 2].xyz; + Color += SH_C1 * (-shd1_0 * y + shd1_1 * z - shd1_2 * x); + } + + if(SHCount >= 8) { + float3 shd2_0 = {Buffer}[SHOffset + 3]; + float3 shd2_1 = {Buffer}[SHOffset + 4]; + float3 shd2_2 = {Buffer}[SHOffset + 5]; + float3 shd2_3 = {Buffer}[SHOffset + 6]; + float3 shd2_4 = {Buffer}[SHOffset + 7]; + Color += (SH_C2[0] * xy) * shd2_0 + (SH_C2[1] * yz) * shd2_1 + (SH_C2[2] * (2.0 * zz - xx - yy)) * shd2_2 + + (SH_C2[3] * xz) * shd2_3 + (SH_C2[4] * (xx - yy)) * shd2_4; + } + + if(SHCount >= 15) { + float3 shd3_0 = {Buffer}[SHOffset + 8]; + float3 shd3_1 = {Buffer}[SHOffset + 9]; + float3 shd3_2 = {Buffer}[SHOffset + 10]; + float3 shd3_3 = {Buffer}[SHOffset + 11]; + float3 shd3_4 = {Buffer}[SHOffset + 12]; + float3 shd3_5 = {Buffer}[SHOffset + 13]; + float3 shd3_6 = {Buffer}[SHOffset + 14]; + + Color += SH_C3[0] * shd3_0 * (3.0 * xx - yy) * y + SH_C3[1] * shd3_1 * xyz + + SH_C3[2] * shd3_2 * (4.0 * zz - xx - yy) * y + + SH_C3[3] * shd3_3 * z * (2.0 * zz - 3.0 * xx - 3.0 * yy) + + SH_C3[4] * shd3_4 * x * (4.0 * zz - xx - yy) + + SH_C3[5] * shd3_5 * (xx - yy) * z + SH_C3[6] * shd3_6 * x * (xx - 3.0 * yy); + } + + OutColor = Color; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("Buffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SHNonZeroCoeffs"))}, + {TEXT("SHDegrees"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatSHDegrees"))}, + {TEXT("SplatIndices"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatIndices"))}, + {TEXT("PosBuffer"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_Positions"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else if (FunctionInfo.DefinitionName == *GetNumSplatsFunctionName) { + static const TCHAR* FormatBounds = TEXT(R"( + void {FunctionName}(int Index, out int OutNumSplats) + { + OutNumSplats = {SplatCount}; + } + )"); + + const TMap ArgsBounds = { + {TEXT("FunctionName"), FStringFormatArg(FunctionInfo.InstanceName)}, + {TEXT("SplatCount"), + FStringFormatArg( + ParamInfo.DataInterfaceHLSLSymbol + TEXT("_SplatsCount"))}}; + + OutHLSL += FString::Format(FormatBounds, ArgsBounds); + } else { + return false; + } + + return true; +} + +bool UCesiumGaussianSplatDataInterface::AppendCompileHash( + FNiagaraCompileHashVisitor* InVisitor) const { + bool bSuccess = UNiagaraDataInterface::AppendCompileHash(InVisitor); + bSuccess &= InVisitor->UpdateShaderParameters(); + return bSuccess; +} + +void UCesiumGaussianSplatDataInterface::GetFunctions( + TArray& OutFunctions) { + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetPositionFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetVec3Def(), + TEXT("Position"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetScaleFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetVec3Def(), TEXT("Scale"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetOrientationFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetVec4Def(), + TEXT("Orientation"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetVisibilityFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetFloatDef(), + TEXT("Visibility"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetTransformRotationFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetVec4Def(), + TEXT("Rotation"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetColorFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetVec4Def(), TEXT("Color"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetSHContributionFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetVec3Def(), + TEXT("WorldViewDir"))); + Sig.Inputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetIntDef(), TEXT("Index"))); + Sig.Outputs.Add( + FNiagaraVariable(FNiagaraTypeDefinition::GetVec3Def(), TEXT("Color"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } + + { + FNiagaraFunctionSignature Sig; + Sig.Name = *GetNumSplatsFunctionName; + Sig.Inputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition(GetClass()), + TEXT("GaussianSplatNDI"))); + Sig.Outputs.Add(FNiagaraVariable( + FNiagaraTypeDefinition::GetIntDef(), + TEXT("NumSplats"))); + Sig.bMemberFunction = true; + Sig.bRequiresContext = false; + OutFunctions.Add(Sig); + } +} +#endif + +void UCesiumGaussianSplatDataInterface::BuildShaderParameters( + FNiagaraShaderParametersBuilder& Builder) const { + Builder.AddNestedStruct(); +} + +void UCesiumGaussianSplatDataInterface::SetShaderParameters( + const FNiagaraDataInterfaceSetShaderParametersContext& Context) const { + FGaussianSplatShaderParams* Params = + Context.GetParameterNestedStruct(); + FNDIGaussianSplatProxy& DIProxy = Context.GetProxy(); + + if (Params) { + UCesiumGaussianSplatSubsystem* SplatSystem = this->GetSubsystem(); + + DIProxy.UploadToGPU(SplatSystem); + + Params->SplatsCount = + IsValid(SplatSystem) ? SplatSystem->GetNumSplats() : 0; + Params->SplatIndices = + FNiagaraRenderer::GetSrvOrDefaultUInt(DIProxy.SplatIndicesBuffer.SRV); + Params->SplatMatrices = FNiagaraRenderer::GetSrvOrDefaultFloat4( + DIProxy.SplatMatricesBuffer.SRV); + Params->Positions = + FNiagaraRenderer::GetSrvOrDefaultFloat4(DIProxy.PositionsBuffer.SRV); + Params->Scales = + FNiagaraRenderer::GetSrvOrDefaultFloat4(DIProxy.ScalesBuffer.SRV); + Params->Orientations = + FNiagaraRenderer::GetSrvOrDefaultFloat4(DIProxy.OrientationsBuffer.SRV); + Params->Colors = + FNiagaraRenderer::GetSrvOrDefaultFloat4(DIProxy.ColorsBuffer.SRV); + Params->SHNonZeroCoeffs = FNiagaraRenderer::GetSrvOrDefaultFloat4( + DIProxy.SHNonZeroCoeffsBuffer.SRV); + Params->SplatSHDegrees = + FNiagaraRenderer::GetSrvOrDefaultUInt(DIProxy.SplatSHDegreesBuffer.SRV); + } +} + +void UCesiumGaussianSplatDataInterface::PostInitProperties() { + UNiagaraDataInterface::PostInitProperties(); + + if (HasAnyFlags(RF_ClassDefaultObject)) { + ENiagaraTypeRegistryFlags DIFlags = + ENiagaraTypeRegistryFlags::AllowAnyVariable | + ENiagaraTypeRegistryFlags::AllowParameter; + FNiagaraTypeRegistry::Register(FNiagaraTypeDefinition(GetClass()), DIFlags); + } +} + +bool UCesiumGaussianSplatDataInterface::CanExecuteOnTarget( + ENiagaraSimTarget target) const { + return target == ENiagaraSimTarget::GPUComputeSim; +} + +void UCesiumGaussianSplatDataInterface::Refresh() { + this->GetProxyAs()->bNeedsUpdate = true; + this->GetProxyAs()->bMatricesNeedUpdate = true; +} + +void UCesiumGaussianSplatDataInterface::RefreshMatrices() { + this->GetProxyAs()->bMatricesNeedUpdate = true; +} + +FScopeLock UCesiumGaussianSplatDataInterface::LockGaussianBuffers() { + return FScopeLock(&this->GetProxyAs()->BufferLock); +} + +UCesiumGaussianSplatSubsystem* +UCesiumGaussianSplatDataInterface::GetSubsystem() const { + if (!IsValid(GEngine)) { + return nullptr; + } + + return GEngine->GetEngineSubsystem(); +} diff --git a/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.h b/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.h new file mode 100644 index 000000000..b71b15fa0 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGaussianSplatDataInterface.h @@ -0,0 +1,90 @@ +// Copyright 2020-2025 Bentley Systems, Inc. and Contributors + +#pragma once + +#include "CoreMinimal.h" +#include "NiagaraDataInterface.h" +#include "NiagaraDataInterfaceBase.h" +#include "RHIUtilities.h" + +#include "CesiumGaussianSplatDataInterface.generated.h" + +class UCesiumGaussianSplatSubsystem; + +struct FNDIGaussianSplatProxy : public FNiagaraDataInterfaceProxy { + FNDIGaussianSplatProxy(class UCesiumGaussianSplatDataInterface* InOwner); + + TObjectPtr Owner = nullptr; + FCriticalSection BufferLock; + + FReadBuffer SplatIndicesBuffer; + FReadBuffer SplatMatricesBuffer; + FReadBuffer SplatSHDegreesBuffer; + FReadBuffer PositionsBuffer; + FReadBuffer ScalesBuffer; + FReadBuffer OrientationsBuffer; + FReadBuffer ColorsBuffer; + FReadBuffer SHNonZeroCoeffsBuffer; + + bool bNeedsUpdate = true; + bool bMatricesNeedUpdate = true; + + virtual int32 PerInstanceDataPassedToRenderThreadSize() const override { + return 0; + } + + void UploadToGPU(UCesiumGaussianSplatSubsystem* SplatSystem); +}; + +BEGIN_SHADER_PARAMETER_STRUCT(FGaussianSplatShaderParams, ) +SHADER_PARAMETER(int, SplatsCount) +SHADER_PARAMETER_SRV(Buffer, SplatIndices) +SHADER_PARAMETER_SRV(Buffer, SplatMatrices) +SHADER_PARAMETER_SRV(Buffer, Positions) +SHADER_PARAMETER_SRV(Buffer, Scales) +SHADER_PARAMETER_SRV(Buffer, Orientations) +SHADER_PARAMETER_SRV(Buffer, Colors) +SHADER_PARAMETER_SRV(Buffer, SplatSHDegrees) +SHADER_PARAMETER_SRV(Buffer, SHNonZeroCoeffs) +END_SHADER_PARAMETER_STRUCT() + +UCLASS() +class UCesiumGaussianSplatDataInterface : public UNiagaraDataInterface { + GENERATED_BODY() + + UCesiumGaussianSplatDataInterface(const FObjectInitializer& Initializer); + + virtual bool CanExecuteOnTarget(ENiagaraSimTarget target) const override; + +#if WITH_EDITORONLY_DATA + virtual void GetParameterDefinitionHLSL( + const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, + FString& OutHLSL) override; + virtual bool GetFunctionHLSL( + const FNiagaraDataInterfaceGPUParamInfo& ParamInfo, + const FNiagaraDataInterfaceGeneratedFunction& FunctionInfo, + int FunctionInstanceIndex, + FString& OutHLSL) override; + virtual bool + AppendCompileHash(FNiagaraCompileHashVisitor* InVisitor) const override; + + virtual void + GetFunctions(TArray& OutFunctions) override; +#endif + + virtual void BuildShaderParameters( + FNiagaraShaderParametersBuilder& Builder) const override; + virtual void SetShaderParameters( + const FNiagaraDataInterfaceSetShaderParametersContext& Context) + const override; + + virtual void PostInitProperties() override; + + UCesiumGaussianSplatSubsystem* GetSubsystem() const; + +public: + void Refresh(); + void RefreshMatrices(); + + FScopeLock LockGaussianBuffers(); +}; diff --git a/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.cpp b/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.cpp new file mode 100644 index 000000000..081da234b --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.cpp @@ -0,0 +1,292 @@ +// Copyright 2020-2025 Bentley Systems, Inc. and Contributors + +#include "CesiumGaussianSplatSubsystem.h" + +#include "CesiumRuntime.h" + +#include "Engine/Engine.h" +#include "EngineUtils.h" +#include "RHIGlobals.h" + +#include "NiagaraActor.h" +#include "NiagaraComponent.h" +#include "NiagaraFunctionLibrary.h" +#include "NiagaraSystem.h" + +const int32 RESET_FRAME_COUNT = 5; + +namespace { +FBox CalculateBounds( + const TArray& Components) { + std::optional Bounds; + for (UCesiumGltfGaussianSplatComponent* Component : Components) { + check(Component); + const FTransform& ComponentTransform = Component->GetComponentTransform(); + FBox ActorBounds = Component->GetBounds(); + FVector Center = ComponentTransform.GetTranslation(); + FBox TransformedBounds{ + ComponentTransform.TransformPositionNoScale(ActorBounds.Min), + ComponentTransform.TransformPositionNoScale(ActorBounds.Max)}; + + FVector BoundsMin = TransformedBounds.Min; + FVector BoundsMax = TransformedBounds.Max; + if (Bounds) { + Bounds->Min = FVector( + std::min(Bounds->Min.X, (double)BoundsMin.X), + std::min(Bounds->Min.Y, (double)BoundsMin.Y), + std::min(Bounds->Min.Z, (double)BoundsMin.Z)); + Bounds->Max = FVector( + std::max(Bounds->Max.X, (double)BoundsMax.X), + std::max(Bounds->Max.Y, (double)BoundsMax.Y), + std::max(Bounds->Max.Z, (double)BoundsMax.Z)); + } else { + Bounds = FBox( + FVector4(BoundsMin.X, BoundsMin.Y, BoundsMin.Z, 0.0), + FVector4(BoundsMax.X, BoundsMax.Y, BoundsMax.Z, 0.0)); + } + } + return Bounds.value_or(FBox()); +} + +// Running in a build, there is only one world context at a time. However, in +// play-in-editor, there can be both the editor world context and the +// play-in-editor world context. +// +// We need to choose the "primary world." Technically, it would be best to +// support *all* available world contexts, but considering that we are talking +// about uploading potentially multiple gigabytes of data to the GPU per +// instance, it seems like it might not be wise to be doing it more than once at +// a time unless necessary. +UWorld* GetPrimaryWorld() { +#if WITH_EDITOR + if (!IsValid(GEditor)) { + return nullptr; + } + + const TIndirectArray& Contexts = GEditor->GetWorldContexts(); +#else + if (!IsValid(GEngine)) { + return nullptr; + } + + const TIndirectArray& Contexts = GEngine->GetWorldContexts(); +#endif + + if (Contexts.IsEmpty()) { + return nullptr; + } + + for (const FWorldContext& Context : Contexts) { + if (Context.bIsPrimaryPIEInstance) { + return Context.World(); + } + } + + return Contexts[0].World(); +} +} // namespace + +int32 UCesiumGaussianSplatSubsystem::GetNumSplats() const { + return this->NumSplats; +} + +void UCesiumGaussianSplatSubsystem::InitializeForWorld(UWorld& InWorld) { + for (ACesiumGaussianSplatActor* Actor : + TActorRange(&InWorld)) { + // Actor singleton already exists in the world (usually means we stopped a + // PIE session and returned to the editor world). + this->LastCreatedWorld = &InWorld; + this->NiagaraActor = Actor; + this->NiagaraComponent = + this->NiagaraActor->FindComponentByClass(); + this->RecomputeBounds(); + this->UpdateNiagaraComponent(); + return; + } + + FActorSpawnParameters ActorParams; + ActorParams.Name = FName(TEXT("GaussianSplatSystemActor")); + ActorParams.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested; +#if WITH_EDITOR + ActorParams.bTemporaryEditorActor = true; +#endif + ACesiumGaussianSplatActor* SplatActor = + InWorld.SpawnActor(ActorParams); + SplatActor->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + USceneComponent* SceneComponent = + CastChecked(SplatActor->AddComponentByClass( + USceneComponent::StaticClass(), + false, + FTransform(), + false)); + SplatActor->AddInstanceComponent(SceneComponent); + + UNiagaraSystem* SplatNiagaraSystem = CastChecked< + UNiagaraSystem>(StaticLoadObject( + UNiagaraSystem::StaticClass(), + nullptr, + TEXT( + "/Script/Niagara.NiagaraSystem'/CesiumForUnreal/GaussianSplatting/GaussianSplatSystem.GaussianSplatSystem'"))); + + FFXSystemSpawnParameters SpawnParams; + SpawnParams.WorldContextObject = &InWorld; + SpawnParams.SystemTemplate = SplatNiagaraSystem; + SpawnParams.bAutoDestroy = false; + SpawnParams.AttachToComponent = SceneComponent; + SpawnParams.bAutoActivate = true; + + this->LastCreatedWorld = &InWorld; + this->NiagaraActor = SplatActor; + this->NiagaraComponent = + UNiagaraFunctionLibrary::SpawnSystemAttachedWithParams(SpawnParams); + this->NiagaraComponent->Activate(); + SplatActor->AddInstanceComponent(this->NiagaraComponent); + + this->UpdateNiagaraComponent(); +} + +void UCesiumGaussianSplatSubsystem::Initialize( + FSubsystemCollectionBase& Collection) { + // Unreal will call `Tick` on the Class Default Object for this subsystem. We + // don't want that to happen, because this is supposed to be a singleton, and + // doing so will result in multiple actors being spawned. + // + // Because `Initialize` is never called on the CDO, we can use this as a + // marker of whether we're in the *true* singleton instance of this subsystem. + bIsTickEnabled = true; +} + +void UCesiumGaussianSplatSubsystem::Deinitialize() { + bIsTickEnabled = false; + if (IsValid(this->NiagaraActor)) { + this->NiagaraActor->Destroy(); + } + + this->NiagaraActor = nullptr; + this->NiagaraComponent = nullptr; + this->LastCreatedWorld = nullptr; +} + +void UCesiumGaussianSplatSubsystem::RegisterSplat( + UCesiumGltfGaussianSplatComponent* Component) { + check(Component); + + if (IsValid(this->NiagaraComponent)) { + // Lock buffers when adding components to avoid adding components while + // uploading previous components to GPU + FScopeLock ScopeLock = this->GetSplatInterface()->LockGaussianBuffers(); + this->SplatComponents.Add(Component); + this->NumSplats += Component->NumSplats; + } else { + this->SplatComponents.Add(Component); + this->NumSplats += Component->NumSplats; + } + + this->UpdateNiagaraComponent(); +} + +void UCesiumGaussianSplatSubsystem::UnregisterSplat( + UCesiumGltfGaussianSplatComponent* Component) { + check(Component); + + if (IsValid(this->NiagaraComponent)) { + FScopeLock ScopeLock = this->GetSplatInterface()->LockGaussianBuffers(); + this->SplatComponents.Remove(Component); + this->NumSplats -= Component->NumSplats; + } else { + this->SplatComponents.Remove(Component); + this->NumSplats -= Component->NumSplats; + } + + this->UpdateNiagaraComponent(); +} + +void UCesiumGaussianSplatSubsystem::RecomputeBounds() { + if (IsValid(this->NiagaraComponent)) { + FBox Bounds = CalculateBounds(this->SplatComponents); + const FTransform& NiagaraToWorld = + this->NiagaraComponent->GetComponentTransform(); + this->NiagaraComponent->SetSystemFixedBounds(Bounds); + UE_LOG(LogCesium, Log, TEXT("Setting bounds: %s"), *Bounds.ToString()); + GetSplatInterface()->RefreshMatrices(); + } +} + +void UCesiumGaussianSplatSubsystem::UpdateNiagaraComponent() { + if (IsValid(this->NiagaraComponent)) { + this->NiagaraComponent->SetVariableInt( + FName(TEXT("GridSize")), + (int32)std::ceil(std::cbrt((double)this->NumSplats))); + GetSplatInterface()->Refresh(); + this->bNeedsReset = true; + this->ResetFrameCounter = RESET_FRAME_COUNT; + } +} + +UCesiumGaussianSplatDataInterface* +UCesiumGaussianSplatSubsystem::GetSplatInterface() const { + return UNiagaraFunctionLibrary::GetDataInterface< + UCesiumGaussianSplatDataInterface>( + this->NiagaraComponent, + FName(TEXT("SplatInterface"))); +} + +void UCesiumGaussianSplatSubsystem::Tick(float DeltaTime) { + if (!this->bIsTickEnabled) { + return; + } + + if (this->bNeedsReset) { + this->ResetFrameCounter -= 1; + } + + UWorld* World = GetPrimaryWorld(); + + if (!IsValid(World)) { + if (IsValid(this->NiagaraActor)) { + this->NiagaraActor->Destroy(); + } + + this->NiagaraActor = nullptr; + this->NiagaraComponent = nullptr; + this->LastCreatedWorld = nullptr; + return; + } + + if (IsValid(this->NiagaraActor)) { + if (this->bNeedsReset && this->ResetFrameCounter <= 0) { + // We want to avoid calling ResetSystem multiple times a frame, so we + // combine the calls into one. + this->bNeedsReset = false; + this->NiagaraComponent->ResetSystem(); + } + } + + if (IsValid(this->NiagaraActor) && World == this->LastCreatedWorld) { + return; + } + + this->InitializeForWorld(*World); +} + +ETickableTickType UCesiumGaussianSplatSubsystem::GetTickableTickType() const { + return ETickableTickType::Always; +} + +TStatId UCesiumGaussianSplatSubsystem::GetStatId() const { + RETURN_QUICK_DECLARE_CYCLE_STAT( + UCesiumGaussianSplatSubsystem, + STATGROUP_Tickables); +} + +bool UCesiumGaussianSplatSubsystem::IsTickableWhenPaused() const { + return true; +} + +bool UCesiumGaussianSplatSubsystem::IsTickableInEditor() const { return true; } + +bool UCesiumGaussianSplatSubsystem::IsTickable() const { + return this->bIsTickEnabled; +} diff --git a/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.h b/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.h new file mode 100644 index 000000000..820f30051 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGaussianSplatSubsystem.h @@ -0,0 +1,70 @@ +// Copyright 2020-2025 Bentley Systems, Inc. and Contributors + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" + +#include "NiagaraComponent.h" +#include "NiagaraSystem.h" + +#include "Subsystems/EngineSubsystem.h" + +#include "CesiumGaussianSplatDataInterface.h" +#include "CesiumGltfGaussianSplatComponent.h" + +#include "CesiumGaussianSplatSubsystem.generated.h" + +/** + * A blank actor type just to signify the splat singleton actor. + */ +UCLASS(Transient) +class ACesiumGaussianSplatActor : public AActor { + GENERATED_BODY() + +public: + int32 NumSplatsSpawned = 0; +}; + +UCLASS() +class UCesiumGaussianSplatSubsystem : public UEngineSubsystem, + public FTickableGameObject { + GENERATED_BODY() + +public: + // static UCesiumGaussianSplatSubsystem* Get(UWorld* InWorld); + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + void RegisterSplat(UCesiumGltfGaussianSplatComponent* Component); + void UnregisterSplat(UCesiumGltfGaussianSplatComponent* Component); + void RecomputeBounds(); + int32 GetNumSplats() const; + + TArray SplatComponents; + + virtual void Tick(float DeltaTime) override; + virtual ETickableTickType GetTickableTickType() const override; + virtual TStatId GetStatId() const override; + virtual bool IsTickableWhenPaused() const override; + virtual bool IsTickableInEditor() const override; + virtual bool IsTickable() const override; + +private: + void InitializeForWorld(UWorld& InWorld); + + void UpdateNiagaraComponent(); + UCesiumGaussianSplatDataInterface* GetSplatInterface() const; + + UNiagaraComponent* NiagaraComponent = nullptr; + + ACesiumGaussianSplatActor* NiagaraActor = nullptr; + + UWorld* LastCreatedWorld = nullptr; + bool bIsTickEnabled = false; + int32 NumSplats = 0; + + // Dirty way to avoid running too many resets too quickly. + int32 ResetFrameCounter = 0; + bool bNeedsReset = false; +}; diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index b392dbf24..83cd877dc 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -5,6 +5,7 @@ #include "CesiumCommon.h" #include "CesiumEncodedMetadataUtility.h" #include "CesiumFeatureIdSet.h" +#include "CesiumGltfGaussianSplatComponent.h" #include "CesiumGltfLinesComponent.h" #include "CesiumGltfPointsComponent.h" #include "CesiumGltfPrimitiveComponent.h" @@ -45,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -3035,8 +3037,22 @@ static void loadPrimitiveGameThreadPart( UStaticMeshComponent* pMesh = nullptr; ICesiumPrimitive* pCesiumPrimitive = nullptr; - - if (meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::POINTS) { + bool createMesh = true; + if (meshPrimitive.hasExtension()) { + UCesiumGltfGaussianSplatComponent* pGaussianSplat = + NewObject(pGltf, componentName); + pGaussianSplat->Dimensions = loadResult.dimensions; + // UCesiumGltfGaussianSplatComponent works differently to other primitives - + // it just acts as a source of data for UCesiumGaussianSplatSystem to + // accumulate and render. We do not need to create a mesh from it. + pGaussianSplat->SetData(model, meshPrimitive); + pGaussianSplat->SetupAttachment(pGltf); + pGaussianSplat->RegisterComponent(); + pGaussianSplat->SetMobility(pGltf->Mobility); + pGaussianSplat->RegisterWithSubsystem(); + pCesiumPrimitive = pGaussianSplat; + createMesh = false; + } else if (meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::POINTS) { UCesiumGltfPointsComponent* pPointMesh = NewObject(pGltf, componentName); pPointMesh->UsesAdditiveRefinement = @@ -3076,21 +3092,27 @@ static void loadPrimitiveGameThreadPart( pMesh = pComponent; pCesiumPrimitive = pComponent; } + CesiumPrimitiveData& primData = pCesiumPrimitive->getPrimitiveData(); + primData.pTilesetActor = pTilesetActor; + primData.overlayTextureCoordinateIDToUVIndex = + loadResult.overlayTextureCoordinateIDToUVIndex; + primData.GltfToUnrealTexCoordMap = + std::move(loadResult.GltfToUnrealTexCoordMap); + primData.TexCoordAccessorMap = std::move(loadResult.TexCoordAccessorMap); + primData.PositionAccessor = std::move(loadResult.PositionAccessor); + primData.IndexAccessor = std::move(loadResult.IndexAccessor); + primData.HighPrecisionNodeTransform = loadResult.transform; + pCesiumPrimitive->UpdateTransformFromCesium(cesiumToUnrealTransform); + + if (!createMesh) { + return; + } + UStaticMesh* pStaticMesh; { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetupMesh) - primData.pTilesetActor = pTilesetActor; - primData.overlayTextureCoordinateIDToUVIndex = - loadResult.overlayTextureCoordinateIDToUVIndex; - primData.GltfToUnrealTexCoordMap = - std::move(loadResult.GltfToUnrealTexCoordMap); - primData.TexCoordAccessorMap = std::move(loadResult.TexCoordAccessorMap); - primData.PositionAccessor = std::move(loadResult.PositionAccessor); - primData.IndexAccessor = std::move(loadResult.IndexAccessor); - primData.HighPrecisionNodeTransform = loadResult.transform; - pCesiumPrimitive->UpdateTransformFromCesium(cesiumToUnrealTransform); pMesh->bUseDefaultCollision = false; pMesh->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic); pMesh->SetFlags( diff --git a/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.cpp new file mode 100644 index 000000000..eb8c00bf2 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.cpp @@ -0,0 +1,456 @@ +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +#include "CesiumGltfGaussianSplatComponent.h" + +#include "CesiumGaussianSplatSubsystem.h" +#include "CesiumRuntime.h" + +#include +#include + +#include +#include +#include +#include + +#include + +namespace { +// We multiplied by 1024 because of the physics hack when building the glTF +// primitive. This is the factor we use to reverse that operation. +// This is 10.24, not 1024.0, because we *do* need to scale up by 100 units +// because of glTF -> Unreal units conversions. +const double RESCALE_FACTOR = 1024.0 / 100.0; + +int32 countShCoeffsOnPrimitive(CesiumGltf::MeshPrimitive& primitive) { + if (primitive.attributes.contains( + "KHR_gaussian_splatting:SH_DEGREE_3_COEF_6")) { + return 15; + } + + if (primitive.attributes.contains( + "KHR_gaussian_splatting:SH_DEGREE_2_COEF_4")) { + return 8; + } + + if (primitive.attributes.contains( + "KHR_gaussian_splatting:SH_DEGREE_1_COEF_2")) { + return 3; + } + + return 0; +} + +bool writeShCoeffs( + CesiumGltf::Model& model, + CesiumGltf::MeshPrimitive& meshPrimitive, + TArray& data, + int32 stride, + int32 offset, + int32 degree) { + const int32 numCoeffs = 3 + 2 * (degree - 1); + for (int32 i = 0; i < numCoeffs; i++) { + std::unordered_map::const_iterator accessorIt = + meshPrimitive.attributes.find(fmt::format( + "KHR_gaussian_splatting:SH_DEGREE_{}_COEF_{}", + degree, + i)); + if (accessorIt == meshPrimitive.attributes.end()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Could not find spherical harmonic attribute for degree %d index %d on mesh primitive"), + degree, + i); + return false; + } + + CesiumGltf::AccessorView accessorView(model, accessorIt->second); + if (accessorView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Accessor view for spherical harmonic attribute degree %d index %d on mesh primitive returned invalid status: %d"), + degree, + i, + accessorView.status()); + return false; + } + + for (int32 j = 0; j < accessorView.size(); j++) { + data[j * stride + offset + i * 4] = accessorView[j].x; + data[j * stride + offset + i * 4 + 1] = accessorView[j].y; + data[j * stride + offset + i * 4 + 2] = accessorView[j].z; + data[j * stride + offset + i * 4 + 3] = 0.0; + } + } + + return true; +} + +template +void writeConvertedAccessor( + CesiumGltf::AccessorView& accessorView, + TArray& data, + int32 stride) { + for (int32 i = 0; i < accessorView.size(); i++) { + data[i * stride] = accessorView[i].x / + static_cast(TNumericLimits::Max()); + data[i * stride + 1] = + accessorView[i].y / + static_cast(TNumericLimits::Max()); + data[i * stride + 2] = + accessorView[i].z / + static_cast(TNumericLimits::Max()); + data[i * stride + 3] = + accessorView[i].w / + static_cast(TNumericLimits::Max()); + } +} +} // namespace + +// Sets default values for this component's properties +UCesiumGltfGaussianSplatComponent::UCesiumGltfGaussianSplatComponent() + : Dimensions(glm::vec3(0)) {} + +UCesiumGltfGaussianSplatComponent::~UCesiumGltfGaussianSplatComponent() {} + +void UCesiumGltfGaussianSplatComponent::UpdateTransformFromCesium( + const glm::dmat4& CesiumToUnrealTransform) { + UCesiumGltfPrimitiveComponent::UpdateTransformFromCesium( + CesiumToUnrealTransform); + + const FTransform& Transform = this->GetComponentTransform(); + + check(GEngine); + UCesiumGaussianSplatSubsystem* SplatSubsystem = + GEngine->GetEngineSubsystem(); + ensure(SplatSubsystem); + + SplatSubsystem->RecomputeBounds(); +} + +void UCesiumGltfGaussianSplatComponent::OnUpdateTransform( + EUpdateTransformFlags /*UpdateTransformFlags*/, + ETeleportType /*Teleport*/) { + const FTransform& Transform = this->GetComponentTransform(); + check(GEngine); + UCesiumGaussianSplatSubsystem* SplatSubsystem = + GEngine->GetEngineSubsystem(); + ensure(SplatSubsystem); + + SplatSubsystem->RecomputeBounds(); +} + +void UCesiumGltfGaussianSplatComponent::OnVisibilityChanged() { + const FTransform& Transform = this->GetComponentTransform(); + check(GEngine); + UCesiumGaussianSplatSubsystem* SplatSubsystem = + GEngine->GetEngineSubsystem(); + ensure(SplatSubsystem); + + SplatSubsystem->RecomputeBounds(); + UE_LOG( + LogCesium, + Log, + TEXT("Transform visible: %s"), + this->IsVisible() ? TEXT("true") : TEXT("false")); +} + +void UCesiumGltfGaussianSplatComponent::SetData( + CesiumGltf::Model& model, + CesiumGltf::MeshPrimitive& meshPrimitive) { + const int32 numShCoeffs = countShCoeffsOnPrimitive(meshPrimitive); + this->NumCoefficients = numShCoeffs; + + const std::unordered_map::const_iterator positionIt = + meshPrimitive.attributes.find("POSITION"); + if (positionIt == meshPrimitive.attributes.end()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Mesh primitive has no 'POSITION' attribute")); + return; + } + + CesiumGltf::AccessorView positionView(model, positionIt->second); + if (positionView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'POSITION' accessor view on mesh primitive returned invalid status: %d"), + positionView.status()); + return; + } + + this->Positions.SetNum(positionView.size() * 4, true); + this->NumSplats = positionView.size(); + for (int32 i = 0; i < positionView.size(); i++) { + FVector Position( + positionView[i].x * CesiumPrimitiveData::positionScaleFactor, + positionView[i].y * -CesiumPrimitiveData::positionScaleFactor, + positionView[i].z * CesiumPrimitiveData::positionScaleFactor); + this->Positions[i * 4] = Position.X; + this->Positions[i * 4 + 1] = Position.Y; + this->Positions[i * 4 + 2] = Position.Z; + // We need a W component because Unreal can only upload float2s or float4s + // to the GPU, not float3s... + this->Positions[i * 4 + 3] = 0.0; + + // Take this opportunity to update the bounds. + if (this->Bounds) { + this->Bounds->Min = FVector( + std::min(Bounds->Min.X, Position.X / RESCALE_FACTOR), + std::min(Bounds->Min.Y, Position.Y / RESCALE_FACTOR), + std::min(Bounds->Min.Z, Position.Z / RESCALE_FACTOR)); + this->Bounds->Max = FVector( + std::max(Bounds->Max.X, Position.X / RESCALE_FACTOR), + std::max(Bounds->Max.Y, Position.Y / RESCALE_FACTOR), + std::max(Bounds->Max.Z, Position.Z / RESCALE_FACTOR)); + } else { + this->Bounds = FBox(); + this->Bounds->Min = FVector( + Position.X / RESCALE_FACTOR, + Position.Y / RESCALE_FACTOR, + Position.Z / RESCALE_FACTOR); + this->Bounds->Max = Bounds->Min; + } + } + + const std::unordered_map::const_iterator scaleIt = + meshPrimitive.attributes.find("KHR_gaussian_splatting:SCALE"); + if (scaleIt == meshPrimitive.attributes.end()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Mesh primitive has no 'KHR_gaussian_splatting:SCALE' attribute")); + return; + } + + CesiumGltf::AccessorView scaleView(model, scaleIt->second); + if (scaleView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'KHR_gaussian_splatting:SCALE' accessor view on mesh primitive returned invalid status: %d"), + scaleView.status()); + return; + } + + this->Scales.SetNum(scaleView.size() * 4, true); + for (int32 i = 0; i < scaleView.size(); i++) { + this->Scales[i * 4] = + scaleView[i].x * CesiumPrimitiveData::positionScaleFactor; + this->Scales[i * 4 + 1] = + scaleView[i].y * CesiumPrimitiveData::positionScaleFactor; + this->Scales[i * 4 + 2] = + scaleView[i].z * CesiumPrimitiveData::positionScaleFactor; + this->Scales[i * 4 + 3] = 0.0; + } + + const std::unordered_map::const_iterator rotationIt = + meshPrimitive.attributes.find("KHR_gaussian_splatting:ROTATION"); + if (rotationIt == meshPrimitive.attributes.end()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Mesh primitive has no 'KHR_gaussian_splatting:ROTATION' attribute")); + return; + } + + CesiumGltf::AccessorView rotationView(model, rotationIt->second); + if (rotationView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'KHR_gaussian_splatting:ROTATION' accessor view on mesh primitive returned invalid status: %d"), + rotationView.status()); + return; + } + + this->Orientations.SetNum(rotationView.size() * 4, true); + for (int32 i = 0; i < rotationView.size(); i++) { + this->Orientations[i * 4] = rotationView[i].x; + this->Orientations[i * 4 + 1] = -rotationView[i].y; + this->Orientations[i * 4 + 2] = rotationView[i].z; + this->Orientations[i * 4 + 3] = rotationView[i].w; + } + + const std::unordered_map::const_iterator colorIt = + meshPrimitive.attributes.find("COLOR_0"); + if (colorIt == meshPrimitive.attributes.end()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Mesh primitive has no 'COLOR_0' attribute")); + return; + } + + if (colorIt->second < 0 || colorIt->second >= model.accessors.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Mesh primitive has invalid 'COLOR_0' accessor index %d"), + colorIt->second); + return; + } + + CesiumGltf::Accessor& colorAccessor = model.accessors[colorIt->second]; + if (colorAccessor.componentType == + CesiumGltf::AccessorSpec::ComponentType::UNSIGNED_BYTE) { + CesiumGltf::AccessorView accessorView(model, colorIt->second); + if (accessorView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'COLOR_0' accessor view on mesh primitive returned invalid status: %d"), + accessorView.status()); + return; + } + + this->Colors.SetNum(accessorView.size() * 4, true); + writeConvertedAccessor(accessorView, this->Colors, 4); + } else if ( + colorAccessor.componentType == + CesiumGltf::AccessorSpec::ComponentType::UNSIGNED_SHORT) { + CesiumGltf::AccessorView accessorView(model, colorIt->second); + if (accessorView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'COLOR_0' accessor view on mesh primitive returned invalid status: %d"), + accessorView.status()); + return; + } + + this->Colors.SetNum(accessorView.size() * 4, true); + writeConvertedAccessor(accessorView, this->Colors, 4); + } else if ( + colorAccessor.componentType == + CesiumGltf::AccessorSpec::ComponentType::FLOAT) { + CesiumGltf::AccessorView accessorView(model, colorIt->second); + if (accessorView.status() != CesiumGltf::AccessorViewStatus::Valid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "'COLOR_0' accessor view on mesh primitive returned invalid status: %d"), + accessorView.status()); + return; + } + + this->Colors.SetNum(accessorView.size() * 4, true); + for (int32 i = 0; i < accessorView.size(); i++) { + this->Colors[i * 4] = accessorView[i].x; + this->Colors[i * 4 + 1] = accessorView[i].y; + this->Colors[i * 4 + 2] = accessorView[i].z; + this->Colors[i * 4 + 3] = accessorView[i].w; + } + } else { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Invalid 'COLOR_0' componentType. Allowed values are UNSIGNED_BYTE, UNSIGNED_SHORT, and FLOAT.")); + return; + } + + const int32 shStride = numShCoeffs * 4; + this->SphericalHarmonics.SetNum(shStride * this->NumSplats); + if (numShCoeffs >= 3) { + if (!writeShCoeffs( + model, + meshPrimitive, + this->SphericalHarmonics, + shStride, + 0, + 1)) { + return; + } + } + + if (numShCoeffs >= 8) { + if (!writeShCoeffs( + model, + meshPrimitive, + this->SphericalHarmonics, + shStride, + 3 * 4, + 2)) { + return; + } + } + + if (numShCoeffs >= 15) { + if (!writeShCoeffs( + model, + meshPrimitive, + this->SphericalHarmonics, + shStride, + 5 * 4 + 3 * 4, + 3)) { + return; + } + } +} + +FBox UCesiumGltfGaussianSplatComponent::GetBounds() const { + return this->Bounds.Get(FBox()); +} + +glm::mat4x4 UCesiumGltfGaussianSplatComponent::GetMatrix() const { + const FTransform& Transform = this->GetComponentTransform(); + glm::quat quat( + Transform.GetRotation().W, + Transform.GetRotation().X, + Transform.GetRotation().Y, + Transform.GetRotation().Z); + const glm::mat4x4 mat = glm::translate( + glm::identity(), + glm::vec3( + Transform.GetLocation().X, + Transform.GetLocation().Y, + Transform.GetLocation().Z)) * + glm::mat4_cast(quat) * + glm::scale( + glm::identity(), + glm::vec3( + Transform.GetScale3D().X, + Transform.GetScale3D().Y, + Transform.GetScale3D().Z)); + return mat; +} + +void UCesiumGltfGaussianSplatComponent::RegisterWithSubsystem() { + check(GEngine); + UCesiumGaussianSplatSubsystem* SplatSubsystem = + GEngine->GetEngineSubsystem(); + ensure(SplatSubsystem); + + SplatSubsystem->RegisterSplat(this); +} + +void UCesiumGltfGaussianSplatComponent::BeginDestroy() { + Super::BeginDestroy(); + + if (!IsValid(GEngine)) { + return; + } + + UCesiumGaussianSplatSubsystem* SplatSubsystem = + GEngine->GetEngineSubsystem(); + + if (!IsValid(SplatSubsystem)) { + return; + } + + SplatSubsystem->UnregisterSplat(this); +} diff --git a/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.h b/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.h new file mode 100644 index 000000000..5cb0165e0 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfGaussianSplatComponent.h @@ -0,0 +1,61 @@ +// Copyright 2020-2025 Bentley Systems, Inc. and Contributors + +#pragma once + +#include "CesiumGltfPrimitiveComponent.h" + +#include + +#include + +#include "CesiumGltfGaussianSplatComponent.generated.h" + +/** + * A component that represents and renders a glTF gaussian splat. + */ +UCLASS() +class UCesiumGltfGaussianSplatComponent : public UCesiumGltfPrimitiveComponent { + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UCesiumGltfGaussianSplatComponent(); + virtual ~UCesiumGltfGaussianSplatComponent(); + + // The dimensions of the point component. Used to estimate the geometric + // error. + glm::vec3 Dimensions; + + virtual void + UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform) override; + + virtual void OnVisibilityChanged() override; + + virtual void OnUpdateTransform( + EUpdateTransformFlags UpdateTransformFlags, + ETeleportType Teleport) override; + + void + SetData(CesiumGltf::Model& model, CesiumGltf::MeshPrimitive& meshPrimitive); + + void RegisterWithSubsystem(); + + FBox GetBounds() const; + + glm::mat4x4 GetMatrix() const; + + virtual void BeginDestroy() override; + + TArray Positions; + TArray Scales; + TArray Orientations; + TArray Colors; + TArray SphericalHarmonics; + + int32 NumCoefficients = 0; + + int32 NumSplats = 0; + +private: + TOptional Bounds; +}; diff --git a/Source/CesiumRuntime/Private/CesiumPrimitive.h b/Source/CesiumRuntime/Private/CesiumPrimitive.h index da54c66ed..0e78a55f9 100644 --- a/Source/CesiumRuntime/Private/CesiumPrimitive.h +++ b/Source/CesiumRuntime/Private/CesiumPrimitive.h @@ -140,7 +140,6 @@ class ICesiumPrimitive { public: virtual CesiumPrimitiveData& getPrimitiveData() = 0; virtual const CesiumPrimitiveData& getPrimitiveData() const = 0; - virtual void UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform) = 0; }; diff --git a/extern/cesium-native b/extern/cesium-native index 126a412a7..fc6802053 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 126a412a791dd596c5ed81c05ef2443339c64ff8 +Subproject commit fc680205351d88bd5f7a7290c89ab940123a0876