// Copyright 2020-2024 CesiumGS, Inc. and Contributors

#include "Cesium3DTileset.h"
#include "Async/Async.h"
#include "Camera/CameraTypes.h"
#include "Camera/PlayerCameraManager.h"
#include "Cesium3DTilesSelection/EllipsoidTilesetLoader.h"
#include "Cesium3DTilesSelection/Tile.h"
#include "Cesium3DTilesSelection/TilesetLoadFailureDetails.h"
#include "Cesium3DTilesSelection/TilesetOptions.h"
#include "Cesium3DTilesSelection/TilesetSharedAssetSystem.h"
#include "Cesium3DTilesetLoadFailureDetails.h"
#include "Cesium3DTilesetRoot.h"
#include "CesiumActors.h"
#include "CesiumAsync/SharedAssetDepot.h"
#include "CesiumBoundingVolumeComponent.h"
#include "CesiumCamera.h"
#include "CesiumCameraManager.h"
#include "CesiumCommon.h"
#include "CesiumCustomVersion.h"
#include "CesiumGeospatial/GlobeTransforms.h"
#include "CesiumGltf/ImageAsset.h"
#include "CesiumGltf/Ktx2TranscodeTargets.h"
#include "CesiumGltfComponent.h"
#include "CesiumGltfPointsSceneProxyUpdater.h"
#include "CesiumGltfPrimitiveComponent.h"
#include "CesiumIonClient/Connection.h"
#include "CesiumRasterOverlay.h"
#include "CesiumRuntime.h"
#include "CesiumRuntimeSettings.h"
#include "CesiumTileExcluder.h"
#include "CesiumViewExtension.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/Engine.h"
#include "Engine/LocalPlayer.h"
#include "Engine/SceneCapture2D.h"
#include "Engine/Texture.h"
#include "Engine/Texture2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/World.h"
#include "EngineUtils.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "LevelSequenceActor.h"
#include "LevelSequencePlayer.h"
#include "Math/UnrealMathUtility.h"
#include "PixelFormat.h"
#include "StereoRendering.h"
#include "UnrealPrepareRendererResources.h"
#include "VecMath.h"
#include <glm/gtc/matrix_inverse.hpp>
#include <memory>
#include <spdlog/spdlog.h>

#ifdef CESIUM_DEBUG_TILE_STATES
#include "HAL/PlatformFileManager.h"
#include <Cesium3DTilesSelection/DebugTileStateDatabase.h>
#endif

FCesium3DTilesetLoadFailure OnCesium3DTilesetLoadFailure{};

#if WITH_EDITOR
#include "Editor.h"
#include "EditorViewportClient.h"
#include "FileHelpers.h"
#include "LevelEditorViewport.h"
#endif

// Avoid complaining about the deprecated metadata struct
PRAGMA_DISABLE_DEPRECATION_WARNINGS

// Sets default values
ACesium3DTileset::ACesium3DTileset()
    : AActor(),
      Georeference(nullptr),
      ResolvedGeoreference(nullptr),
      CreditSystem(nullptr),

      _pTileset(nullptr),

#ifdef CESIUM_DEBUG_TILE_STATES
      _pStateDebug(nullptr),
#endif

      _lastTilesRendered(0),
      _lastWorkerThreadTileLoadQueueLength(0),
      _lastMainThreadTileLoadQueueLength(0),

      _lastTilesVisited(0),
      _lastTilesCulled(0),
      _lastTilesOccluded(0),
      _lastTilesWaitingForOcclusionResults(0),
      _lastMaxDepthVisited(0),

      _captureMovieMode{false},
      _beforeMoviePreloadAncestors{PreloadAncestors},
      _beforeMoviePreloadSiblings{PreloadSiblings},
      _beforeMovieLoadingDescendantLimit{LoadingDescendantLimit},
      _beforeMovieUseLodTransitions{true},

      _tilesetsBeingDestroyed(0) {
  PrimaryActorTick.bCanEverTick = true;
  PrimaryActorTick.TickGroup = ETickingGroup::TG_PostUpdateWork;

#if WITH_EDITOR
  this->SetIsSpatiallyLoaded(false);
#endif

  this->SetActorEnableCollision(true);

  this->RootComponent =
      CreateDefaultSubobject<UCesium3DTilesetRoot>(TEXT("Tileset"));
  this->Root = this->RootComponent;

  PlatformName = UGameplayStatics::GetPlatformName();
}

ACesium3DTileset::~ACesium3DTileset() { this->DestroyTileset(); }
PRAGMA_ENABLE_DEPRECATION_WARNINGS

TSoftObjectPtr<ACesiumGeoreference> ACesium3DTileset::GetGeoreference() const {
  return this->Georeference;
}

void ACesium3DTileset::SetMobility(EComponentMobility::Type NewMobility) {
  if (NewMobility != this->RootComponent->Mobility) {
    this->RootComponent->SetMobility(NewMobility);
    DestroyTileset();
  }
}

void ACesium3DTileset::SampleHeightMostDetailed(
    const TArray<FVector>& LongitudeLatitudeHeightArray,
    FCesiumSampleHeightMostDetailedCallback OnHeightsSampled) {
  // It's possible to call this function before a Tick happens, so make sure
  // that the necessary variables are resolved.
  this->ResolveGeoreference();
  this->ResolveCameraManager();
  this->ResolveCreditSystem();

  if (this->_pTileset == nullptr) {
    this->LoadTileset();
  }

  std::vector<CesiumGeospatial::Cartographic> positions;
  positions.reserve(LongitudeLatitudeHeightArray.Num());

  for (const FVector& position : LongitudeLatitudeHeightArray) {
    positions.emplace_back(CesiumGeospatial::Cartographic::fromDegrees(
        position.X,
        position.Y,
        position.Z));
  }

  auto sampleHeights = [this, &positions]() mutable {
    if (this->_pTileset) {
      return this->_pTileset->sampleHeightMostDetailed(positions)
          .catchImmediately([positions = std::move(positions)](
                                std::exception&& exception) mutable {
            std::vector<bool> sampleSuccess(positions.size(), false);
            return Cesium3DTilesSelection::SampleHeightResult{
                std::move(positions),
                std::move(sampleSuccess),
                {exception.what()}};
          });
    } else {
      std::vector<bool> sampleSuccess(positions.size(), false);
      return getAsyncSystem().createResolvedFuture(
          Cesium3DTilesSelection::SampleHeightResult{
              std::move(positions),
              std::move(sampleSuccess),
              {"Could not sample heights from tileset because it has not "
               "been created."}});
    }
  };

  sampleHeights().thenImmediately(
      [this, OnHeightsSampled = std::move(OnHeightsSampled)](
          Cesium3DTilesSelection::SampleHeightResult&& result) {
        if (!IsValid(this))
          return;

        check(result.positions.size() == result.sampleSuccess.size());

        // This should do nothing, but will prevent undefined behavior if
        // the array sizes are unexpectedly different.
        result.sampleSuccess.resize(result.positions.size(), false);

        TArray<FCesiumSampleHeightResult> sampleHeightResults;
        sampleHeightResults.Reserve(result.positions.size());

        for (size_t i = 0; i < result.positions.size(); ++i) {
          const CesiumGeospatial::Cartographic& position = result.positions[i];

          FCesiumSampleHeightResult unrealResult;
          unrealResult.LongitudeLatitudeHeight = FVector(
              CesiumUtility::Math::radiansToDegrees(position.longitude),
              CesiumUtility::Math::radiansToDegrees(position.latitude),
              position.height);
          unrealResult.SampleSuccess = result.sampleSuccess[i];

          sampleHeightResults.Emplace(std::move(unrealResult));
        }

        TArray<FString> warnings;
        warnings.Reserve(result.warnings.size());

        for (const std::string& warning : result.warnings) {
          warnings.Emplace(UTF8_TO_TCHAR(warning.c_str()));
        }

        OnHeightsSampled.ExecuteIfBound(this, sampleHeightResults, warnings);
      });
}

void ACesium3DTileset::SetGeoreference(
    TSoftObjectPtr<ACesiumGeoreference> NewGeoreference) {
  this->Georeference = NewGeoreference;
  this->InvalidateResolvedGeoreference();
  this->ResolveGeoreference();
}

ACesiumGeoreference* ACesium3DTileset::ResolveGeoreference() {
  if (IsValid(this->ResolvedGeoreference)) {
    return this->ResolvedGeoreference;
  }

  if (IsValid(this->Georeference.Get())) {
    this->ResolvedGeoreference = this->Georeference.Get();
  } else {
    this->ResolvedGeoreference =
        ACesiumGeoreference::GetDefaultGeoreferenceForActor(this);
  }

  UCesium3DTilesetRoot* pRoot = Cast<UCesium3DTilesetRoot>(this->RootComponent);
  if (pRoot) {
    this->ResolvedGeoreference->OnGeoreferenceUpdated.AddUniqueDynamic(
        pRoot,
        &UCesium3DTilesetRoot::HandleGeoreferenceUpdated);
    this->ResolvedGeoreference->OnEllipsoidChanged.AddUniqueDynamic(
        this,
        &ACesium3DTileset::HandleOnGeoreferenceEllipsoidChanged);

    // Update existing tile positions, if any.
    pRoot->HandleGeoreferenceUpdated();
  }

  return this->ResolvedGeoreference;
}

void ACesium3DTileset::InvalidateResolvedGeoreference() {
  if (IsValid(this->ResolvedGeoreference)) {
    this->ResolvedGeoreference->OnGeoreferenceUpdated.RemoveAll(
        this->RootComponent);
  }
  this->ResolvedGeoreference = nullptr;
}

TSoftObjectPtr<ACesiumCreditSystem> ACesium3DTileset::GetCreditSystem() const {
  return this->CreditSystem;
}

void ACesium3DTileset::SetCreditSystem(
    TSoftObjectPtr<ACesiumCreditSystem> NewCreditSystem) {
  this->CreditSystem = NewCreditSystem;
  this->InvalidateResolvedCreditSystem();
  this->ResolveCreditSystem();
}

ACesiumCreditSystem* ACesium3DTileset::ResolveCreditSystem() {
  if (IsValid(this->ResolvedCreditSystem)) {
    return this->ResolvedCreditSystem;
  }

  if (IsValid(this->CreditSystem.Get())) {
    this->ResolvedCreditSystem = this->CreditSystem.Get();
  } else {
    this->ResolvedCreditSystem =
        ACesiumCreditSystem::GetDefaultCreditSystem(this);
  }

  // Refresh the tileset so it uses the new credit system.
  this->RefreshTileset();

  return this->ResolvedCreditSystem;
}

void ACesium3DTileset::InvalidateResolvedCreditSystem() {
  this->ResolvedCreditSystem = nullptr;
  this->RefreshTileset();
}

TSoftObjectPtr<ACesiumCameraManager>
ACesium3DTileset::GetCameraManager() const {
  return this->CameraManager;
}

void ACesium3DTileset::SetCameraManager(
    TSoftObjectPtr<ACesiumCameraManager> NewCameraManager) {
  this->CameraManager = NewCameraManager;
  this->InvalidateResolvedCameraManager();
  this->ResolveCameraManager();
}

ACesiumCameraManager* ACesium3DTileset::ResolveCameraManager() {
  if (IsValid(this->ResolvedCameraManager)) {
    return this->ResolvedCameraManager;
  }

  if (IsValid(this->CameraManager.Get())) {
    this->ResolvedCameraManager = this->CameraManager.Get();
  } else {
    this->ResolvedCameraManager =
        ACesiumCameraManager::GetDefaultCameraManager(this);
  }

  return this->ResolvedCameraManager;
}

void ACesium3DTileset::InvalidateResolvedCameraManager() {
  this->ResolvedCameraManager = nullptr;
  this->RefreshTileset();
}

void ACesium3DTileset::RefreshTileset() { this->DestroyTileset(); }

void ACesium3DTileset::TroubleshootToken() {
  OnCesium3DTilesetIonTroubleshooting.Broadcast(this);
}

void ACesium3DTileset::AddFocusViewportDelegate() {
#if WITH_EDITOR
  FEditorDelegates::OnFocusViewportOnActors.AddLambda(
      [this](const TArray<AActor*>& actors) {
        if (actors.Num() == 1 && actors[0] == this) {
          this->OnFocusEditorViewportOnThis();
        }
      });
#endif // WITH_EDITOR
}

void ACesium3DTileset::PostInitProperties() {
  UE_LOG(
      LogCesium,
      Verbose,
      TEXT("Called PostInitProperties on actor %s"),
      *this->GetName());

  Super::PostInitProperties();

  AddFocusViewportDelegate();

  UCesiumRuntimeSettings* pSettings =
      GetMutableDefault<UCesiumRuntimeSettings>();
  if (pSettings) {
    CanEnableOcclusionCulling =
        pSettings->EnableExperimentalOcclusionCullingFeature;
#if WITH_EDITOR
    pSettings->OnSettingChanged().AddUObject(
        this,
        &ACesium3DTileset::RuntimeSettingsChanged);
#endif
  }
}

void ACesium3DTileset::SetUseLodTransitions(bool InUseLodTransitions) {
  if (InUseLodTransitions != this->UseLodTransitions) {
    this->UseLodTransitions = InUseLodTransitions;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetTilesetSource(ETilesetSource InSource) {
  if (InSource != this->TilesetSource) {
    this->DestroyTileset();
    this->TilesetSource = InSource;
  }
}

namespace {

bool MapsAreEqual(
    const TMap<FString, FString>& Lhs,
    const TMap<FString, FString>& Rhs) {
  if (Lhs.Num() != Rhs.Num()) {
    return false;
  }

  for (const auto& [Key, Value] : Lhs) {
    const FString* RhsVal = Rhs.Find(Key);
    if (!RhsVal || *RhsVal != Value) {
      return false;
    }
  }

  return true;
}

} // namespace

void ACesium3DTileset::SetRequestHeaders(
    const TMap<FString, FString>& InRequestHeaders) {
  if (!MapsAreEqual(InRequestHeaders, this->RequestHeaders)) {
    this->DestroyTileset();
    this->RequestHeaders = InRequestHeaders;
  }
}

void ACesium3DTileset::SetUrl(const FString& InUrl) {
  if (InUrl != this->Url) {
    if (this->TilesetSource == ETilesetSource::FromUrl) {
      this->DestroyTileset();
    }
    this->Url = InUrl;
  }
}

void ACesium3DTileset::SetIonAssetID(int64 InAssetID) {
  if (InAssetID >= 0 && InAssetID != this->IonAssetID) {
    if (this->TilesetSource == ETilesetSource::FromCesiumIon) {
      this->DestroyTileset();
    }
    this->IonAssetID = InAssetID;
  }
}

void ACesium3DTileset::SetIonAccessToken(const FString& InAccessToken) {
  if (this->IonAccessToken != InAccessToken) {
    if (this->TilesetSource == ETilesetSource::FromCesiumIon) {
      this->DestroyTileset();
    }
    this->IonAccessToken = InAccessToken;
  }
}

void ACesium3DTileset::SetCesiumIonServer(UCesiumIonServer* Server) {
  if (this->CesiumIonServer != Server) {
    if (this->TilesetSource == ETilesetSource::FromCesiumIon) {
      this->DestroyTileset();
    }
    this->CesiumIonServer = Server;
  }
}

void ACesium3DTileset::SetMaximumScreenSpaceError(
    double InMaximumScreenSpaceError) {
  if (MaximumScreenSpaceError != InMaximumScreenSpaceError) {
    MaximumScreenSpaceError = InMaximumScreenSpaceError;
    FCesiumGltfPointsSceneProxyUpdater::UpdateSettingsInProxies(this);
  }
}

bool ACesium3DTileset::GetEnableOcclusionCulling() const {
  return GetDefault<UCesiumRuntimeSettings>()
             ->EnableExperimentalOcclusionCullingFeature &&
         EnableOcclusionCulling;
}

void ACesium3DTileset::SetEnableOcclusionCulling(bool bEnableOcclusionCulling) {
  if (this->EnableOcclusionCulling != bEnableOcclusionCulling) {
    this->EnableOcclusionCulling = bEnableOcclusionCulling;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetOcclusionPoolSize(int32 newOcclusionPoolSize) {
  if (this->OcclusionPoolSize != newOcclusionPoolSize) {
    this->OcclusionPoolSize = newOcclusionPoolSize;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetDelayRefinementForOcclusion(
    bool bDelayRefinementForOcclusion) {
  if (this->DelayRefinementForOcclusion != bDelayRefinementForOcclusion) {
    this->DelayRefinementForOcclusion = bDelayRefinementForOcclusion;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetCreatePhysicsMeshes(bool bCreatePhysicsMeshes) {
  if (this->CreatePhysicsMeshes != bCreatePhysicsMeshes) {
    this->CreatePhysicsMeshes = bCreatePhysicsMeshes;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetCreateNavCollision(bool bCreateNavCollision) {
  if (this->CreateNavCollision != bCreateNavCollision) {
    this->CreateNavCollision = bCreateNavCollision;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetAlwaysIncludeTangents(bool bAlwaysIncludeTangents) {
  if (this->AlwaysIncludeTangents != bAlwaysIncludeTangents) {
    this->AlwaysIncludeTangents = bAlwaysIncludeTangents;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetGenerateSmoothNormals(bool bGenerateSmoothNormals) {
  if (this->GenerateSmoothNormals != bGenerateSmoothNormals) {
    this->GenerateSmoothNormals = bGenerateSmoothNormals;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetEnableWaterMask(bool bEnableMask) {
  if (this->EnableWaterMask != bEnableMask) {
    this->EnableWaterMask = bEnableMask;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetIgnoreKhrMaterialsUnlit(
    bool bIgnoreKhrMaterialsUnlit) {
  if (this->IgnoreKhrMaterialsUnlit != bIgnoreKhrMaterialsUnlit) {
    this->IgnoreKhrMaterialsUnlit = bIgnoreKhrMaterialsUnlit;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetMaterial(UMaterialInterface* InMaterial) {
  if (this->Material != InMaterial) {
    this->Material = InMaterial;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetTranslucentMaterial(UMaterialInterface* InMaterial) {
  if (this->TranslucentMaterial != InMaterial) {
    this->TranslucentMaterial = InMaterial;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetWaterMaterial(UMaterialInterface* InMaterial) {
  if (this->WaterMaterial != InMaterial) {
    this->WaterMaterial = InMaterial;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetCustomDepthParameters(
    FCustomDepthParameters InCustomDepthParameters) {
  if (this->CustomDepthParameters != InCustomDepthParameters) {
    this->CustomDepthParameters = InCustomDepthParameters;
    this->DestroyTileset();
  }
}

void ACesium3DTileset::SetPointCloudShading(
    FCesiumPointCloudShading InPointCloudShading) {
  if (PointCloudShading != InPointCloudShading) {
    PointCloudShading = InPointCloudShading;
    FCesiumGltfPointsSceneProxyUpdater::UpdateSettingsInProxies(this);
  }
}

void ACesium3DTileset::PlayMovieSequencer() {
  this->_beforeMoviePreloadAncestors = this->PreloadAncestors;
  this->_beforeMoviePreloadSiblings = this->PreloadSiblings;
  this->_beforeMovieLoadingDescendantLimit = this->LoadingDescendantLimit;
  this->_beforeMovieUseLodTransitions = this->UseLodTransitions;

  this->_captureMovieMode = true;
  this->PreloadAncestors = false;
  this->PreloadSiblings = false;
  this->LoadingDescendantLimit = 10000;
  this->UseLodTransitions = false;
}

void ACesium3DTileset::StopMovieSequencer() {
  this->_captureMovieMode = false;
  this->PreloadAncestors = this->_beforeMoviePreloadAncestors;
  this->PreloadSiblings = this->_beforeMoviePreloadSiblings;
  this->LoadingDescendantLimit = this->_beforeMovieLoadingDescendantLimit;
  this->UseLodTransitions = this->_beforeMovieUseLodTransitions;
}

void ACesium3DTileset::PauseMovieSequencer() { this->StopMovieSequencer(); }

#if WITH_EDITOR
void ACesium3DTileset::OnFocusEditorViewportOnThis() {
  UE_LOG(
      LogCesium,
      Verbose,
      TEXT("Called OnFocusEditorViewportOnThis on actor %s"),
      *this->GetName());

  struct CalculateECEFCameraPosition {
    const CesiumGeospatial::Ellipsoid& ellipsoid;

    glm::dvec3 operator()(const CesiumGeometry::BoundingSphere& sphere) {
      const glm::dvec3& center = sphere.getCenter();
      glm::dmat4 ENU =
          CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(
              center,
              ellipsoid);
      glm::dvec3 offset =
          sphere.getRadius() *
          glm::normalize(
              glm::dvec3(ENU[0]) + glm::dvec3(ENU[1]) + glm::dvec3(ENU[2]));
      glm::dvec3 position = center + offset;
      return position;
    }

    glm::dvec3
    operator()(const CesiumGeometry::OrientedBoundingBox& orientedBoundingBox) {
      const glm::dvec3& center = orientedBoundingBox.getCenter();
      glm::dmat4 ENU =
          CesiumGeospatial::GlobeTransforms::eastNorthUpToFixedFrame(
              center,
              ellipsoid);
      const glm::dmat3& halfAxes = orientedBoundingBox.getHalfAxes();
      glm::dvec3 offset =
          glm::length(halfAxes[0] + halfAxes[1] + halfAxes[2]) *
          glm::normalize(
              glm::dvec3(ENU[0]) + glm::dvec3(ENU[1]) + glm::dvec3(ENU[2]));
      glm::dvec3 position = center + offset;
      return position;
    }

    glm::dvec3
    operator()(const CesiumGeospatial::BoundingRegion& boundingRegion) {
      return (*this)(boundingRegion.getBoundingBox());
    }

    glm::dvec3
    operator()(const CesiumGeospatial::BoundingRegionWithLooseFittingHeights&
                   boundingRegionWithLooseFittingHeights) {
      return (*this)(boundingRegionWithLooseFittingHeights.getBoundingRegion()
                         .getBoundingBox());
    }

    glm::dvec3 operator()(const CesiumGeospatial::S2CellBoundingVolume& s2) {
      return (*this)(s2.computeBoundingRegion());
    }
  };

  const Cesium3DTilesSelection::Tile* pRootTile =
      this->_pTileset->getRootTile();
  if (!pRootTile) {
    return;
  }

  const Cesium3DTilesSelection::BoundingVolume& boundingVolume =
      pRootTile->getBoundingVolume();

  ACesiumGeoreference* pGeoreference = this->ResolveGeoreference();

  const CesiumGeospatial::Ellipsoid& ellipsoid =
      pGeoreference->GetEllipsoid()->GetNativeEllipsoid();

  // calculate unreal camera position
  glm::dvec3 ecefCameraPosition =
      std::visit(CalculateECEFCameraPosition{ellipsoid}, boundingVolume);
  FVector unrealCameraPosition =
      pGeoreference->TransformEarthCenteredEarthFixedPositionToUnreal(
          VecMath::createVector(ecefCameraPosition));

  // calculate unreal camera orientation
  glm::dvec3 ecefCenter =
      Cesium3DTilesSelection::getBoundingVolumeCenter(boundingVolume);
  FVector unrealCenter =
      pGeoreference->TransformEarthCenteredEarthFixedPositionToUnreal(
          VecMath::createVector(ecefCenter));
  FVector unrealCameraFront =
      (unrealCenter - unrealCameraPosition).GetSafeNormal();
  FVector unrealCameraRight =
      FVector::CrossProduct(FVector::ZAxisVector, unrealCameraFront)
          .GetSafeNormal();
  FVector unrealCameraUp =
      FVector::CrossProduct(unrealCameraFront, unrealCameraRight)
          .GetSafeNormal();
  FRotator cameraRotator = FMatrix(
                               unrealCameraFront,
                               unrealCameraRight,
                               unrealCameraUp,
                               FVector::ZeroVector)
                               .Rotator();

  // Update all viewports.
  for (FLevelEditorViewportClient* LinkedViewportClient :
       GEditor->GetLevelViewportClients()) {
    // Dont move camera attach to an actor
    if (!LinkedViewportClient->IsAnyActorLocked()) {
      FViewportCameraTransform& ViewTransform =
          LinkedViewportClient->GetViewTransform();
      LinkedViewportClient->SetViewRotation(cameraRotator);
      LinkedViewportClient->SetViewLocation(unrealCameraPosition);
      LinkedViewportClient->Invalidate();
    }
  }
}
#endif

const glm::dmat4&
ACesium3DTileset::GetCesiumTilesetToUnrealRelativeWorldTransform() const {
  return Cast<UCesium3DTilesetRoot>(this->RootComponent)
      ->GetCesiumTilesetToUnrealRelativeWorldTransform();
}

void ACesium3DTileset::UpdateTransformFromCesium() {
  const glm::dmat4& CesiumToUnreal =
      this->GetCesiumTilesetToUnrealRelativeWorldTransform();
  TArray<UCesiumGltfComponent*> gltfComponents;
  this->GetComponents<UCesiumGltfComponent>(gltfComponents);

  for (UCesiumGltfComponent* pGltf : gltfComponents) {
    pGltf->UpdateTransformFromCesium(CesiumToUnreal);
  }

  if (this->BoundingVolumePoolComponent) {
    this->BoundingVolumePoolComponent->UpdateTransformFromCesium(
        CesiumToUnreal);
  }
}

void ACesium3DTileset::HandleOnGeoreferenceEllipsoidChanged(
    UCesiumEllipsoid* OldEllipsoid,
    UCesiumEllipsoid* NewEllpisoid) {
  UE_LOG(LogCesium, Warning, TEXT("Ellipsoid changed"));
  this->RefreshTileset();
}

// Called when the game starts or when spawned
void ACesium3DTileset::BeginPlay() {
  Super::BeginPlay();

  this->ResolveGeoreference();
  this->ResolveCameraManager();
  this->ResolveCreditSystem();

  this->LoadTileset();

  // Search for level sequence.
  for (auto sequenceActorIt = TActorIterator<ALevelSequenceActor>(GetWorld());
       sequenceActorIt;
       ++sequenceActorIt) {
    ALevelSequenceActor* sequenceActor = *sequenceActorIt;

    if (!IsValid(sequenceActor->GetSequencePlayer())) {
      continue;
    }

    FScriptDelegate playMovieSequencerDelegate;
    playMovieSequencerDelegate.BindUFunction(this, FName("PlayMovieSequencer"));
    sequenceActor->GetSequencePlayer()->OnPlay.Add(playMovieSequencerDelegate);

    FScriptDelegate stopMovieSequencerDelegate;
    stopMovieSequencerDelegate.BindUFunction(this, FName("StopMovieSequencer"));
    sequenceActor->GetSequencePlayer()->OnStop.Add(stopMovieSequencerDelegate);

    FScriptDelegate pauseMovieSequencerDelegate;
    pauseMovieSequencerDelegate.BindUFunction(
        this,
        FName("PauseMovieSequencer"));
    sequenceActor->GetSequencePlayer()->OnPause.Add(
        pauseMovieSequencerDelegate);
  }
}

void ACesium3DTileset::OnConstruction(const FTransform& Transform) {
  this->ResolveGeoreference();
  this->ResolveCameraManager();
  this->ResolveCreditSystem();

  this->LoadTileset();

  // Hide all existing tiles. The still-visible ones will be shown next time we
  // tick. But if update is suspended, leave the components in their current
  // state.
  if (!this->SuspendUpdate) {
    TArray<UCesiumGltfComponent*> gltfComponents;
    this->GetComponents<UCesiumGltfComponent>(gltfComponents);

    for (UCesiumGltfComponent* pGltf : gltfComponents) {
      if (pGltf && IsValid(pGltf) && pGltf->IsVisible()) {
        pGltf->SetVisibility(false, true);
        pGltf->SetCollisionEnabled(ECollisionEnabled::NoCollision);
      }
    }
  }
}

void ACesium3DTileset::NotifyHit(
    UPrimitiveComponent* MyComp,
    AActor* Other,
    UPrimitiveComponent* OtherComp,
    bool bSelfMoved,
    FVector HitLocation,
    FVector HitNormal,
    FVector NormalImpulse,
    const FHitResult& Hit) {
  // std::cout << "Hit face index: " << Hit.FaceIndex << std::endl;

  // FHitResult detailedHit;
  // FCollisionQueryParams params;
  // params.bReturnFaceIndex = true;
  // params.bTraceComplex = true;
  // MyComp->LineTraceComponent(detailedHit, Hit.TraceStart, Hit.TraceEnd,
  // params);

  // std::cout << "Hit face index 2: " << detailedHit.FaceIndex << std::endl;
}

void ACesium3DTileset::UpdateLoadStatus() {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::UpdateLoadStatus)

  float nativeLoadProgress = this->_pTileset->computeLoadProgress();

  // If native tileset still loading, just copy its progress
  if (nativeLoadProgress < 100) {
    this->LoadProgress = nativeLoadProgress;
    return;
  }

  // Native tileset is 100% loaded, but there might be a few frames where
  // nothing needs to be loaded as we are waiting for occlusion results to come
  // back, which means we are not done with loading all the tiles in the tileset
  // yet. Interpret this as 99% (almost) done
  if (this->_lastTilesWaitingForOcclusionResults > 0) {
    this->LoadProgress = 99;
    return;
  }

  // If we have tiles to hide next frame, we haven't completely finished loading
  // yet. We need to tick once more. We're really close to done.
  if (!this->_tilesToHideNextFrame.empty()) {
    this->LoadProgress = glm::min(this->LoadProgress, 99.9999f);
    return;
  }

  // We can now report 100 percent loaded
  float lastLoadProgress = this->LoadProgress;
  this->LoadProgress = 100;

  // Only broadcast the update when we first hit 100%, not everytime
  if (lastLoadProgress != LoadProgress) {
    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::BroadcastOnTilesetLoaded)

    // Tileset just finished loading, we broadcast the update
    UE_LOG(LogCesium, Verbose, TEXT("Broadcasting OnTileLoaded"));
    OnTilesetLoaded.Broadcast();
  }
}

namespace {
const TSharedRef<CesiumViewExtension, ESPMode::ThreadSafe>&
getCesiumViewExtension() {
  static TSharedRef<CesiumViewExtension, ESPMode::ThreadSafe>
      cesiumViewExtension =
          GEngine->ViewExtensions->NewExtension<CesiumViewExtension>();
  return cesiumViewExtension;
}
} // namespace

void ACesium3DTileset::LoadTileset() {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTileset)

  if (this->_pTileset) {
    // Tileset already loaded, do nothing.
    return;
  }

  UWorld* pWorld = this->GetWorld();
  if (!pWorld) {
    return;
  }

  AWorldSettings* pWorldSettings = pWorld->GetWorldSettings();
  if (pWorldSettings && pWorldSettings->bEnableWorldBoundsChecks) {
    UE_LOG(
        LogCesium,
        Warning,
        TEXT(
            "\"Enable World Bounds Checks\" in the world settings is currently enabled. Please consider disabling it to avoid potential issues."),
        *this->Url);
  }

  // Make sure we have a valid Cesium ion server if we need one.
  if (this->TilesetSource == ETilesetSource::FromCesiumIon &&
      !IsValid(this->CesiumIonServer)) {
    this->Modify();
    this->CesiumIonServer = UCesiumIonServer::GetServerForNewObjects();
  }

  const TSharedRef<CesiumViewExtension, ESPMode::ThreadSafe>&
      cesiumViewExtension = getCesiumViewExtension();
  const std::shared_ptr<CesiumAsync::IAssetAccessor>& pAssetAccessor =
      getAssetAccessor();
  const CesiumAsync::AsyncSystem& asyncSystem = getAsyncSystem();

  // Both the feature flag and the CesiumViewExtension are global, not owned by
  // the Tileset. We're just applying one to the other here out of convenience.
  cesiumViewExtension->SetEnabled(
      GetDefault<UCesiumRuntimeSettings>()
          ->EnableExperimentalOcclusionCullingFeature);

  TArray<UCesiumRasterOverlay*> rasterOverlays;
  this->GetComponents<UCesiumRasterOverlay>(rasterOverlays);

  TArray<UCesiumTileExcluder*> tileExcluders;
  this->GetComponents<UCesiumTileExcluder>(tileExcluders);

  const UCesiumFeaturesMetadataComponent* pFeaturesMetadataComponent =
      this->FindComponentByClass<UCesiumFeaturesMetadataComponent>();

  // Check if this component exists for backwards compatibility.
  PRAGMA_DISABLE_DEPRECATION_WARNINGS

  const UDEPRECATED_CesiumEncodedMetadataComponent* pEncodedMetadataComponent =
      this->FindComponentByClass<UDEPRECATED_CesiumEncodedMetadataComponent>();

  this->_featuresMetadataDescription = std::nullopt;
  this->_metadataDescription_DEPRECATED = std::nullopt;

  if (pFeaturesMetadataComponent) {
    FCesiumFeaturesMetadataDescription& description =
        this->_featuresMetadataDescription.emplace();
    description.Features = {pFeaturesMetadataComponent->FeatureIdSets};
    description.PrimitiveMetadata = {
        pFeaturesMetadataComponent->PropertyTextureNames};
    description.ModelMetadata = {
        pFeaturesMetadataComponent->PropertyTables,
        pFeaturesMetadataComponent->PropertyTextures};
  } else if (pEncodedMetadataComponent) {
    UE_LOG(
        LogCesium,
        Warning,
        TEXT(
            "CesiumEncodedMetadataComponent is deprecated. Use CesiumFeaturesMetadataComponent instead."));
    this->_metadataDescription_DEPRECATED = {
        pEncodedMetadataComponent->FeatureTables,
        pEncodedMetadataComponent->FeatureTextures};
  }

  PRAGMA_ENABLE_DEPRECATION_WARNINGS

  this->_cesiumViewExtension = cesiumViewExtension;

  if (GetDefault<UCesiumRuntimeSettings>()
          ->EnableExperimentalOcclusionCullingFeature &&
      this->EnableOcclusionCulling && !this->BoundingVolumePoolComponent) {
    const glm::dmat4& cesiumToUnreal =
        GetCesiumTilesetToUnrealRelativeWorldTransform();
    this->BoundingVolumePoolComponent =
        NewObject<UCesiumBoundingVolumePoolComponent>(this);
    this->BoundingVolumePoolComponent->SetFlags(
        RF_Transient | RF_DuplicateTransient | RF_TextExportTransient);
    this->BoundingVolumePoolComponent->RegisterComponent();
    this->BoundingVolumePoolComponent->UpdateTransformFromCesium(
        cesiumToUnreal);
  }

  if (this->BoundingVolumePoolComponent) {
    this->BoundingVolumePoolComponent->initPool(this->OcclusionPoolSize);
  }

  CesiumGeospatial::Ellipsoid pNativeEllipsoid =
      this->ResolveGeoreference()->GetEllipsoid()->GetNativeEllipsoid();

  ACesiumCreditSystem* pCreditSystem = this->ResolvedCreditSystem;

  Cesium3DTilesSelection::TilesetExternals externals{
      pAssetAccessor,
      std::make_shared<UnrealPrepareRendererResources>(this),
      asyncSystem,
      pCreditSystem ? pCreditSystem->GetExternalCreditSystem() : nullptr,
      spdlog::default_logger(),
      (GetDefault<UCesiumRuntimeSettings>()
           ->EnableExperimentalOcclusionCullingFeature &&
       this->EnableOcclusionCulling && this->BoundingVolumePoolComponent)
          ? this->BoundingVolumePoolComponent->getPool()
          : nullptr};

  this->_startTime = std::chrono::high_resolution_clock::now();

  this->LoadProgress = 0;

  Cesium3DTilesSelection::TilesetOptions options;

  options.ellipsoid = pNativeEllipsoid;

  options.enableOcclusionCulling =
      GetDefault<UCesiumRuntimeSettings>()
          ->EnableExperimentalOcclusionCullingFeature &&
      this->EnableOcclusionCulling;
  options.delayRefinementForOcclusion = this->DelayRefinementForOcclusion;

  options.showCreditsOnScreen = ShowCreditsOnScreen;

  options.loadErrorCallback =
      [this](const Cesium3DTilesSelection::TilesetLoadFailureDetails& details) {
        static_assert(
            uint8_t(ECesium3DTilesetLoadType::CesiumIon) ==
            uint8_t(Cesium3DTilesSelection::TilesetLoadType::CesiumIon));
        static_assert(
            uint8_t(ECesium3DTilesetLoadType::TilesetJson) ==
            uint8_t(Cesium3DTilesSelection::TilesetLoadType::TilesetJson));
        static_assert(
            uint8_t(ECesium3DTilesetLoadType::Unknown) ==
            uint8_t(Cesium3DTilesSelection::TilesetLoadType::Unknown));

        uint8_t typeValue = uint8_t(details.type);
        assert(
            uint8_t(details.type) <=
            uint8_t(Cesium3DTilesSelection::TilesetLoadType::TilesetJson));
        assert(this->_pTileset == details.pTileset);

        FCesium3DTilesetLoadFailureDetails ueDetails{};
        ueDetails.Tileset = this;
        ueDetails.Type = ECesium3DTilesetLoadType(typeValue);
        ueDetails.HttpStatusCode = details.statusCode;
        ueDetails.Message = UTF8_TO_TCHAR(details.message.c_str());

        // Broadcast the event from the game thread.
        // Even if we're already in the game thread, let the stack unwind.
        // Otherwise actions that destroy the Tileset will cause a deadlock.
        AsyncTask(
            ENamedThreads::GameThread,
            [ueDetails = std::move(ueDetails)]() {
              OnCesium3DTilesetLoadFailure.Broadcast(ueDetails);
            });
      };

  // Generous per-frame time limits for loading / unloading on main thread.
  options.mainThreadLoadingTimeLimit = 5.0;
  options.tileCacheUnloadTimeLimit = 5.0;

  options.contentOptions.generateMissingNormalsSmooth =
      this->GenerateSmoothNormals;

  // TODO: figure out why water material crashes mac
#if PLATFORM_MAC
#else
  options.contentOptions.enableWaterMask = this->EnableWaterMask;
#endif

  CesiumGltf::SupportedGpuCompressedPixelFormats supportedFormats;
  supportedFormats.ETC1_RGB = GPixelFormats[EPixelFormat::PF_ETC1].Supported;
  supportedFormats.ETC2_RGBA =
      GPixelFormats[EPixelFormat::PF_ETC2_RGBA].Supported;
  supportedFormats.BC1_RGB = GPixelFormats[EPixelFormat::PF_DXT1].Supported;
  supportedFormats.BC3_RGBA = GPixelFormats[EPixelFormat::PF_DXT5].Supported;
  supportedFormats.BC4_R = GPixelFormats[EPixelFormat::PF_BC4].Supported;
  supportedFormats.BC5_RG = GPixelFormats[EPixelFormat::PF_BC5].Supported;
  supportedFormats.BC7_RGBA = GPixelFormats[EPixelFormat::PF_BC7].Supported;
  supportedFormats.ASTC_4x4_RGBA =
      GPixelFormats[EPixelFormat::PF_ASTC_4x4].Supported;
  supportedFormats.PVRTC2_4_RGBA =
      GPixelFormats[EPixelFormat::PF_PVRTC2].Supported;
  supportedFormats.ETC2_EAC_R11 =
      GPixelFormats[EPixelFormat::PF_ETC2_R11_EAC].Supported;
  supportedFormats.ETC2_EAC_RG11 =
      GPixelFormats[EPixelFormat::PF_ETC2_RG11_EAC].Supported;

  options.contentOptions.ktx2TranscodeTargets =
      CesiumGltf::Ktx2TranscodeTargets(supportedFormats, false);

  options.contentOptions.applyTextureTransform = false;

  options.requestHeaders.reserve(this->RequestHeaders.Num());

  for (const auto& [Key, Value] : this->RequestHeaders) {
    options.requestHeaders.emplace_back(CesiumAsync::IAssetAccessor::THeader{
        TCHAR_TO_UTF8(*Key),
        TCHAR_TO_UTF8(*Value)});
  }

  switch (this->TilesetSource) {
  case ETilesetSource::FromEllipsoid:
    UE_LOG(LogCesium, Log, TEXT("Loading tileset from ellipsoid"));
    this->_pTileset = TUniquePtr<Cesium3DTilesSelection::Tileset>(
        Cesium3DTilesSelection::EllipsoidTilesetLoader::createTileset(
            externals,
            options)
            .release());
    break;
  case ETilesetSource::FromUrl:
    UE_LOG(LogCesium, Log, TEXT("Loading tileset from URL %s"), *this->Url);
    this->_pTileset = MakeUnique<Cesium3DTilesSelection::Tileset>(
        externals,
        TCHAR_TO_UTF8(*this->Url),
        options);
    break;
  case ETilesetSource::FromCesiumIon:
    UE_LOG(
        LogCesium,
        Log,
        TEXT("Loading tileset for asset ID %d"),
        this->IonAssetID);
    FString token = this->IonAccessToken.IsEmpty()
                        ? this->CesiumIonServer->DefaultIonAccessToken
                        : this->IonAccessToken;

#if WITH_EDITOR
    this->CesiumIonServer->ResolveApiUrl();
#endif

    std::string ionAssetEndpointUrl =
        TCHAR_TO_UTF8(*this->CesiumIonServer->ApiUrl);

    if (!ionAssetEndpointUrl.empty()) {
      // Make sure the URL ends with a slash
      if (!ionAssetEndpointUrl.empty() && *ionAssetEndpointUrl.rbegin() != '/')
        ionAssetEndpointUrl += '/';

      this->_pTileset = MakeUnique<Cesium3DTilesSelection::Tileset>(
          externals,
          static_cast<uint32_t>(this->IonAssetID),
          TCHAR_TO_UTF8(*token),
          options,
          ionAssetEndpointUrl);
    }
    break;
  }

#ifdef CESIUM_DEBUG_TILE_STATES
  FString dbDirectory = FPaths::Combine(
      FPaths::ProjectSavedDir(),
      TEXT("CesiumDebugTileStateDatabase"));

  IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
  if (!PlatformFile.DirectoryExists(*dbDirectory)) {
    PlatformFile.CreateDirectory(*dbDirectory);
  }

  FString dbFile =
      FPaths::Combine(dbDirectory, this->GetName() + TEXT(".sqlite"));
  this->_pStateDebug =
      MakeUnique<Cesium3DTilesSelection::DebugTileStateDatabase>(
          TCHAR_TO_UTF8(*dbFile));
#endif

  for (UCesiumRasterOverlay* pOverlay : rasterOverlays) {
    if (pOverlay->IsActive()) {
      pOverlay->AddToTileset();
    }
  }

  for (UCesiumTileExcluder* pTileExcluder : tileExcluders) {
    if (pTileExcluder->IsActive()) {
      pTileExcluder->AddToTileset();
    }
  }

  switch (this->TilesetSource) {
  case ETilesetSource::FromEllipsoid:
    UE_LOG(LogCesium, Log, TEXT("Loading tileset from ellipsoid done"));
    break;
  case ETilesetSource::FromUrl:
    UE_LOG(
        LogCesium,
        Log,
        TEXT("Loading tileset from URL %s done"),
        *this->Url);
    break;
  case ETilesetSource::FromCesiumIon:
    UE_LOG(
        LogCesium,
        Log,
        TEXT("Loading tileset for asset ID %d done"),
        this->IonAssetID);
    break;
  }

  switch (ApplyDpiScaling) {
  case (EApplyDpiScaling::UseProjectDefault):
    _scaleUsingDPI =
        GetDefault<UCesiumRuntimeSettings>()->ScaleLevelOfDetailByDPI;
    break;
  case (EApplyDpiScaling::Yes):
    _scaleUsingDPI = true;
    break;
  case (EApplyDpiScaling::No):
    _scaleUsingDPI = false;
    break;
  default:
    _scaleUsingDPI = true;
  }
}

void ACesium3DTileset::DestroyTileset() {
  if (this->_cesiumViewExtension) {
    this->_cesiumViewExtension = nullptr;
  }

  switch (this->TilesetSource) {
  case ETilesetSource::FromEllipsoid:
    UE_LOG(LogCesium, Verbose, TEXT("Destroying tileset from ellipsoid"));
    break;
  case ETilesetSource::FromUrl:
    UE_LOG(
        LogCesium,
        Verbose,
        TEXT("Destroying tileset from URL %s"),
        *this->Url);
    break;
  case ETilesetSource::FromCesiumIon:
    UE_LOG(
        LogCesium,
        Verbose,
        TEXT("Destroying tileset for asset ID %d"),
        this->IonAssetID);
    break;
  }

  // The way CesiumRasterOverlay::add is currently implemented, destroying the
  // tileset without removing overlays will make it impossible to add it again
  // once a new tileset is created (e.g. when switching between terrain
  // assets)
  TArray<UCesiumRasterOverlay*> rasterOverlays;
  this->GetComponents<UCesiumRasterOverlay>(rasterOverlays);
  for (UCesiumRasterOverlay* pOverlay : rasterOverlays) {
    if (pOverlay->IsActive()) {
      pOverlay->RemoveFromTileset();
    }
  }

  TArray<UCesiumTileExcluder*> tileExcluders;
  this->GetComponents<UCesiumTileExcluder>(tileExcluders);
  for (UCesiumTileExcluder* pTileExcluder : tileExcluders) {
    if (pTileExcluder->IsActive()) {
      pTileExcluder->RemoveFromTileset();
    }
  }

  if (!this->_pTileset) {
    return;
  }

  // Don't allow this Cesium3DTileset to be fully destroyed until
  // any cesium-native Tilesets it created have wrapped up any async
  // operations in progress and have been fully destroyed.
  // See IsReadyForFinishDestroy.
  ++this->_tilesetsBeingDestroyed;
  this->_pTileset->getAsyncDestructionCompleteEvent().thenInMainThread(
      [this]() { --this->_tilesetsBeingDestroyed; });
  this->_pTileset.Reset();

  switch (this->TilesetSource) {
  case ETilesetSource::FromEllipsoid:
    UE_LOG(LogCesium, Verbose, TEXT("Destroying tileset from ellipsoid done"));
    break;
  case ETilesetSource::FromUrl:
    UE_LOG(
        LogCesium,
        Verbose,
        TEXT("Destroying tileset from URL %s done"),
        *this->Url);
    break;
  case ETilesetSource::FromCesiumIon:
    UE_LOG(
        LogCesium,
        Verbose,
        TEXT("Destroying tileset for asset ID %d done"),
        this->IonAssetID);
    break;
  }
}

std::vector<FCesiumCamera> ACesium3DTileset::GetCameras() const {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::CollectCameras)
  std::vector<FCesiumCamera> cameras = this->GetPlayerCameras();

  std::vector<FCesiumCamera> sceneCaptures = this->GetSceneCaptures();
  cameras.insert(
      cameras.end(),
      std::make_move_iterator(sceneCaptures.begin()),
      std::make_move_iterator(sceneCaptures.end()));

#if WITH_EDITOR
  std::vector<FCesiumCamera> editorCameras = this->GetEditorCameras();
  cameras.insert(
      cameras.end(),
      std::make_move_iterator(editorCameras.begin()),
      std::make_move_iterator(editorCameras.end()));
#endif

  ACesiumCameraManager* pCameraManager = this->ResolvedCameraManager;
  if (pCameraManager) {
    const TMap<int32, FCesiumCamera>& extraCameras =
        pCameraManager->GetCameras();
    cameras.reserve(cameras.size() + extraCameras.Num());
    for (auto cameraIt : extraCameras) {
      cameras.push_back(cameraIt.Value);
    }
  }

  return cameras;
}

std::vector<FCesiumCamera> ACesium3DTileset::GetPlayerCameras() const {
  UWorld* pWorld = this->GetWorld();
  if (!pWorld) {
    return {};
  }

  double worldToMeters = 100.0;
  AWorldSettings* pWorldSettings = pWorld->GetWorldSettings();
  if (pWorldSettings) {
    worldToMeters = pWorldSettings->WorldToMeters;
  }

  TSharedPtr<IStereoRendering, ESPMode::ThreadSafe> pStereoRendering = nullptr;
  if (GEngine) {
    pStereoRendering = GEngine->StereoRenderingDevice;
  }

  bool useStereoRendering = false;
  if (pStereoRendering && pStereoRendering->IsStereoEnabled()) {
    useStereoRendering = true;
  }

  std::vector<FCesiumCamera> cameras;
  cameras.reserve(pWorld->GetNumPlayerControllers());

  for (auto playerControllerIt = pWorld->GetPlayerControllerIterator();
       playerControllerIt;
       playerControllerIt++) {
    const TWeakObjectPtr<APlayerController> pPlayerController =
        *playerControllerIt;
    if (pPlayerController == nullptr) {
      continue;
    }

    const APlayerCameraManager* pPlayerCameraManager =
        pPlayerController->PlayerCameraManager;

    if (!pPlayerCameraManager) {
      continue;
    }

    double fov = pPlayerCameraManager->GetFOVAngle();

    FVector location;
    FRotator rotation;
    pPlayerController->GetPlayerViewPoint(location, rotation);

    int32 sizeX, sizeY;
    pPlayerController->GetViewportSize(sizeX, sizeY);
    if (sizeX < 1 || sizeY < 1) {
      continue;
    }

    float dpiScalingFactor = 1.0f;
    if (this->_scaleUsingDPI) {
      ULocalPlayer* LocPlayer = Cast<ULocalPlayer>(pPlayerController->Player);
      if (LocPlayer && LocPlayer->ViewportClient) {
        dpiScalingFactor = LocPlayer->ViewportClient->GetDPIScale();
      }
    }

    if (useStereoRendering) {
      const auto leftEye = EStereoscopicEye::eSSE_LEFT_EYE;
      const auto rightEye = EStereoscopicEye::eSSE_RIGHT_EYE;

      uint32 stereoLeftSizeX = static_cast<uint32>(sizeX);
      uint32 stereoLeftSizeY = static_cast<uint32>(sizeY);
      uint32 stereoRightSizeX = static_cast<uint32>(sizeX);
      uint32 stereoRightSizeY = static_cast<uint32>(sizeY);
      if (useStereoRendering) {
        int32 _x;
        int32 _y;

        pStereoRendering
            ->AdjustViewRect(leftEye, _x, _y, stereoLeftSizeX, stereoLeftSizeY);

        pStereoRendering->AdjustViewRect(
            rightEye,
            _x,
            _y,
            stereoRightSizeX,
            stereoRightSizeY);
      }

      FVector2D stereoLeftSize(stereoLeftSizeX, stereoLeftSizeY);
      FVector2D stereoRightSize(stereoRightSizeX, stereoRightSizeY);

      if (stereoLeftSize.X >= 1.0 && stereoLeftSize.Y >= 1.0) {
        FVector leftEyeLocation = location;
        FRotator leftEyeRotation = rotation;
        pStereoRendering->CalculateStereoViewOffset(
            leftEye,
            leftEyeRotation,
            worldToMeters,
            leftEyeLocation);

        FMatrix projection =
            pStereoRendering->GetStereoProjectionMatrix(leftEye);

        // TODO: consider assymetric frustums using 4 fovs
        double one_over_tan_half_hfov = projection.M[0][0];

        double hfov =
            glm::degrees(2.0 * glm::atan(1.0 / one_over_tan_half_hfov));

        cameras.emplace_back(
            stereoLeftSize,
            leftEyeLocation,
            leftEyeRotation,
            hfov);
      }

      if (stereoRightSize.X >= 1.0 && stereoRightSize.Y >= 1.0) {
        FVector rightEyeLocation = location;
        FRotator rightEyeRotation = rotation;
        pStereoRendering->CalculateStereoViewOffset(
            rightEye,
            rightEyeRotation,
            worldToMeters,
            rightEyeLocation);

        FMatrix projection =
            pStereoRendering->GetStereoProjectionMatrix(rightEye);

        double one_over_tan_half_hfov = projection.M[0][0];

        double hfov =
            glm::degrees(2.0f * glm::atan(1.0f / one_over_tan_half_hfov));

        cameras.emplace_back(
            stereoRightSize,
            rightEyeLocation,
            rightEyeRotation,
            hfov);
      }
    } else {
      cameras.emplace_back(
          FVector2D(sizeX / dpiScalingFactor, sizeY / dpiScalingFactor),
          location,
          rotation,
          fov);
    }
  }

  return cameras;
}

std::vector<FCesiumCamera> ACesium3DTileset::GetSceneCaptures() const {
  // TODO: really USceneCaptureComponent2D can be attached to any actor, is it
  // worth searching every actor? Might it be better to provide an interface
  // where users can volunteer cameras to be used with the tile selection as
  // needed?
  TArray<AActor*> sceneCaptures;
  static TSubclassOf<ASceneCapture2D> SceneCapture2D =
      ASceneCapture2D::StaticClass();
  UGameplayStatics::GetAllActorsOfClass(this, SceneCapture2D, sceneCaptures);

  std::vector<FCesiumCamera> cameras;
  cameras.reserve(sceneCaptures.Num());

  for (AActor* pActor : sceneCaptures) {
    ASceneCapture2D* pSceneCapture = static_cast<ASceneCapture2D*>(pActor);
    if (!pSceneCapture) {
      continue;
    }

    USceneCaptureComponent2D* pSceneCaptureComponent =
        pSceneCapture->GetCaptureComponent2D();
    if (!pSceneCaptureComponent) {
      continue;
    }

    if (pSceneCaptureComponent->ProjectionType !=
        ECameraProjectionMode::Type::Perspective) {
      continue;
    }

    UTextureRenderTarget2D* pRenderTarget =
        pSceneCaptureComponent->TextureTarget;
    if (!pRenderTarget) {
      continue;
    }

    FVector2D renderTargetSize(pRenderTarget->SizeX, pRenderTarget->SizeY);
    if (renderTargetSize.X < 1.0 || renderTargetSize.Y < 1.0) {
      continue;
    }

    FVector captureLocation = pSceneCaptureComponent->GetComponentLocation();
    FRotator captureRotation = pSceneCaptureComponent->GetComponentRotation();
    double captureFov = pSceneCaptureComponent->FOVAngle;

    cameras.emplace_back(
        renderTargetSize,
        captureLocation,
        captureRotation,
        captureFov);
  }

  return cameras;
}

/*static*/ Cesium3DTilesSelection::ViewState
ACesium3DTileset::CreateViewStateFromViewParameters(
    const FCesiumCamera& camera,
    const glm::dmat4& unrealWorldToTileset,
    UCesiumEllipsoid* ellipsoid) {
  double horizontalFieldOfView =
      FMath::DegreesToRadians(camera.FieldOfViewDegrees);

  double actualAspectRatio;
  glm::dvec2 size(camera.ViewportSize.X, camera.ViewportSize.Y);

  if (camera.OverrideAspectRatio != 0.0f) {
    // Use aspect ratio and recompute effective viewport size after black bars
    // are added.
    actualAspectRatio = camera.OverrideAspectRatio;
    double computedX = actualAspectRatio * camera.ViewportSize.Y;
    double computedY = camera.ViewportSize.Y / actualAspectRatio;

    double barWidth = camera.ViewportSize.X - computedX;
    double barHeight = camera.ViewportSize.Y - computedY;

    if (barWidth > 0.0 && barWidth > barHeight) {
      // Black bars on the sides
      size.x = computedX;
    } else if (barHeight > 0.0 && barHeight > barWidth) {
      // Black bars on the top and bottom
      size.y = computedY;
    }
  } else {
    actualAspectRatio = camera.ViewportSize.X / camera.ViewportSize.Y;
  }

  double verticalFieldOfView =
      atan(tan(horizontalFieldOfView * 0.5) / actualAspectRatio) * 2.0;

  FVector direction = camera.Rotation.RotateVector(FVector(1.0f, 0.0f, 0.0f));
  FVector up = camera.Rotation.RotateVector(FVector(0.0f, 0.0f, 1.0f));

  glm::dvec3 tilesetCameraLocation = glm::dvec3(
      unrealWorldToTileset *
      glm::dvec4(camera.Location.X, camera.Location.Y, camera.Location.Z, 1.0));
  glm::dvec3 tilesetCameraFront = glm::normalize(glm::dvec3(
      unrealWorldToTileset *
      glm::dvec4(direction.X, direction.Y, direction.Z, 0.0)));
  glm::dvec3 tilesetCameraUp = glm::normalize(
      glm::dvec3(unrealWorldToTileset * glm::dvec4(up.X, up.Y, up.Z, 0.0)));

  return Cesium3DTilesSelection::ViewState::create(
      tilesetCameraLocation,
      tilesetCameraFront,
      tilesetCameraUp,
      size,
      horizontalFieldOfView,
      verticalFieldOfView,
      ellipsoid->GetNativeEllipsoid());
}

#if WITH_EDITOR
std::vector<FCesiumCamera> ACesium3DTileset::GetEditorCameras() const {
  if (!GEditor) {
    return {};
  }

  UWorld* pWorld = this->GetWorld();
  if (!IsValid(pWorld)) {
    return {};
  }

  // Do not include editor cameras when running in a game world (which includes
  // Play-in-Editor)
  if (pWorld->IsGameWorld()) {
    return {};
  }

  const TArray<FEditorViewportClient*>& viewportClients =
      GEditor->GetAllViewportClients();

  std::vector<FCesiumCamera> cameras;
  cameras.reserve(viewportClients.Num());

  for (FEditorViewportClient* pEditorViewportClient : viewportClients) {
    if (!pEditorViewportClient) {
      continue;
    }

    if (!pEditorViewportClient->IsVisible() ||
        !pEditorViewportClient->IsRealtime() ||
        !pEditorViewportClient->IsPerspective()) {
      continue;
    }

    FRotator rotation;
    if (pEditorViewportClient->bUsingOrbitCamera) {
      rotation = (pEditorViewportClient->GetLookAtLocation() -
                  pEditorViewportClient->GetViewLocation())
                     .Rotation();
    } else {
      rotation = pEditorViewportClient->GetViewRotation();
    }

    const FVector& location = pEditorViewportClient->GetViewLocation();
    double fov = pEditorViewportClient->ViewFOV;
    FIntPoint offset;
    FIntPoint size;
    pEditorViewportClient->GetViewportDimensions(offset, size);

    if (size.X < 1 || size.Y < 1) {
      continue;
    }

    if (this->_scaleUsingDPI) {
      float dpiScalingFactor = pEditorViewportClient->GetDPIScale();
      size.X = static_cast<float>(size.X) / dpiScalingFactor;
      size.Y = static_cast<float>(size.Y) / dpiScalingFactor;
    }

    if (pEditorViewportClient->IsAspectRatioConstrained()) {
      cameras.emplace_back(
          size,
          location,
          rotation,
          fov,
          pEditorViewportClient->AspectRatio);
    } else {
      cameras.emplace_back(size, location, rotation, fov);
    }
  }

  return cameras;
}
#endif

bool ACesium3DTileset::ShouldTickIfViewportsOnly() const {
  return this->UpdateInEditor;
}

namespace {
template <typename Func>
void forEachRenderableTile(const auto& tiles, Func&& f) {
  for (Cesium3DTilesSelection::Tile* pTile : tiles) {
    if (!pTile ||
        pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) {
      continue;
    }

    const Cesium3DTilesSelection::TileContent& content = pTile->getContent();
    const Cesium3DTilesSelection::TileRenderContent* pRenderContent =
        content.getRenderContent();
    if (!pRenderContent) {
      continue;
    }

    UCesiumGltfComponent* Gltf = static_cast<UCesiumGltfComponent*>(
        pRenderContent->getRenderResources());
    if (!Gltf) {
      // When a tile does not have render resources (i.e. a glTF), then
      // the resources either have not yet been loaded or prepared,
      // or the tile is from an external tileset and does not directly
      // own renderable content. In both cases, the tile is ignored here.
      continue;
    }

    f(pTile, Gltf);
  }
}

void removeVisibleTilesFromList(
    std::vector<Cesium3DTilesSelection::Tile*>& list,
    const std::vector<Cesium3DTilesSelection::Tile*>& visibleTiles) {
  if (list.empty()) {
    return;
  }

  for (Cesium3DTilesSelection::Tile* pTile : visibleTiles) {
    auto it = std::find(list.begin(), list.end(), pTile);
    if (it != list.end()) {
      list.erase(it);
    }
  }
}

/**
 * @brief Hides the visual representations of the given tiles.
 *
 * The visual representations (i.e. the `getRendererResources` of the
 * tiles) are assumed to be `UCesiumGltfComponent` instances that
 * are made invisible by this call.
 *
 * @param tiles The tiles to hide
 */
void hideTiles(const std::vector<Cesium3DTilesSelection::Tile*>& tiles) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::HideTiles)
  forEachRenderableTile(
      tiles,
      [](Cesium3DTilesSelection::Tile* /*pTile*/, UCesiumGltfComponent* pGltf) {
        if (pGltf->IsVisible()) {
          TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetVisibilityFalse)
          pGltf->SetVisibility(false, true);
        } else {
          // TODO: why is this happening?
          UE_LOG(
              LogCesium,
              Verbose,
              TEXT("Tile to no longer render does not have a visible Gltf"));
        }
      });
}

/**
 * @brief Removes collision for tiles that have been removed from the render
 * list. This includes tiles that are fading out.
 */
void removeCollisionForTiles(
    const std::unordered_set<Cesium3DTilesSelection::Tile*>& tiles) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::RemoveCollisionForTiles)
  forEachRenderableTile(
      tiles,
      [](Cesium3DTilesSelection::Tile* /*pTile*/, UCesiumGltfComponent* pGltf) {
        TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetCollisionDisabled)
        pGltf->SetCollisionEnabled(ECollisionEnabled::NoCollision);
      });
}

/**
 * @brief Applies the actor collision settings for a newly created glTF
 * component
 *
 * TODO Add details here what that means
 * @param BodyInstance ...
 * @param Gltf ...
 */
void applyActorCollisionSettings(
    const FBodyInstance& BodyInstance,
    UCesiumGltfComponent* Gltf) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::ApplyActorCollisionSettings)

  const TArray<USceneComponent*>& ChildrenComponents =
      Gltf->GetAttachChildren();

  for (USceneComponent* ChildComponent : ChildrenComponents) {
    UCesiumGltfPrimitiveComponent* PrimitiveComponent =
        Cast<UCesiumGltfPrimitiveComponent>(ChildComponent);
    if (PrimitiveComponent != nullptr) {
      if (PrimitiveComponent->GetCollisionObjectType() !=
          BodyInstance.GetObjectType()) {
        PrimitiveComponent->SetCollisionObjectType(
            BodyInstance.GetObjectType());
      }
      const UEnum* ChannelEnum = StaticEnum<ECollisionChannel>();
      if (ChannelEnum) {
        FCollisionResponseContainer responseContainer =
            BodyInstance.GetResponseToChannels();
        PrimitiveComponent->SetCollisionResponseToChannels(responseContainer);
      }
    }
  }
}
} // namespace

void ACesium3DTileset::updateTilesetOptionsFromProperties() {
  Cesium3DTilesSelection::TilesetOptions& options =
      this->_pTileset->getOptions();
  options.maximumScreenSpaceError =
      static_cast<double>(this->MaximumScreenSpaceError);
  options.maximumCachedBytes = this->MaximumCachedBytes;
  options.preloadAncestors = this->PreloadAncestors;
  options.preloadSiblings = this->PreloadSiblings;
  options.forbidHoles = this->ForbidHoles;
  options.maximumSimultaneousTileLoads = this->MaximumSimultaneousTileLoads;
  options.loadingDescendantLimit = this->LoadingDescendantLimit;
  options.enableFrustumCulling = this->EnableFrustumCulling;
  options.enableOcclusionCulling =
      GetDefault<UCesiumRuntimeSettings>()
          ->EnableExperimentalOcclusionCullingFeature &&
      this->EnableOcclusionCulling;
  options.showCreditsOnScreen = this->ShowCreditsOnScreen;

  options.delayRefinementForOcclusion = this->DelayRefinementForOcclusion;
  options.enableFogCulling = this->EnableFogCulling;
  options.enforceCulledScreenSpaceError = this->EnforceCulledScreenSpaceError;
  options.culledScreenSpaceError =
      static_cast<double>(this->CulledScreenSpaceError);
  options.enableLodTransitionPeriod = this->UseLodTransitions;
  options.lodTransitionLength = this->LodTransitionLength;
  // options.kickDescendantsWhileFadingIn = false;
}

void ACesium3DTileset::updateLastViewUpdateResultState(
    const Cesium3DTilesSelection::ViewUpdateResult& result) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::updateLastViewUpdateResultState)

  if (this->DrawTileInfo) {
    const UWorld* World = GetWorld();
    check(World);

    const TSoftObjectPtr<ACesiumGeoreference> Georeference =
        ResolveGeoreference();
    check(Georeference);

    for (Cesium3DTilesSelection::Tile* tile : result.tilesToRenderThisFrame) {
      CesiumGeometry::OrientedBoundingBox obb =
          Cesium3DTilesSelection::getOrientedBoundingBoxFromBoundingVolume(
              tile->getBoundingVolume(),
              Georeference->GetEllipsoid()->GetNativeEllipsoid());

      FVector unrealCenter =
          Georeference->TransformEarthCenteredEarthFixedPositionToUnreal(
              VecMath::createVector(obb.getCenter()));

      FString text = FString::Printf(
          TEXT("ID %s (%p)"),
          UTF8_TO_TCHAR(
              Cesium3DTilesSelection::TileIdUtilities::createTileIdString(
                  tile->getTileID())
                  .c_str()),
          tile);

      DrawDebugString(World, unrealCenter, text, nullptr, FColor::Red, 0, true);
    }
  }

#ifdef CESIUM_DEBUG_TILE_STATES
  if (this->_pStateDebug && GetWorld()->IsPlayInEditor()) {
    this->_pStateDebug->recordAllTileStates(
        result.frameNumber,
        *this->_pTileset);
  }
#endif

  if (!this->LogSelectionStats && !this->LogSharedAssetStats) {
    return;
  }

  if (result.tilesToRenderThisFrame.size() != this->_lastTilesRendered ||
      result.workerThreadTileLoadQueueLength !=
          this->_lastWorkerThreadTileLoadQueueLength ||
      result.mainThreadTileLoadQueueLength !=
          this->_lastMainThreadTileLoadQueueLength ||
      result.tilesVisited != this->_lastTilesVisited ||
      result.culledTilesVisited != this->_lastCulledTilesVisited ||
      result.tilesCulled != this->_lastTilesCulled ||
      result.tilesOccluded != this->_lastTilesOccluded ||
      result.tilesWaitingForOcclusionResults !=
          this->_lastTilesWaitingForOcclusionResults ||
      result.maxDepthVisited != this->_lastMaxDepthVisited) {
    this->_lastTilesRendered = result.tilesToRenderThisFrame.size();
    this->_lastWorkerThreadTileLoadQueueLength =
        result.workerThreadTileLoadQueueLength;
    this->_lastMainThreadTileLoadQueueLength =
        result.mainThreadTileLoadQueueLength;

    this->_lastTilesVisited = result.tilesVisited;
    this->_lastCulledTilesVisited = result.culledTilesVisited;
    this->_lastTilesCulled = result.tilesCulled;
    this->_lastTilesOccluded = result.tilesOccluded;
    this->_lastTilesWaitingForOcclusionResults =
        result.tilesWaitingForOcclusionResults;
    this->_lastMaxDepthVisited = result.maxDepthVisited;

    if (this->LogSelectionStats) {
      UE_LOG(
          LogCesium,
          Display,
          TEXT(
              "%s: %d ms, Unreal Frame #%d, Tileset Frame: #%d, Visited %d, Culled Visited %d, Rendered %d, Culled %d, Occluded %d, Waiting For Occlusion Results %d, Max Depth Visited: %d, Loading-Worker %d, Loading-Main %d, Loaded tiles %g%%"),
          *this->GetName(),
          (std::chrono::high_resolution_clock::now() - this->_startTime)
                  .count() /
              1000000,
          GFrameCounter,
          result.frameNumber,
          result.tilesVisited,
          result.culledTilesVisited,
          result.tilesToRenderThisFrame.size(),
          result.tilesCulled,
          result.tilesOccluded,
          result.tilesWaitingForOcclusionResults,
          result.maxDepthVisited,
          result.workerThreadTileLoadQueueLength,
          result.mainThreadTileLoadQueueLength,
          this->LoadProgress);
    }

    if (this->LogSharedAssetStats && this->_pTileset) {
      const Cesium3DTilesSelection::TilesetSharedAssetSystem::ImageDepot&
          imageDepot = *this->_pTileset->getSharedAssetSystem().pImage;
      UE_LOG(
          LogCesium,
          Display,
          TEXT(
              "Images shared asset depot: %d distinct assets, %d inactive assets pending deletion (%d bytes)"),
          imageDepot.getAssetCount(),
          imageDepot.getInactiveAssetCount(),
          imageDepot.getInactiveAssetTotalSizeBytes());
    }
  }
}

void ACesium3DTileset::showTilesToRender(
    const std::vector<Cesium3DTilesSelection::Tile*>& tiles) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::ShowTilesToRender)
  forEachRenderableTile(
      tiles,
      [&RootComponent = this->RootComponent,
       &BodyInstance = this->BodyInstance](
          Cesium3DTilesSelection::Tile* pTile,
          UCesiumGltfComponent* pGltf) {
        applyActorCollisionSettings(BodyInstance, pGltf);

        if (pGltf->GetAttachParent() == nullptr) {
          // The AttachToComponent method is ridiculously complex,
          // so print a warning if attaching fails for some reason
          bool attached = pGltf->AttachToComponent(
              RootComponent,
              FAttachmentTransformRules::KeepRelativeTransform);
          if (!attached) {
            FString tileIdString(
                Cesium3DTilesSelection::TileIdUtilities::createTileIdString(
                    pTile->getTileID())
                    .c_str());
            UE_LOG(
                LogCesium,
                Warning,
                TEXT("Tile %s could not be attached to root"),
                *tileIdString);
          }
        }

        if (!pGltf->IsVisible()) {
          TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetVisibilityTrue)
          pGltf->SetVisibility(true, true);
        }

        {
          TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::SetCollisionEnabled)
          pGltf->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
        }
      });
}

static void updateTileFades(const auto& tiles, bool fadingIn) {
  forEachRenderableTile(
      tiles,
      [fadingIn](
          Cesium3DTilesSelection::Tile* pTile,
          UCesiumGltfComponent* pGltf) {
        float percentage = pTile->getContent()
                               .getRenderContent()
                               ->getLodTransitionFadePercentage();
        pGltf->UpdateFade(percentage, fadingIn);
      });
}

// Called every frame
void ACesium3DTileset::Tick(float DeltaTime) {
  TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::TilesetTick)

  Super::Tick(DeltaTime);

  this->ResolveGeoreference();
  this->ResolveCameraManager();
  this->ResolveCreditSystem();

  UCesium3DTilesetRoot* pRoot = Cast<UCesium3DTilesetRoot>(this->RootComponent);
  if (!pRoot) {
    return;
  }

  if (this->SuspendUpdate) {
    return;
  }

  if (!this->_pTileset) {
    LoadTileset();

    // In the unlikely event that we _still_ don't have a tileset, stop here so
    // we don't crash below. This shouldn't happen.
    if (!this->_pTileset) {
      assert(false);
      return;
    }
  }

  if (this->BoundingVolumePoolComponent && this->_cesiumViewExtension) {
    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::UpdateOcclusion)
    const TArray<USceneComponent*>& children =
        this->BoundingVolumePoolComponent->GetAttachChildren();
    for (USceneComponent* pChild : children) {
      UCesiumBoundingVolumeComponent* pBoundingVolume =
          Cast<UCesiumBoundingVolumeComponent>(pChild);

      if (!pBoundingVolume) {
        continue;
      }

      pBoundingVolume->UpdateOcclusion(*this->_cesiumViewExtension.Get());
    }
  }

  updateTilesetOptionsFromProperties();

  std::vector<FCesiumCamera> cameras = this->GetCameras();

  glm::dmat4 ueTilesetToUeWorld =
      VecMath::createMatrix4D(this->GetActorTransform().ToMatrixWithScale());

  const glm::dmat4& cesiumTilesetToUeTileset =
      this->GetCesiumTilesetToUnrealRelativeWorldTransform();
  glm::dmat4 unrealWorldToCesiumTileset =
      glm::affineInverse(ueTilesetToUeWorld * cesiumTilesetToUeTileset);

  if (glm::isnan(unrealWorldToCesiumTileset[3].x) ||
      glm::isnan(unrealWorldToCesiumTileset[3].y) ||
      glm::isnan(unrealWorldToCesiumTileset[3].z)) {
    // Probably caused by a zero scale.
    return;
  }

  UCesiumEllipsoid* ellipsoid = this->ResolveGeoreference()->GetEllipsoid();

  std::vector<Cesium3DTilesSelection::ViewState> frustums;
  for (const FCesiumCamera& camera : cameras) {
    frustums.push_back(CreateViewStateFromViewParameters(
        camera,
        unrealWorldToCesiumTileset,
        ellipsoid));
  }

  const Cesium3DTilesSelection::ViewUpdateResult* pResult;
  if (this->_captureMovieMode) {
    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::updateViewOffline)
    pResult = &this->_pTileset->updateViewOffline(frustums);
  } else {
    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::updateView)
    pResult = &this->_pTileset->updateView(frustums, DeltaTime);
  }
  updateLastViewUpdateResultState(*pResult);

  removeCollisionForTiles(pResult->tilesFadingOut);

  removeVisibleTilesFromList(
      _tilesToHideNextFrame,
      pResult->tilesToRenderThisFrame);
  hideTiles(_tilesToHideNextFrame);

  _tilesToHideNextFrame.clear();
  for (Cesium3DTilesSelection::Tile* pTile : pResult->tilesFadingOut) {
    Cesium3DTilesSelection::TileRenderContent* pRenderContent =
        pTile->getContent().getRenderContent();
    if (!this->UseLodTransitions ||
        (pRenderContent &&
         pRenderContent->getLodTransitionFadePercentage() >= 1.0f)) {
      _tilesToHideNextFrame.push_back(pTile);
    }
  }

  showTilesToRender(pResult->tilesToRenderThisFrame);

  if (this->UseLodTransitions) {
    TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::UpdateTileFades)
    updateTileFades(pResult->tilesToRenderThisFrame, true);
    updateTileFades(pResult->tilesFadingOut, false);
  }

  this->UpdateLoadStatus();
}

void ACesium3DTileset::EndPlay(const EEndPlayReason::Type EndPlayReason) {
  this->DestroyTileset();
  AActor::EndPlay(EndPlayReason);
}

void ACesium3DTileset::PostLoad() {
  BodyInstance.FixupData(this); // We need to call this one after Loading the
                                // actor to have correct BodyInstance values.

  Super::PostLoad();

  if (CesiumActors::shouldValidateFlags(this))
    CesiumActors::validateActorFlags(this);

#if WITH_EDITOR
  const int32 CesiumVersion =
      this->GetLinkerCustomVersion(FCesiumCustomVersion::GUID);

  PRAGMA_DISABLE_DEPRECATION_WARNINGS
  if (CesiumVersion < FCesiumCustomVersion::CesiumIonServer) {
    this->CesiumIonServer = UCesiumIonServer::GetBackwardCompatibleServer(
        this->IonAssetEndpointUrl_DEPRECATED);
  }
  PRAGMA_ENABLE_DEPRECATION_WARNINGS
#endif
}

void ACesium3DTileset::Serialize(FArchive& Ar) {
  Super::Serialize(Ar);

  Ar.UsingCustomVersion(FCesiumCustomVersion::GUID);

  const int32 CesiumVersion = Ar.CustomVer(FCesiumCustomVersion::GUID);

  if (CesiumVersion < FCesiumCustomVersion::TilesetExplicitSource) {
    // In previous versions, the tileset source was inferred from the presence
    // of a non-empty URL property, rather than being explicitly specified.
    if (this->Url.Len() > 0) {
      this->TilesetSource = ETilesetSource::FromUrl;
    } else {
      this->TilesetSource = ETilesetSource::FromCesiumIon;
    }
  }

  if (CesiumVersion < FCesiumCustomVersion::TilesetMobilityRemoved) {
    this->RootComponent->SetMobility(this->Mobility_DEPRECATED);
  }
}

#if WITH_EDITOR
void ACesium3DTileset::PostEditChangeProperty(
    FPropertyChangedEvent& PropertyChangedEvent) {
  Super::PostEditChangeProperty(PropertyChangedEvent);

  if (!PropertyChangedEvent.Property) {
    return;
  }

  FName PropName = PropertyChangedEvent.Property->GetFName();
  FString PropNameAsString = PropertyChangedEvent.Property->GetName();

  if (PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, TilesetSource) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, Url) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, IonAssetID) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, IonAccessToken) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, CreatePhysicsMeshes) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, CreateNavCollision) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, AlwaysIncludeTangents) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, GenerateSmoothNormals) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, EnableWaterMask) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, IgnoreKhrMaterialsUnlit) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, Material) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, TranslucentMaterial) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, WaterMaterial) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, ApplyDpiScaling) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, EnableOcclusionCulling) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, UseLodTransitions) ||
      PropName ==
          GET_MEMBER_NAME_CHECKED(ACesium3DTileset, ShowCreditsOnScreen) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, Root) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, CesiumIonServer) ||
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, RequestHeaders) ||
      // For properties nested in structs, GET_MEMBER_NAME_CHECKED will prefix
      // with the struct name, so just do a manual string comparison.
      PropNameAsString == TEXT("RenderCustomDepth") ||
      PropNameAsString == TEXT("CustomDepthStencilValue") ||
      PropNameAsString == TEXT("CustomDepthStencilWriteMask")) {
    this->DestroyTileset();
  } else if (
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, Georeference)) {
    this->InvalidateResolvedGeoreference();
  } else if (
      PropName == GET_MEMBER_NAME_CHECKED(ACesium3DTileset, CreditSystem)) {
    this->InvalidateResolvedCreditSystem();
  } else if (
      PropName ==
      GET_MEMBER_NAME_CHECKED(ACesium3DTileset, MaximumScreenSpaceError)) {
    TArray<UCesiumRasterOverlay*> rasterOverlays;
    this->GetComponents<UCesiumRasterOverlay>(rasterOverlays);

    for (UCesiumRasterOverlay* pOverlay : rasterOverlays) {
      pOverlay->Refresh();
    }
    TArray<UCesiumTileExcluder*> tileExcluders;
    this->GetComponents<UCesiumTileExcluder>(tileExcluders);

    for (UCesiumTileExcluder* pTileExcluder : tileExcluders) {
      pTileExcluder->Refresh();
    }

    // Maximum Screen Space Error can affect how attenuated points are rendered,
    // so propagate the new value to the render proxies for this tileset.
    FCesiumGltfPointsSceneProxyUpdater::UpdateSettingsInProxies(this);
  }
}

void ACesium3DTileset::PostEditChangeChainProperty(
    FPropertyChangedChainEvent& PropertyChangedChainEvent) {
  Super::PostEditChangeChainProperty(PropertyChangedChainEvent);

  if (!PropertyChangedChainEvent.Property ||
      PropertyChangedChainEvent.PropertyChain.IsEmpty()) {
    return;
  }

  FName PropName =
      PropertyChangedChainEvent.PropertyChain.GetHead()->GetValue()->GetFName();
  if (PropName ==
      GET_MEMBER_NAME_CHECKED(ACesium3DTileset, PointCloudShading)) {
    FCesiumGltfPointsSceneProxyUpdater::UpdateSettingsInProxies(this);
  }
}

void ACesium3DTileset::PostEditUndo() {
  Super::PostEditUndo();

  // It doesn't appear to be possible to get detailed information about what
  // changed in the undo/redo operation, so we have to assume the worst and
  // recreate the tileset.
  this->DestroyTileset();
}

void ACesium3DTileset::PostEditImport() {
  Super::PostEditImport();

  // Recreate the tileset on Paste.
  this->DestroyTileset();
}

bool ACesium3DTileset::CanEditChange(const FProperty* InProperty) const {
  if (InProperty->GetFName() ==
      GET_MEMBER_NAME_CHECKED(ACesium3DTileset, EnableWaterMask)) {
    // Disable this option on Mac
    return PlatformName != TEXT("Mac");
  }
  return true;
}
#endif

void ACesium3DTileset::BeginDestroy() {
  this->InvalidateResolvedGeoreference();
  this->DestroyTileset();

  AActor::BeginDestroy();
}

bool ACesium3DTileset::IsReadyForFinishDestroy() {
  bool ready = AActor::IsReadyForFinishDestroy();
  ready &= this->_tilesetsBeingDestroyed == 0;

  if (!ready) {
    getAssetAccessor()->tick();
    getAsyncSystem().dispatchMainThreadTasks();
  }

  return ready;
}

void ACesium3DTileset::Destroyed() {
  this->DestroyTileset();

  AActor::Destroyed();
}

#if WITH_EDITOR
void ACesium3DTileset::RuntimeSettingsChanged(
    UObject* pObject,
    struct FPropertyChangedEvent& changed) {
  bool occlusionCullingAvailable =
      GetDefault<UCesiumRuntimeSettings>()
          ->EnableExperimentalOcclusionCullingFeature;
  if (occlusionCullingAvailable != this->CanEnableOcclusionCulling) {
    this->CanEnableOcclusionCulling = occlusionCullingAvailable;
    this->RefreshTileset();
  }
}
#endif