diff --git a/CHANGES.md b/CHANGES.md index f5324fb74..7d124f020 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log {#changes} +### ? - ? + +##### Additions :tada: + +- Added support for rendering glTFs with line primitives. + ### v2.19.1 - 2025-09-02 ##### Fixes :wrench: diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 4df6f8457..b392dbf24 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 "CesiumGltfLinesComponent.h" #include "CesiumGltfPointsComponent.h" #include "CesiumGltfPrimitiveComponent.h" #include "CesiumGltfTextures.h" @@ -682,14 +683,21 @@ static void setUnlitNormals( glm::dvec4( VecMath::createVector3D(FVector(positionBuffer.VertexPosition(i))), 1.0)); - glm::dvec3 normal = ellipsoid.geodeticSurfaceNormal(positionFixed); + + glm::dvec3 surfaceNormal = ellipsoid.geodeticSurfaceNormal(positionFixed); + + if (surfaceNormal == glm::dvec3(0.0)) { + // This can happen for tilesets georeferenced at the center of the earth, + // resulting in NaN normals. Manually assign a normal aligned with Z-up. + surfaceNormal = glm::dvec3(0.0, 0.0, 1.0); + } normalBuffer.SetVertexTangents( i, FVector3f(0.0f), FVector3f(0.0), - FVector3f(VecMath::createVector( - glm::normalize(ellipsoidFixedToVertex * glm::dvec4(normal, 0.0))))); + FVector3f(VecMath::createVector(glm::normalize( + ellipsoidFixedToVertex * glm::dvec4(surfaceNormal, 0.0))))); } } @@ -1259,36 +1267,6 @@ std::string getPrimitiveName( return name; } -/// Helper used to log only once per unsupported primitive mode. -struct PrimitiveModeLogger { - std::array< - std::atomic_bool, - (size_t)CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN + 1> - alreadyLogged; - - PrimitiveModeLogger() - : alreadyLogged{ - {{false}, {false}, {false}, {false}, {false}, {false}, {false}}} {} - - inline void OnUnsupportedMode(int32_t primMode) { - bool bPrintLog = false; - if (primMode < 0 || primMode >= (int32_t)alreadyLogged.size()) { - ensureMsgf(false, TEXT("Unknown primitive mode %d!"), primMode); - bPrintLog = true; - } else if (!alreadyLogged[(size_t)primMode].exchange(true)) { - bPrintLog = true; - } - if (bPrintLog) { - UE_LOG( - LogCesium, - Warning, - TEXT("Primitive mode %d is not supported"), - primMode); - } - } -}; -static PrimitiveModeLogger UnsupportedPrimitiveLogger; - template TArray getIndices(const TIndexAccessor& indicesView, int32 primitiveMode) { @@ -1315,9 +1293,8 @@ getIndices(const TIndexAccessor& indicesView, int32 primitiveMode) { } break; case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN: - // The TRIANGLE_FAN primitive mode cannot be enabled without creating a - // custom render proxy, so the geometry must be emulated through separate - // triangles. + // The TRIANGLE_FAN primitive mode is not supported in Unreal, so geometry + // must be emulated through separate triangles. indices.SetNum( static_cast::SizeType>(3 * (indicesView.size() - 2))); for (int32 i = 2, j = 0; i < indicesView.size(); ++i, j += 3) { @@ -1326,7 +1303,31 @@ getIndices(const TIndexAccessor& indicesView, int32 primitiveMode) { indices[j + 2] = indicesView[i]; } break; + case CesiumGltf::MeshPrimitive::Mode::LINE_LOOP: + // The LINE_LOOP primitive mode is not supported in Unreal, so geometry must + // be emulated through separate lines. + indices.SetNum( + static_cast::SizeType>(2 * indicesView.size())); + for (int32 i = 0, j = 0; i < indicesView.size(); ++i, j += 2) { + // Loop to the first index once we reach the last line segment. + size_t nextIndex = (i < indicesView.size() - 1) ? i + 1 : 0; + + indices[j] = indicesView[i]; + indices[j + 1] = indicesView[nextIndex]; + } + break; + case CesiumGltf::MeshPrimitive::Mode::LINE_STRIP: + // The LINE_STRIP primitive mode is not supported in Unreal, so geometry + // must be emulated through separate lines. + indices.SetNum( + static_cast::SizeType>(2 * (indicesView.size() - 1))); + for (int32 i = 0, j = 0; i < indicesView.size() - 1; ++i, j += 2) { + indices[j] = indicesView[i]; + indices[j + 1] = indicesView[i + 1]; + } + break; case CesiumGltf::MeshPrimitive::Mode::TRIANGLES: + case CesiumGltf::MeshPrimitive::Mode::LINES: case CesiumGltf::MeshPrimitive::Mode::POINTS: default: indices.SetNum(static_cast::SizeType>(indicesView.size())); @@ -1358,18 +1359,6 @@ static void loadPrimitive( CesiumGltf::MeshPrimitive& primitive = mesh.primitives[options.primitiveIndex]; - switch (primitive.mode) { - case CesiumGltf::MeshPrimitive::Mode::POINTS: - case CesiumGltf::MeshPrimitive::Mode::TRIANGLES: - case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_STRIP: - case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN: - break; - default: - // TODO: add support for other primitive types. - UnsupportedPrimitiveLogger.OnUnsupportedMode(primitive.mode); - return; - } - const std::string name = getPrimitiveName(model, mesh, primitive); primitiveResult.name = name; @@ -1426,14 +1415,14 @@ static void loadPrimitive( !options.pMeshOptions->pNodeOptions->pModelOptions ->ignoreKhrMaterialsUnlit; - // We can't calculate flat normals for points or lines, so we have to force - // them to be unlit if no normals are specified. Otherwise this causes a - // crash when attempting to calculate flat normals. bool isTriangles = primitive.mode == CesiumGltf::MeshPrimitive::Mode::TRIANGLES || primitive.mode == CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN || primitive.mode == CesiumGltf::MeshPrimitive::Mode::TRIANGLE_STRIP; + // We can't calculate flat normals for points or lines, so we have to force + // them to be unlit if no normals are specified. Otherwise this causes a + // crash when attempting to calculate flat normals. if (!isTriangles && !hasNormals) { primitiveResult.isUnlit = true; } @@ -1536,12 +1525,23 @@ static void loadPrimitive( // vertices shared by multiple triangles. If we don't have tangents, but // need them, we need to use a tangent space generation algorithm which // requires duplicated vertices. - bool normalsAreRequired = !primitiveResult.isUnlit; + bool normalsAreRequired = !primitiveResult.isUnlit && isTriangles; bool needToGenerateFlatNormals = normalsAreRequired && !hasNormals; bool needToGenerateTangents = needsTangents && !hasTangents; bool duplicateVertices = needToGenerateFlatNormals || needToGenerateTangents; - duplicateVertices = duplicateVertices && - primitive.mode != CesiumGltf::MeshPrimitive::Mode::POINTS; + + // Some primitive modes may require duplication of vertices anyways due to + // the lack of support in Unreal. + switch (primitive.mode) { + case CesiumGltf::MeshPrimitive::Mode::LINE_LOOP: + case CesiumGltf::MeshPrimitive::Mode::LINE_STRIP: + case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN: + case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_STRIP: + duplicateVertices = true; + break; + default: + break; + } uint32 numVertices = duplicateVertices ? uint32(indices.Num()) : uint32(positionView.size()); @@ -1707,16 +1707,14 @@ static void loadPrimitive( FVector3f(normal.X, -normal.Y, normal.Z)); } } + } else if (primitiveResult.isUnlit || !isTriangles) { + setUnlitNormals( + LODResources.VertexBuffers, + ellipsoid, + transform * yInvertMatrix * scaleMatrix); } else { - if (primitiveResult.isUnlit) { - setUnlitNormals( - LODResources.VertexBuffers, - ellipsoid, - transform * yInvertMatrix * scaleMatrix); - } else { - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::ComputeFlatNormals) - computeFlatNormals(LODResources.VertexBuffers); - } + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::ComputeFlatNormals) + computeFlatNormals(LODResources.VertexBuffers); } if (hasTangents) { @@ -1753,13 +1751,12 @@ static void loadPrimitive( FStaticMeshSectionArray& Sections = LODResources.Sections; FStaticMeshSection& section = Sections.AddDefaulted_GetRef(); - // This will be ignored if the primitive contains points. + // This will be ignored if the primitive contains lines or points. section.NumTriangles = indices.Num() / 3; section.FirstIndex = 0; section.MinVertexIndex = 0; section.MaxVertexIndex = numVertices - 1; - section.bEnableCollision = - primitive.mode != CesiumGltf::MeshPrimitive::Mode::POINTS; + section.bEnableCollision = isTriangles; section.bCastShadow = true; section.MaterialIndex = 0; @@ -1784,9 +1781,7 @@ static void loadPrimitive( LODResources.bHasReversedDepthOnlyIndices = false; #if ENGINE_VERSION_5_5_OR_HIGHER - // UE 5.5 requires that we do this in order to avoid a crash when ray - // tracing is enabled. - if (primitive.mode != CesiumGltf::MeshPrimitive::Mode::POINTS) { + if (isTriangles) { // UE 5.5 requires that we do this in order to avoid a crash when ray // tracing is enabled. RenderData->InitializeRayTracingRepresentationFromRenderingLODs(); @@ -1800,7 +1795,7 @@ static void loadPrimitive( primitiveResult.transform = transform * yInvertMatrix * scaleMatrix; - if (primitive.mode != CesiumGltf::MeshPrimitive::Mode::POINTS && + if (isTriangles && options.pMeshOptions->pNodeOptions->pModelOptions->createPhysicsMeshes) { if (numVertices != 0 && indices.Num() != 0) { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::ChaosCook) @@ -3040,6 +3035,7 @@ static void loadPrimitiveGameThreadPart( UStaticMeshComponent* pMesh = nullptr; ICesiumPrimitive* pCesiumPrimitive = nullptr; + if (meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::POINTS) { UCesiumGltfPointsComponent* pPointMesh = NewObject(pGltf, componentName); @@ -3049,6 +3045,14 @@ static void loadPrimitiveGameThreadPart( pPointMesh->Dimensions = loadResult.dimensions; pMesh = pPointMesh; pCesiumPrimitive = pPointMesh; + } else if ( + meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::LINES || + meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::LINE_LOOP || + meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::LINE_STRIP) { + UCesiumGltfLinesComponent* pLineMesh = + NewObject(pGltf, componentName); + pMesh = pLineMesh; + pCesiumPrimitive = pLineMesh; } else if (!instanceTransforms.empty()) { auto* pInstancedComponent = NewObject(pGltf, componentName); @@ -3110,11 +3114,18 @@ static void loadPrimitiveGameThreadPart( primData.pTilesetActor->GetTranslucencySortPriority(); pStaticMesh = NewObject(pMesh, componentName); - // Not only does the concept of ray tracing a point cloud not make much - // sense, but if Unreal will crash trying to generate ray tracing - // information for a static mesh without triangles. - pStaticMesh->bSupportRayTracing = - meshPrimitive.mode != CesiumGltf::MeshPrimitive::Mode::POINTS; + // Unreal will crash trying to generate ray tracing information for a static + // mesh without triangles (and it doesn't make sense anyways!) + switch (meshPrimitive.mode) { + case CesiumGltf::MeshPrimitive::Mode::TRIANGLES: + case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_FAN: + case CesiumGltf::MeshPrimitive::Mode::TRIANGLE_STRIP: + pStaticMesh->bSupportRayTracing = true; + break; + default: + pStaticMesh->bSupportRayTracing = false; + break; + } pMesh->SetStaticMesh(pStaticMesh); pStaticMesh->SetFlags( diff --git a/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.cpp new file mode 100644 index 000000000..1c4d431ff --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.cpp @@ -0,0 +1,18 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumGltfLinesComponent.h" +#include "CesiumGltfLinesSceneProxy.h" +#include "SceneInterface.h" + +// Sets default values for this component's properties +UCesiumGltfLinesComponent::UCesiumGltfLinesComponent() {} + +UCesiumGltfLinesComponent::~UCesiumGltfLinesComponent() {} + +FPrimitiveSceneProxy* UCesiumGltfLinesComponent::CreateSceneProxy() { + if (!IsValid(this)) { + return nullptr; + } + + return new FCesiumGltfLinesSceneProxy(this, GetScene()->GetFeatureLevel()); +} diff --git a/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.h b/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.h new file mode 100644 index 000000000..ab4c2dacb --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfLinesComponent.h @@ -0,0 +1,22 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumGltfPrimitiveComponent.h" +#include "CesiumGltfLinesComponent.generated.h" + +/** + * A component that represents and renders a glTF lines primitive. + */ +UCLASS() +class UCesiumGltfLinesComponent : public UCesiumGltfPrimitiveComponent { + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UCesiumGltfLinesComponent(); + virtual ~UCesiumGltfLinesComponent(); + + // Override UPrimitiveComponent interface. + virtual FPrimitiveSceneProxy* CreateSceneProxy() override; +}; diff --git a/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.cpp b/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.cpp new file mode 100644 index 000000000..e2284cf35 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.cpp @@ -0,0 +1,101 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumGltfLinesSceneProxy.h" +#include "CesiumGltfLinesComponent.h" +#include "DataDrivenShaderPlatformInfo.h" +#include "Engine/StaticMesh.h" +#include "RHIResources.h" +#include "Runtime/Launch/Resources/Version.h" +#include "SceneInterface.h" +#include "SceneView.h" +#include "StaticMeshResources.h" + +SIZE_T FCesiumGltfLinesSceneProxy::GetTypeHash() const { + static size_t UniquePointer; + return reinterpret_cast(&UniquePointer); +} + +FCesiumGltfLinesSceneProxy::FCesiumGltfLinesSceneProxy( + UCesiumGltfLinesComponent* InComponent, + ERHIFeatureLevel::Type InFeatureLevel) + : FPrimitiveSceneProxy(InComponent), + RenderData(InComponent->GetStaticMesh()->GetRenderData()), + NumLines(RenderData->LODResources[0].IndexBuffer.GetNumIndices() / 2), + Material(InComponent->GetMaterial(0)), + MaterialRelevance(InComponent->GetMaterialRelevance(InFeatureLevel)) {} + +FCesiumGltfLinesSceneProxy::~FCesiumGltfLinesSceneProxy() {} + +void FCesiumGltfLinesSceneProxy::DrawStaticElements( + FStaticPrimitiveDrawInterface* PDI) { + if (!HasViewDependentDPG()) { + FMeshBatch Mesh; + CreateMesh(Mesh); + PDI->DrawMesh(Mesh, FLT_MAX); + } +} + +void FCesiumGltfLinesSceneProxy::GetDynamicMeshElements( + const TArray& Views, + const FSceneViewFamily& ViewFamily, + uint32 VisibilityMap, + FMeshElementCollector& Collector) const { + QUICK_SCOPE_CYCLE_COUNTER(STAT_GltfLinesSceneProxy_GetDynamicMeshElements); + + for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++) { + if (VisibilityMap & (1 << ViewIndex)) { + const FSceneView* View = Views[ViewIndex]; + FMeshBatch& Mesh = Collector.AllocateMesh(); + CreateMesh(Mesh); + Collector.AddMesh(ViewIndex, Mesh); + } + } +} + +FPrimitiveViewRelevance +FCesiumGltfLinesSceneProxy::GetViewRelevance(const FSceneView* View) const { + FPrimitiveViewRelevance Result; + Result.bDrawRelevance = IsShown(View); + + if (HasViewDependentDPG()) { + Result.bDynamicRelevance = true; + } else { + Result.bStaticRelevance = true; + } + + Result.bRenderCustomDepth = ShouldRenderCustomDepth(); + Result.bRenderInMainPass = ShouldRenderInMainPass(); + Result.bRenderInDepthPass = ShouldRenderInDepthPass(); + Result.bUsesLightingChannels = + GetLightingChannelMask() != GetDefaultLightingChannelMask(); + Result.bShadowRelevance = IsShadowCast(View); + Result.bVelocityRelevance = + IsMovable() & Result.bOpaque & Result.bRenderInMainPass; + + MaterialRelevance.SetPrimitiveViewRelevance(Result); + + return Result; +} + +uint32 FCesiumGltfLinesSceneProxy::GetMemoryFootprint(void) const { + return (sizeof(*this) + GetAllocatedSize()); +} + +void FCesiumGltfLinesSceneProxy::CreateMesh(FMeshBatch& Mesh) const { + Mesh.VertexFactory = &RenderData->LODVertexFactories[0].VertexFactory; + Mesh.MaterialRenderProxy = Material->GetRenderProxy(); + Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative(); + Mesh.Type = PT_LineList; + Mesh.DepthPriorityGroup = SDPG_World; + Mesh.LODIndex = 0; + Mesh.bCanApplyViewModeOverrides = false; + Mesh.bUseAsOccluder = false; + Mesh.bWireframe = false; + + FMeshBatchElement& BatchElement = Mesh.Elements[0]; + BatchElement.IndexBuffer = &RenderData->LODResources[0].IndexBuffer; + BatchElement.NumPrimitives = NumLines; + BatchElement.FirstIndex = 0; + BatchElement.MinVertexIndex = 0; + BatchElement.MaxVertexIndex = BatchElement.NumPrimitives; +} diff --git a/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.h b/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.h new file mode 100644 index 000000000..a852d8446 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.h @@ -0,0 +1,43 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "PrimitiveSceneProxy.h" + +class UCesiumGltfLinesComponent; + +class FCesiumGltfLinesSceneProxy final : public FPrimitiveSceneProxy { +private: + // The original render data of the static mesh. + const FStaticMeshRenderData* RenderData; + int32_t NumLines; + +public: + SIZE_T GetTypeHash() const override; + + FCesiumGltfLinesSceneProxy( + UCesiumGltfLinesComponent* InComponent, + ERHIFeatureLevel::Type InFeatureLevel); + + virtual ~FCesiumGltfLinesSceneProxy(); + +protected: + virtual void DrawStaticElements(FStaticPrimitiveDrawInterface* PDI) override; + + virtual void GetDynamicMeshElements( + const TArray& Views, + const FSceneViewFamily& ViewFamily, + uint32 VisibilityMap, + FMeshElementCollector& Collector) const override; + + virtual FPrimitiveViewRelevance + GetViewRelevance(const FSceneView* View) const override; + + virtual uint32 GetMemoryFootprint(void) const override; + +private: + UMaterialInterface* Material; + FMaterialRelevance MaterialRelevance; + + void CreateMesh(FMeshBatch& Mesh) const; +};