Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
126 changes: 66 additions & 60 deletions Source/CesiumRuntime/Private/CesiumGltfComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1259,36 +1260,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 <class TIndexAccessor>
TArray<uint32>
getIndices(const TIndexAccessor& indicesView, int32 primitiveMode) {
Expand All @@ -1315,9 +1286,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<TArray<uint32>::SizeType>(3 * (indicesView.size() - 2)));
for (int32 i = 2, j = 0; i < indicesView.size(); ++i, j += 3) {
Expand All @@ -1326,7 +1296,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<TArray<uint32>::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<TArray<uint32>::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<TArray<uint32>::SizeType>(indicesView.size()));
Expand Down Expand Up @@ -1358,18 +1352,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;

Expand Down Expand Up @@ -1536,12 +1518,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());
Expand Down Expand Up @@ -1708,7 +1701,7 @@ static void loadPrimitive(
}
}
} else {
if (primitiveResult.isUnlit) {
if (primitiveResult.isUnlit || !isTriangles) {
setUnlitNormals(
LODResources.VertexBuffers,
ellipsoid,
Expand Down Expand Up @@ -1758,8 +1751,7 @@ static void loadPrimitive(
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;

Expand All @@ -1784,9 +1776,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();
Expand All @@ -1800,7 +1790,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)
Expand Down Expand Up @@ -3040,6 +3030,7 @@ static void loadPrimitiveGameThreadPart(

UStaticMeshComponent* pMesh = nullptr;
ICesiumPrimitive* pCesiumPrimitive = nullptr;

if (meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::POINTS) {
UCesiumGltfPointsComponent* pPointMesh =
NewObject<UCesiumGltfPointsComponent>(pGltf, componentName);
Expand All @@ -3049,6 +3040,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<UCesiumGltfLinesComponent>(pGltf, componentName);
pMesh = pLineMesh;
pCesiumPrimitive = pLineMesh;
} else if (!instanceTransforms.empty()) {
auto* pInstancedComponent =
NewObject<UCesiumGltfInstancedComponent>(pGltf, componentName);
Expand Down Expand Up @@ -3110,11 +3109,18 @@ static void loadPrimitiveGameThreadPart(
primData.pTilesetActor->GetTranslucencySortPriority();

pStaticMesh = NewObject<UStaticMesh>(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(
Expand Down
18 changes: 18 additions & 0 deletions Source/CesiumRuntime/Private/CesiumGltfLinesComponent.cpp
Original file line number Diff line number Diff line change
@@ -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());
}
22 changes: 22 additions & 0 deletions Source/CesiumRuntime/Private/CesiumGltfLinesComponent.h
Original file line number Diff line number Diff line change
@@ -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;
};
101 changes: 101 additions & 0 deletions Source/CesiumRuntime/Private/CesiumGltfLinesSceneProxy.cpp
Original file line number Diff line number Diff line change
@@ -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<size_t>(&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<const FSceneView*>& 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;
}
Loading
Loading