Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8b92701
[enhancement#1658] mesh, material and tile loading callbacks
GhisBntly May 8, 2025
6025fa9
Account for most of the feedback received to this day
GhisBntly May 13, 2025
51d7b21
Fix missing condition on GetLifecycleEventReceiver() pointer
GhisBntly May 19, 2025
e30313e
Reworked/simplified CreateMaterial/CustomizeMaterial
GhisBntly May 19, 2025
dd2bf54
revert adding CPU access option from Tilesets
GhisBntly May 28, 2025
e15b06a
Merge branch 'main' into enh-1658_mesh-build-callbacks
GhisBntly May 28, 2025
65efbd2
Remove CPU access option mention from CHANGES.md
GhisBntly May 28, 2025
40676e5
Merge remote-tracking branch 'cesium/main' into enh-1658_mesh-build-c…
GhisBntly Jun 11, 2025
5d357f5
AdvViz: add GetVertexPositionScaleFactor to UCesiumLoadedTile to make…
GhisBntly Jun 11, 2025
380fa13
CesiumMetadataValueAccess => FCesiumMetadataValueAccess
GhisBntly Jun 17, 2025
335d612
missing fwd decl
GhisBntly Jun 17, 2025
d58decc
Hide implementation detail
GhisBntly Jun 17, 2025
e4336ac
Revert addition of tile render-readiness concept
GhisBntly Jul 22, 2025
cb8cade
Merge remote-tracking branch 'origin/main' into enh-1658_mesh-build-c…
GhisBntly Jul 22, 2025
c33861f
Update CHANGES.md
GhisBntly Jul 22, 2025
8113a0a
Merge remote-tracking branch 'origin/main' into enh-1658_mesh-build-c…
GhisBntly Aug 14, 2025
f047b24
_lifecycleEventReceive => _pLifecycleEventReceive, make it visible to GC
GhisBntly Aug 14, 2025
1293e31
Methods are no longer pure virtual, which is more user friendly
GhisBntly Aug 14, 2025
e16a061
language
GhisBntly Aug 14, 2025
1adf464
Revert "CesiumMetadataValueAccess => FCesiumMetadataValueAccess"
GhisBntly Aug 14, 2025
f2fd3a0
Semantics and comments' rewrite by K.Ring
GhisBntly Aug 14, 2025
67f880c
require that CreateMaterial returns non-null
GhisBntly Aug 14, 2025
ceb0511
remove the need for GlTFmaterialPBR from CustomizeMaterial
GhisBntly Aug 14, 2025
17c87f8
bring back GetGltfModel, probably lost during a merge...
GhisBntly Aug 14, 2025
7c8b222
FindTexCoordIndexForGltfAttribute => FindTextureCoordinateIndexForGlt…
GhisBntly Aug 14, 2025
ebd52b1
doc comments in CesiumLoadedTile.h
GhisBntly Aug 14, 2025
2430526
Merge remote-tracking branch 'origin/main' into enh-1658_mesh-build-c…
kring Oct 16, 2025
42f31ec
Move changelog entry to correct version.
kring Oct 16, 2025
ca34f86
clang-format
kring Oct 16, 2025
7e19b17
Doc tweaks.
kring Oct 16, 2025
a08335e
Doc tweaks.
kring Oct 16, 2025
2daba76
clang-format.
kring Oct 16, 2025
f8293a5
More doc tweaks.
kring Oct 16, 2025
ff67aa4
Avoid auto.
kring Oct 16, 2025
c8b3006
Move implementation out of header.
kring Oct 16, 2025
cf29ac1
clang-format
kring Oct 16, 2025
4bb28fe
Only provide const pointers to tiles.
kring Oct 16, 2025
52bd20d
Minor code tweaks.
kring Oct 16, 2025
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Added a Cesium -> Geocoder -> Geocode Blueprint function, making it easy to query the Cesium ion geocoder.
- Added `UCesiumMetadataPickingBlueprintLibrary::FindPropertyTableProperty` to search for a `FCesiumPropertyTableProperty` by name on a given `UPrimitiveComponent`.
- Added a property `bAllowMeshBuffersCPUAccess` to `ACesium3DTileset` actors to keep the buffers of Unreal meshes created by the tileset in CPU memory, in order to have access to them in-game. Defaults to false.
- Added the class `CesiumMeshBuildCallbacks`: when an implementation is registered on a tileset (with `ACesium3DTileset::SetMeshBuildCallbacks`), its functions will be called at various points in a tile's lifecycle, like when a mesh component is created, when a material is instanced, when the tile changes visibility, when it is unloaded, etc.

##### Fixes :wrench:

Expand Down
12 changes: 12 additions & 0 deletions Source/CesiumRuntime/Private/Cesium3DTileset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,14 @@ void ACesium3DTileset::SetCesiumIonServer(UCesiumIonServer* Server) {
}
}

void ACesium3DTileset::SetAllowMeshBuffersCPUAccess(
bool InMeshBuffersCPUAccess) {
if (bAllowMeshBuffersCPUAccess != InMeshBuffersCPUAccess) {
this->bAllowMeshBuffersCPUAccess = InMeshBuffersCPUAccess;
this->DestroyTileset();
}
}

void ACesium3DTileset::SetMaximumScreenSpaceError(
double InMaximumScreenSpaceError) {
if (MaximumScreenSpaceError != InMaximumScreenSpaceError) {
Expand Down Expand Up @@ -2322,3 +2330,7 @@ void ACesium3DTileset::RuntimeSettingsChanged(
}
}
#endif

void ACesium3DTileset::SetMeshBuildCallbacks(const TWeakPtr<CesiumMeshBuildCallbacks>& Callbacks) {
this->_meshBuildCallbacks = Callbacks;
}
109 changes: 93 additions & 16 deletions Source/CesiumRuntime/Private/CesiumGltfComponent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "CesiumGltfPrimitiveComponent.h"
#include "CesiumGltfTextures.h"
#include "CesiumMaterialUserData.h"
#include "CesiumMeshBuildCallbacks.h"
#include "CesiumRasterOverlays.h"
#include "CesiumRuntime.h"
#include "CesiumTextureUtility.h"
Expand Down Expand Up @@ -1253,6 +1254,9 @@ static void loadPrimitive(
}
}

primitiveResult.MeshBuildCallbacks =
options.pMeshOptions->pNodeOptions->pModelOptions->MeshBuildCallbacks;

auto normalAccessorIt = primitive.attributes.find("NORMAL");
CesiumGltf::AccessorView<TMeshVector3> normalAccessor;
bool hasNormals = false;
Expand Down Expand Up @@ -1668,6 +1672,12 @@ static void loadPrimitive(
computeTangentSpace(StaticMeshBuildVertices);
}

// For iTwin scene mapping mechanism (used both for Synchro 4D schedules and
// selection highlight), we need to access vertex data from the CPU (in
// packaged game, if we don't set this flag, the data can become inaccessible
// at any time...)
const bool& bNeedsCPUAccess = options.pMeshOptions->pNodeOptions->pModelOptions->allowMeshBuffersCPUAccess;

{
TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::InitBuffers)

Expand All @@ -1679,12 +1689,12 @@ static void loadPrimitive(

LODResources.VertexBuffers.PositionVertexBuffer.Init(
StaticMeshBuildVertices,
false);
bNeedsCPUAccess);

FColorVertexBuffer& ColorVertexBuffer =
LODResources.VertexBuffers.ColorVertexBuffer;
if (hasVertexColors) {
ColorVertexBuffer.Init(StaticMeshBuildVertices, false);
ColorVertexBuffer.Init(StaticMeshBuildVertices, bNeedsCPUAccess);
}

uint32 numberOfTextureCoordinates =
Expand All @@ -1697,7 +1707,7 @@ static void loadPrimitive(
vertexBuffer.Init(
StaticMeshBuildVertices.Num(),
numberOfTextureCoordinates,
false);
bNeedsCPUAccess);

// Manually copy the vertices into the buffer. We do this because UE 5.3
// and 5.4 have a bug where the overload of `FStaticMeshVertexBuffer::Init`
Expand Down Expand Up @@ -1744,6 +1754,9 @@ static void loadPrimitive(

{
TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetIndices)
if (bNeedsCPUAccess) {
LODResources.IndexBuffer.TrySetAllowCPUAccess(true);
}
LODResources.IndexBuffer.SetIndices(
indices,
StaticMeshBuildVertices.Num() >= std::numeric_limits<uint16>::max()
Expand Down Expand Up @@ -2452,7 +2465,8 @@ static void SetGltfParameterValues(
const CesiumGltf::MaterialPBRMetallicRoughness& pbr,
UMaterialInstanceDynamic* pMaterial,
EMaterialParameterAssociation association,
int32 index) {
int32 index,
CesiumMeshBuildCallbacks const* meshBuildCallbacks) {
for (auto& textureCoordinateSet : loadResult.textureCoordinateParameters) {
pMaterial->SetScalarParameterValueByInfo(
FMaterialParameterInfo(
Expand Down Expand Up @@ -2671,6 +2685,11 @@ static void SetGltfParameterValues(
FMaterialParameterInfo("emissiveFactor", association, index),
FVector(1.0f, 1.0f, 1.0f));
}

// Extra material customizations
if (meshBuildCallbacks) {
meshBuildCallbacks->CustomizeGltfMaterial(material, pbr, pMaterial, association, index);
}
}

void SetWaterParameterValues(
Expand Down Expand Up @@ -3130,14 +3149,34 @@ static void loadPrimitiveGameThreadPart(
}
#endif

UMaterialInstanceDynamic* pMaterial;
// Move this right now: CreateMaterial may need them!
// "Safe" even though loadResult is still used later, because the methods used
// during material setup (SetGltfParameterValues, etc.) below do not use these
// members.
primData.Features = std::move(loadResult.Features);
primData.Metadata = std::move(loadResult.Metadata);

UMaterialInstanceDynamic* pMaterial = nullptr;
TSharedPtr<CesiumMeshBuildCallbacks> MeshBuildCallbacks =
loadResult.MeshBuildCallbacks.Pin();
{
TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetupMaterial)

pMaterial = UMaterialInstanceDynamic::Create(
pBaseMaterial,
nullptr,
ImportedSlotName);
ensure(pBaseMaterial);
if (MeshBuildCallbacks) {
// Possibility to override the material for this primitive
pMaterial = MeshBuildCallbacks->CreateMaterial(
*pCesiumPrimitive,
pBaseMaterial,
nullptr,
ImportedSlotName);
}
ensure(pBaseMaterial);
if (!pMaterial) {
pMaterial = UMaterialInstanceDynamic::Create(
pBaseMaterial,
nullptr,
ImportedSlotName);
}

pMaterial->SetFlags(
RF_Transient | RF_DuplicateTransient | RF_TextExportTransient);
Expand All @@ -3148,7 +3187,8 @@ static void loadPrimitiveGameThreadPart(
pbr,
pMaterial,
EMaterialParameterAssociation::GlobalParameter,
INDEX_NONE);
INDEX_NONE,
MeshBuildCallbacks.Get());
SetWaterParameterValues(
model,
loadResult,
Expand Down Expand Up @@ -3195,7 +3235,8 @@ static void loadPrimitiveGameThreadPart(
pbr,
pMaterial,
EMaterialParameterAssociation::LayerParameter,
0);
0,
MeshBuildCallbacks.Get());

// Initialize fade uniform to fully visible, in case LOD transitions
// are off.
Expand Down Expand Up @@ -3250,9 +3291,6 @@ static void loadPrimitiveGameThreadPart(
}
}

primData.Features = std::move(loadResult.Features);
primData.Metadata = std::move(loadResult.Metadata);

primData.EncodedFeatures = std::move(loadResult.EncodedFeatures);
primData.EncodedMetadata = std::move(loadResult.EncodedMetadata);

Expand Down Expand Up @@ -3328,6 +3366,11 @@ static void loadPrimitiveGameThreadPart(
TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::RegisterComponent)
pMesh->RegisterComponent();
}

// Call the observer callback (if any) once all is done
if (MeshBuildCallbacks) {
MeshBuildCallbacks->OnMeshConstructed(*pGltf, *pCesiumPrimitive);
}
}

/*static*/ CesiumAsync::Future<UCesiumGltfComponent::CreateOffGameThreadResult>
Expand All @@ -3352,7 +3395,7 @@ UCesiumGltfComponent::CreateOffGameThread(
UMaterialInterface* pBaseTranslucentMaterial,
UMaterialInterface* pBaseWaterMaterial,
FCustomDepthParameters CustomDepthParameters,
const Cesium3DTilesSelection::Tile& tile,
Cesium3DTilesSelection::Tile& tile,
bool createNavCollision) {
TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadModel)

Expand All @@ -3366,6 +3409,7 @@ UCesiumGltfComponent::CreateOffGameThread(
// }

UCesiumGltfComponent* Gltf = NewObject<UCesiumGltfComponent>(pTilesetActor);
Gltf->pTile = &tile;
Gltf->SetMobility(pTilesetActor->GetRootComponent()->Mobility);
Gltf->SetFlags(RF_Transient | RF_DuplicateTransient | RF_TextExportTransient);

Expand Down Expand Up @@ -3394,10 +3438,12 @@ UCesiumGltfComponent::CreateOffGameThread(
encodeMetadataGameThreadPart(*Gltf->EncodedMetadata_DEPRECATED);
}

LoadGltfResult::LoadedPrimitiveResult* pAnyPrimResult = nullptr;
for (LoadedNodeResult& node : pReal->loadModelResult.nodeResults) {
if (node.meshResult) {
for (LoadedPrimitiveResult& primitive :
node.meshResult->primitiveResults) {
pAnyPrimResult = &primitive;
loadPrimitiveGameThreadPart(
model,
Gltf,
Expand All @@ -3412,11 +3458,28 @@ UCesiumGltfComponent::CreateOffGameThread(
}
}

if (pAnyPrimResult && pAnyPrimResult->MeshBuildCallbacks.IsValid()) {
pAnyPrimResult->MeshBuildCallbacks.Pin()->OnTileConstructed(
tile.getTileID());
Gltf->VisibilityChangedObserver =
[MeshBuildCallbacks = pAnyPrimResult->MeshBuildCallbacks,
TileId = tile.getTileID()](bool visible) {
if (MeshBuildCallbacks.IsValid())
MeshBuildCallbacks.Pin()->OnVisibilityChanged(TileId, visible);
};
}

Gltf->SetVisibility(false, true);
Gltf->SetCollisionEnabled(ECollisionEnabled::NoCollision);
return Gltf;
}

void UCesiumGltfComponent::OnVisibilityChanged() {
USceneComponent::OnVisibilityChanged();
if (VisibilityChangedObserver)
VisibilityChangedObserver(GetVisibleFlag());
}

UCesiumGltfComponent::UCesiumGltfComponent() : USceneComponent() {
// Structure to hold one-time initialization
struct FConstructorStatics {
Expand Down Expand Up @@ -3447,6 +3510,20 @@ UCesiumGltfComponent::UCesiumGltfComponent() : USceneComponent() {
PrimaryComponentTick.bCanEverTick = false;
}

const FCesiumModelMetadata& UCesiumGltfComponent::GetModelMetadata() const {
return Metadata;
}

const Cesium3DTilesSelection::TileID& UCesiumGltfComponent::GetTileID() const {
return pTile->getTileID();
}

void UCesiumGltfComponent::SetRenderReady(bool bToggle) {
if (pTile) {
pTile->setRenderEngineReadiness(bToggle);
}
}

void UCesiumGltfComponent::UpdateTransformFromCesium(
const glm::dmat4& cesiumToUnrealTransform) {
for (USceneComponent* pSceneComponent : this->GetAttachChildren()) {
Expand Down
16 changes: 14 additions & 2 deletions Source/CesiumRuntime/Private/CesiumGltfComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
#include "Cesium3DTilesSelection/Tile.h"
#include "Cesium3DTileset.h"
#include "CesiumEncodedMetadataUtility.h"
#include "CesiumLoadedTile.h"
#include "CesiumModelMetadata.h"
#include "Components/PrimitiveComponent.h"
#include "Components/SceneComponent.h"
#include "CoreMinimal.h"
#include "CustomDepthParameters.h"
#include "EncodedFeaturesMetadata.h"
#include "Interfaces/IHttpRequest.h"
#include "Templates/Function.h"
#include <CesiumAsync/SharedFuture.h>
#include <glm/mat4x4.hpp>
#include <memory>
Expand Down Expand Up @@ -56,7 +58,7 @@ struct FRasterOverlayTile {
};

UCLASS()
class UCesiumGltfComponent : public USceneComponent {
class UCesiumGltfComponent : public USceneComponent, public ICesiumLoadedTile {
GENERATED_BODY()

public:
Expand Down Expand Up @@ -87,7 +89,7 @@ class UCesiumGltfComponent : public USceneComponent {
UMaterialInterface* BaseTranslucentMaterial,
UMaterialInterface* BaseWaterMaterial,
FCustomDepthParameters CustomDepthParameters,
const Cesium3DTilesSelection::Tile& tile,
Cesium3DTilesSelection::Tile& tile,
bool createNavCollision);

UCesiumGltfComponent();
Expand All @@ -104,6 +106,8 @@ class UCesiumGltfComponent : public USceneComponent {
UPROPERTY(EditAnywhere, Category = "Rendering")
FCustomDepthParameters CustomDepthParameters{};

Cesium3DTilesSelection::Tile* pTile = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know why you did this, but I'm hesitant to give this component the power to modify the Native tile. I'm not sure if this could affect the tileset loading algorithm in some unexpected way... maybe @kring would know off the top of his head?

Can you explain when you would use SetRenderReady for your use case? I'm hoping there's a better or equal way to achieve this without reaching into the Native tile state 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm generally ok with the Tile being accessible, so long as it's only accessible from the main thread. The public API of Tile could definitely be better, to make it clear what sort of modifications are ok, but as an advanced API here I think it's fine for now.

I would like to understand better why the "render ready" concept is used, and why it's needed, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah adding this member made me uncomfortable too ;-) But I didn't see any other way, and at least UCesiumGltfComponent is Private. The Public interface I added only gives access to the Tile ID and to the setter I needed (SetRenderReady).

Now for the use case: it will be complex to explain, as we reached this solution after much trial and error...
Our app logic means we need to create textures for some of the mesh components. Initially, we were doing everything in OnMeshConstructed:

  1. discover per-vertex metadata to identify mesh subsets and map them to app domain data
  2. in tiles where it was needed, create property textures (with a size matching the max FeatureID found on vertices), initialized (UTexture2D::UpdateTextureRegions) with app-specific data used by our custom material's shaders
  3. assign (SetTextureParameterValueByInfo) these textures to all materials of the tile's meshes

But we realized that, randomly, some textures were not coming through properly, ie. the render was showing as if the material was accessing garbage data, even though we had triple-checked that all data was correct and the right methods called on our side. We finally found that we had to wait for the render thread to process the render command enqueued by the UpdateTextureRegions call to guarantee that the texture could be assigned to the materials :-/

So we added a synchronization mechanism through UpdateTextureRegions's DataCleanupFunc parameter: to avoid blocking the game thread while waiting for the RHI thread, we had to defer the finalization of our materials setup to the next tick which in turn meant we had to tell the tiles selection algorithm that the tile was not quite ready for render yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation @GhisBntly! I'm still a little confused as to why it's necessary to delay, though.

UpdateTextureRegions enqueues a command to the renderer thread, that makes sense. But as long as any draw commands for that tile are executed by the render thread after that command enqueued by UpdateTextureRegions, I don't think any particular synchronization or delay should be necessary. Certainly this is true when we create textures, at least.

Is it possible something else was happening here? Like the UpdateTextureRegions wasn't invoked until after the tile was drawn with the non-updated texture somehow?

Or is there more going on in UpdateTextureRegions than I expect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree our problem and solution doesn't feel "right" ... but we have spent a fair amount of time debugging and didn't find anything so far that could be a bug in our code. Our assumption is that assigning the texture with SetTextureParameterValueByInfo has immediate side effects on render commands already enqueued :-/

It was our early days with Unreal last year so maybe we missed something. If you think this is too intrusive a change to merge without a better understanding of what's happening, I can remove that part from the PR, and we'll open a new one with it later if need be, after we have had another look at it with the experience gained.


FCesiumModelMetadata Metadata{};
EncodedFeaturesMetadata::EncodedModelMetadata EncodedMetadata{};

Expand Down Expand Up @@ -131,10 +135,18 @@ class UCesiumGltfComponent : public USceneComponent {
virtual void SetCollisionEnabled(ECollisionEnabled::Type NewType);

virtual void BeginDestroy() override;
virtual void OnVisibilityChanged() override;

// from ICesiumLoadedTile
const FCesiumModelMetadata& GetModelMetadata() const override;
const Cesium3DTilesSelection::TileID& GetTileID() const override;
void SetRenderReady(bool bToggle) override;

void UpdateFade(float fadePercentage, bool fadingIn);

private:
UPROPERTY()
UTexture2D* Transparent1x1 = nullptr;

TFunction<void(bool /*visible*/)> VisibilityChangedObserver;
};
Loading