624 lines
21 KiB
C++
624 lines
21 KiB
C++
// Copyright 2020-2024 CesiumGS, Inc. and Contributors
|
|
|
|
#include "CesiumSunSky.h"
|
|
#include "CesiumCustomVersion.h"
|
|
#include "CesiumGlobeAnchorComponent.h"
|
|
#include "CesiumRuntime.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/Pawn.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "SunPosition.h"
|
|
#include "TimerManager.h"
|
|
#include "UObject/ConstructorHelpers.h"
|
|
#include "VecMath.h"
|
|
|
|
#if WITH_EDITOR
|
|
#include "Editor.h"
|
|
#include "EditorViewportClient.h"
|
|
#include "LevelEditorViewport.h"
|
|
#endif
|
|
|
|
// The UE SkyAtmosphere assumes Earth is a sphere. But it's closer to an oblate
|
|
// spheroid, where the radius at the poles is ~21km less than the radius at the
|
|
// equator. And on top of that, there's terrain, causing bumps of up to 8km or
|
|
// so (Mount Everest). Mean Sea Level is nowhere more than 100 meters different
|
|
// from the ellipsoid, and the lowest dry land point on Earth is the Dead
|
|
// Sea at about 432 meters below sea level. So all up, the worst case "ground
|
|
// radius" for atmosphere purposes ranges from about 6356km to about 6387km
|
|
// depending on where you are on Earth. That's a range of 31km, which definitely
|
|
// matters. We can't pick a single globe radius that will work everywhere on
|
|
// Earth.
|
|
//
|
|
// So, our strategy here is:
|
|
// * When close to the surface, it's important that the radius not be too
|
|
// large, or else there will be a gap between the bottom of the atmosphere and
|
|
// the top of the terrain. To avoid that, we want to use a tight fitting globe
|
|
// radius that approximates mean sea level at the camera's position and is
|
|
// guaranteed to be below it. Rather than actually calculate sea level, an
|
|
// ellipsoid height of -100meters will be close enough.
|
|
// * When far from the surface, we can see a lot of the Earth, and it's
|
|
// essential that no bits of the surface extend outside the atmosphere, because
|
|
// that creates a very distracting artifact. So we want to choose a globe
|
|
// radius that is guaranteed to encapsulate all visible parts of the globe.
|
|
// * In between these two extremes, we need to blend smoothly.
|
|
|
|
// Sets default values
|
|
ACesiumSunSky::ACesiumSunSky() : AActor() {
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
|
|
#if WITH_EDITOR
|
|
this->SetIsSpatiallyLoaded(false);
|
|
#endif
|
|
|
|
Scene = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));
|
|
SetRootComponent(Scene);
|
|
|
|
DirectionalLight = CreateDefaultSubobject<UDirectionalLightComponent>(
|
|
TEXT("DirectionalLight"));
|
|
DirectionalLight->SetupAttachment(Scene);
|
|
DirectionalLight->Intensity = 111000.f;
|
|
DirectionalLight->LightSourceAngle = 0.5;
|
|
DirectionalLight->DynamicShadowCascades = 5;
|
|
DirectionalLight->CascadeDistributionExponent = 2.0;
|
|
DirectionalLight->DynamicShadowDistanceMovableLight = 500000.f;
|
|
|
|
// We need to set both of these, because in the case of a pre-UE5 asset, UE5
|
|
// will replace the normal atmosphere sun light flag with the value of the
|
|
// deprecated one on load.
|
|
DirectionalLight->bUsedAsAtmosphereSunLight_DEPRECATED = true;
|
|
DirectionalLight->SetAtmosphereSunLight(true);
|
|
|
|
DirectionalLight->SetRelativeLocation(FVector(0, 0, 0));
|
|
|
|
if (!SkySphereClass) {
|
|
static ConstructorHelpers::FClassFinder<AActor> skySphereFinder(
|
|
TEXT("Blueprint'/CesiumForUnreal/MobileSkySphere.MobileSkySphere_C'"));
|
|
if (skySphereFinder.Succeeded()) {
|
|
SkySphereClass = skySphereFinder.Class;
|
|
}
|
|
}
|
|
|
|
// Always create these components and hide them if not needed (e.g. on mobile)
|
|
SkyLight = CreateDefaultSubobject<USkyLightComponent>(TEXT("SkyLight"));
|
|
SkyLight->SetupAttachment(Scene);
|
|
SkyLight->SetMobility(EComponentMobility::Movable);
|
|
SkyLight->bRealTimeCapture = true;
|
|
SkyLight->bLowerHemisphereIsBlack = false;
|
|
SkyLight->bTransmission = true;
|
|
SkyLight->SamplesPerPixel = 2;
|
|
|
|
SkyLight->CastRaytracedShadow = ECastRayTracedShadow::Enabled;
|
|
|
|
// Initially put the SkyLight at the world origin.
|
|
// This is updated in UpdateSun.
|
|
SkyLight->SetUsingAbsoluteLocation(true);
|
|
SkyLight->SetWorldLocation(FVector(0, 0, 0));
|
|
|
|
// The Sky Atmosphere should be positioned relative to the
|
|
// Scene/RootComponent, which is kept at the center of the Earth by the
|
|
// GlobeAnchorComponent.
|
|
SkyAtmosphere =
|
|
CreateDefaultSubobject<USkyAtmosphereComponent>(TEXT("SkyAtmosphere"));
|
|
SkyAtmosphere->SetupAttachment(Scene);
|
|
SkyAtmosphere->TransformMode =
|
|
ESkyAtmosphereTransformMode::PlanetCenterAtComponentTransform;
|
|
SkyAtmosphere->TransmittanceMinLightElevationAngle = 90.0f;
|
|
|
|
this->GlobeAnchor =
|
|
CreateDefaultSubobject<UCesiumGlobeAnchorComponent>(TEXT("GlobeAnchor"));
|
|
this->GlobeAnchor->SetAdjustOrientationForGlobeWhenMoving(false);
|
|
}
|
|
|
|
void ACesiumSunSky::_handleTransformUpdated(
|
|
USceneComponent* InRootComponent,
|
|
EUpdateTransformFlags UpdateTransformFlags,
|
|
ETeleportType Teleport) {
|
|
// This Actor generally shouldn't move with respect to the globe, but this
|
|
// method will be called on georeference change. We need to update the sun
|
|
// position for the new UE coordinate system.
|
|
this->UpdateSun();
|
|
}
|
|
|
|
void ACesiumSunSky::OnConstruction(const FTransform& Transform) {
|
|
Super::OnConstruction(Transform);
|
|
|
|
UE_LOG(
|
|
LogCesium,
|
|
Verbose,
|
|
TEXT("Called OnConstruction for CesiumSunSky %s"),
|
|
*this->GetName());
|
|
|
|
if (IsValid(this->GlobeAnchor)) {
|
|
this->GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(
|
|
FVector(0.0, 0.0, 0.0));
|
|
}
|
|
|
|
UE_LOG(
|
|
LogCesium,
|
|
Verbose,
|
|
TEXT("Spawn new sky sphere: %s"),
|
|
_wantsSpawnMobileSkySphere ? TEXT("true") : TEXT("false"));
|
|
if (UseMobileRendering) {
|
|
DirectionalLight->Intensity = MobileDirectionalLightIntensity;
|
|
if (_wantsSpawnMobileSkySphere && SkySphereClass) {
|
|
_spawnSkySphere();
|
|
}
|
|
}
|
|
_setSkyAtmosphereVisibility(!UseMobileRendering);
|
|
|
|
this->UpdateSun();
|
|
}
|
|
|
|
void ACesiumSunSky::_spawnSkySphere() {
|
|
UWorld* pWorld = GetWorld();
|
|
if (!UseMobileRendering || !IsValid(pWorld)) {
|
|
return;
|
|
}
|
|
|
|
ACesiumGeoreference* pGeoreference = this->GetGeoreference();
|
|
if (!IsValid(pGeoreference)) {
|
|
return;
|
|
}
|
|
|
|
// Create a new Sky Sphere Actor and anchor it to the center of the Earth.
|
|
this->SkySphereActor = pWorld->SpawnActor<AActor>(SkySphereClass);
|
|
|
|
// Anchor it to the center of the Earth.
|
|
UCesiumGlobeAnchorComponent* GlobeAnchorComponent =
|
|
NewObject<UCesiumGlobeAnchorComponent>(
|
|
SkySphereActor,
|
|
TEXT("GlobeAnchor"));
|
|
this->SkySphereActor->AddInstanceComponent(GlobeAnchorComponent);
|
|
GlobeAnchorComponent->SetAdjustOrientationForGlobeWhenMoving(false);
|
|
GlobeAnchorComponent->SetGeoreference(pGeoreference);
|
|
GlobeAnchorComponent->MoveToEarthCenteredEarthFixedPosition(
|
|
FVector(0.0, 0.0, 0.0));
|
|
|
|
_wantsSpawnMobileSkySphere = false;
|
|
|
|
_setSkySphereDirectionalLight();
|
|
}
|
|
|
|
double ACesiumSunSky::_computeScale() const {
|
|
// The SkyAtmosphere is not affected by Actor scaling, so we do it manually.
|
|
FVector actorScale = this->GetActorScale();
|
|
return actorScale.GetMax();
|
|
}
|
|
|
|
void ACesiumSunSky::UpdateSkySphere() {
|
|
if (!UseMobileRendering || !IsValid(SkySphereActor)) {
|
|
return;
|
|
}
|
|
UFunction* UpdateSkySphere =
|
|
this->SkySphereActor->FindFunction(TEXT("RefreshMaterial"));
|
|
if (UpdateSkySphere) {
|
|
this->SkySphereActor->ProcessEvent(UpdateSkySphere, NULL);
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::BeginPlay() {
|
|
Super::BeginPlay();
|
|
|
|
if (IsValid(this->GlobeAnchor)) {
|
|
this->GlobeAnchor->MoveToEarthCenteredEarthFixedPosition(
|
|
FVector(0.0, 0.0, 0.0));
|
|
}
|
|
|
|
this->_transformUpdatedSubscription =
|
|
this->RootComponent->TransformUpdated.AddUObject(
|
|
this,
|
|
&ACesiumSunSky::_handleTransformUpdated);
|
|
|
|
_setSkyAtmosphereVisibility(!UseMobileRendering);
|
|
|
|
this->UpdateSun();
|
|
|
|
if (this->UpdateAtmosphereAtRuntime) {
|
|
this->UpdateAtmosphereRadius();
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::EndPlay(const EEndPlayReason::Type EndPlayReason) {
|
|
if (this->_transformUpdatedSubscription.IsValid()) {
|
|
this->RootComponent->TransformUpdated.Remove(
|
|
this->_transformUpdatedSubscription);
|
|
this->_transformUpdatedSubscription.Reset();
|
|
}
|
|
|
|
Super::EndPlay(EndPlayReason);
|
|
}
|
|
|
|
void ACesiumSunSky::Serialize(FArchive& Ar) {
|
|
Super::Serialize(Ar);
|
|
|
|
Ar.UsingCustomVersion(FCesiumCustomVersion::GUID);
|
|
|
|
const int32 CesiumVersion = Ar.CustomVer(FCesiumCustomVersion::GUID);
|
|
|
|
if (Ar.IsLoading() &&
|
|
CesiumVersion < FCesiumCustomVersion::GeoreferenceRefactoring) {
|
|
// Now that CesiumSunSky is a C++ class, its Components should be marked
|
|
// with a CreationMethod of Native, and they are to start. But because
|
|
// CesiumSunSky was a Blueprints class in old versions, the CreationMethod
|
|
// of its components gets set to SimpleConstructionScript on load, which
|
|
// causes those components to later (and erroneously) be removed. So we
|
|
// reset the creation method here.
|
|
Ar.Preload(this->RootComponent);
|
|
this->RootComponent->CreationMethod = EComponentCreationMethod::Native;
|
|
Ar.Preload(this->SkyLight);
|
|
this->SkyLight->CreationMethod = EComponentCreationMethod::Native;
|
|
Ar.Preload(this->DirectionalLight);
|
|
this->DirectionalLight->CreationMethod = EComponentCreationMethod::Native;
|
|
Ar.Preload(this->SkyAtmosphere);
|
|
this->SkyAtmosphere->CreationMethod = EComponentCreationMethod::Native;
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::Tick(float DeltaSeconds) {
|
|
Super::Tick(DeltaSeconds);
|
|
|
|
if (this->UpdateAtmosphereAtRuntime) {
|
|
this->UpdateAtmosphereRadius();
|
|
}
|
|
|
|
if (IsValid(this->SkyAtmosphere)) {
|
|
double scale = this->_computeScale();
|
|
|
|
float atmosphereHeight = float(scale * this->AtmosphereHeight);
|
|
if (atmosphereHeight != this->SkyAtmosphere->AtmosphereHeight) {
|
|
this->SkyAtmosphere->SetAtmosphereHeight(atmosphereHeight);
|
|
}
|
|
|
|
float aerialPerspectiveViewDistanceScale =
|
|
float(this->AerialPerspectiveViewDistanceScale / scale);
|
|
if (aerialPerspectiveViewDistanceScale !=
|
|
this->SkyAtmosphere->AerialPespectiveViewDistanceScale) {
|
|
this->SkyAtmosphere->SetAerialPespectiveViewDistanceScale(
|
|
aerialPerspectiveViewDistanceScale);
|
|
}
|
|
|
|
float rayleighExponentialDistribution =
|
|
float(scale * this->RayleighExponentialDistribution);
|
|
if (rayleighExponentialDistribution !=
|
|
this->SkyAtmosphere->RayleighExponentialDistribution) {
|
|
this->SkyAtmosphere->SetRayleighExponentialDistribution(
|
|
rayleighExponentialDistribution);
|
|
}
|
|
|
|
float mieExponentialDistribution =
|
|
float(scale * this->MieExponentialDistribution);
|
|
if (mieExponentialDistribution !=
|
|
this->SkyAtmosphere->MieExponentialDistribution) {
|
|
this->SkyAtmosphere->SetMieExponentialDistribution(
|
|
mieExponentialDistribution);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::PostLoad() {
|
|
Super::PostLoad();
|
|
|
|
// For backward compatibility, copy the value of the deprecated Georeference
|
|
// property to its new home in the GlobeAnchor. It doesn't appear to be
|
|
// possible to do this in Serialize:
|
|
// https://udn.unrealengine.com/s/question/0D54z00007CAbHFCA1/backward-compatibile-serialization-for-uobject-pointers
|
|
const int32 CesiumVersion =
|
|
this->GetLinkerCustomVersion(FCesiumCustomVersion::GUID);
|
|
if (CesiumVersion < FCesiumCustomVersion::GeoreferenceRefactoring) {
|
|
if (this->Georeference_DEPRECATED != nullptr && this->GlobeAnchor &&
|
|
this->GlobeAnchor->GetGeoreference() == nullptr) {
|
|
this->GlobeAnchor->SetGeoreference(this->Georeference_DEPRECATED);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ACesiumSunSky::ShouldTickIfViewportsOnly() const { return true; }
|
|
|
|
void ACesiumSunSky::_setSkyAtmosphereVisibility(bool bVisible) {
|
|
if (IsValid(SkyLight)) {
|
|
SkyLight->SetVisibility(bVisible);
|
|
}
|
|
if (IsValid(SkyAtmosphere)) {
|
|
SkyAtmosphere->SetVisibility(bVisible);
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::_setSkySphereDirectionalLight() {
|
|
if (!UseMobileRendering || !SkySphereClass || !IsValid(SkySphereActor)) {
|
|
return;
|
|
}
|
|
|
|
for (TFieldIterator<FProperty> PropertyIterator(SkySphereClass);
|
|
PropertyIterator;
|
|
++PropertyIterator) {
|
|
FProperty* prop = *PropertyIterator;
|
|
FName const propName = prop->GetFName();
|
|
if (propName == TEXT("DirectionalLightComponent")) {
|
|
FObjectProperty* objectProp = CastField<FObjectProperty>(prop);
|
|
if (objectProp) {
|
|
UDirectionalLightComponent* directionalLightComponent = nullptr;
|
|
if (UseLevelDirectionalLight) {
|
|
// Getting the component from a DirectionalLight actor is editor-only
|
|
#if WITH_EDITORONLY_DATA
|
|
directionalLightComponent = LevelDirectionalLight->GetComponent();
|
|
#endif
|
|
} else {
|
|
directionalLightComponent = DirectionalLight;
|
|
}
|
|
objectProp->SetPropertyValue_InContainer(
|
|
this->SkySphereActor,
|
|
directionalLightComponent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
void ACesiumSunSky::PostEditChangeProperty(
|
|
FPropertyChangedEvent& PropertyChangedEvent) {
|
|
|
|
const FName propertyName = (PropertyChangedEvent.Property)
|
|
? PropertyChangedEvent.Property->GetFName()
|
|
: NAME_None;
|
|
if (propertyName == GET_MEMBER_NAME_CHECKED(ACesiumSunSky, SkySphereClass)) {
|
|
_wantsSpawnMobileSkySphere = true;
|
|
if (SkySphereActor) {
|
|
SkySphereActor->Destroy();
|
|
}
|
|
}
|
|
if (propertyName ==
|
|
GET_MEMBER_NAME_CHECKED(ACesiumSunSky, UseMobileRendering)) {
|
|
_wantsSpawnMobileSkySphere = UseMobileRendering;
|
|
_setSkyAtmosphereVisibility(!UseMobileRendering);
|
|
if (!UseMobileRendering && SkySphereActor) {
|
|
SkySphereActor->Destroy();
|
|
}
|
|
}
|
|
if (propertyName ==
|
|
GET_MEMBER_NAME_CHECKED(ACesiumSunSky, UseLevelDirectionalLight) ||
|
|
propertyName ==
|
|
GET_MEMBER_NAME_CHECKED(ACesiumSunSky, LevelDirectionalLight)) {
|
|
_setSkySphereDirectionalLight();
|
|
if (IsValid(LevelDirectionalLight)) {
|
|
LevelDirectionalLight->GetComponent()->SetAtmosphereSunLight(true);
|
|
LevelDirectionalLight->GetComponent()->MarkRenderStateDirty();
|
|
}
|
|
}
|
|
// Place superclass method after variables are updated, so that a new sky
|
|
// sphere can be spawned if needed
|
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
|
}
|
|
#endif
|
|
|
|
ACesiumGeoreference* ACesiumSunSky::GetGeoreference() const {
|
|
if (!IsValid(this->GlobeAnchor)) {
|
|
UE_LOG(
|
|
LogCesium,
|
|
Error,
|
|
TEXT("ACesiumSunSky %s does not have a GlobeAnchorComponent"),
|
|
*this->GetName());
|
|
return nullptr;
|
|
}
|
|
return this->GlobeAnchor->ResolveGeoreference();
|
|
}
|
|
|
|
void ACesiumSunSky::UpdateSun_Implementation() {
|
|
// Put the Sky Light at the Georeference origin.
|
|
// TODO: should it follow the player?
|
|
this->SkyLight->SetUsingAbsoluteLocation(true);
|
|
this->SkyLight->SetWorldLocation(FVector(0, 0, 0));
|
|
|
|
bool isDST = this->IsDST(
|
|
this->UseDaylightSavingTime,
|
|
this->DSTStartMonth,
|
|
this->DSTStartDay,
|
|
this->DSTEndMonth,
|
|
this->DSTEndDay,
|
|
this->DSTSwitchHour);
|
|
|
|
int32 hours, minutes, seconds;
|
|
this->GetHMSFromSolarTime(this->SolarTime, hours, minutes, seconds);
|
|
|
|
FSunPositionData sunPosition;
|
|
USunPositionFunctionLibrary::GetSunPosition(
|
|
this->GetGeoreference()->GetOriginLatitude(),
|
|
this->GetGeoreference()->GetOriginLongitude(),
|
|
this->TimeZone,
|
|
isDST,
|
|
this->Year,
|
|
this->Month,
|
|
this->Day,
|
|
hours,
|
|
minutes,
|
|
seconds,
|
|
sunPosition);
|
|
|
|
this->Elevation = sunPosition.Elevation - 180.0f;
|
|
this->CorrectedElevation = sunPosition.CorrectedElevation - 180.0f;
|
|
this->Azimuth = sunPosition.Azimuth;
|
|
|
|
FRotator newRotation(
|
|
-this->CorrectedElevation,
|
|
180.0f + (this->Azimuth + this->NorthOffset),
|
|
0.0f);
|
|
|
|
FTransform transform{};
|
|
USceneComponent* pRootComponent = this->GetRootComponent();
|
|
if (IsValid(pRootComponent)) {
|
|
USceneComponent* pParent = pRootComponent->GetAttachParent();
|
|
if (IsValid(pParent)) {
|
|
transform = pParent->GetComponentToWorld();
|
|
}
|
|
}
|
|
|
|
FQuat worldRotation = transform.TransformRotation(newRotation.Quaternion());
|
|
|
|
// Orient sun / directional light
|
|
if (this->UseLevelDirectionalLight && IsValid(this->LevelDirectionalLight) &&
|
|
IsValid(this->LevelDirectionalLight->GetRootComponent())) {
|
|
this->LevelDirectionalLight->GetRootComponent()->SetWorldRotation(
|
|
worldRotation);
|
|
} else {
|
|
this->DirectionalLight->SetWorldRotation(worldRotation);
|
|
}
|
|
|
|
// Mobile only
|
|
this->UpdateSkySphere();
|
|
}
|
|
|
|
namespace {
|
|
|
|
FVector getViewLocation(UWorld* pWorld) {
|
|
#if WITH_EDITOR
|
|
if (!pWorld->IsGameWorld()) {
|
|
// Grab the location of the active viewport.
|
|
FViewport* pViewport = GEditor->GetActiveViewport();
|
|
|
|
const TArray<FEditorViewportClient*>& viewportClients =
|
|
GEditor->GetAllViewportClients();
|
|
for (FEditorViewportClient* pEditorViewportClient : viewportClients) {
|
|
if (pEditorViewportClient && pViewport &&
|
|
pEditorViewportClient == pViewport->GetClient()) {
|
|
return pEditorViewportClient->GetViewLocation();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Get the player's current globe location.
|
|
APawn* pPawn = UGameplayStatics::GetPlayerPawn(pWorld, 0);
|
|
if (pPawn) {
|
|
return pPawn->GetActorLocation();
|
|
}
|
|
|
|
return FVector(0.0f, 0.0f, 0.0f);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ACesiumSunSky::UpdateAtmosphereRadius() {
|
|
UWorld* pWorld = this->GetWorld();
|
|
if (!IsValid(pWorld)) {
|
|
UE_LOG(
|
|
LogCesium,
|
|
Error,
|
|
TEXT("ACesiumSunSky %s GetWorld() returned nullptr"),
|
|
*this->GetName());
|
|
return;
|
|
}
|
|
|
|
// This Actor is located at the center of the Earth (the CesiumGlobeAnchor
|
|
// keeps it there), so we ignore this Actor's transform and use only its
|
|
// parent transform.
|
|
FTransform transform{};
|
|
USceneComponent* pRootComponent = this->GetRootComponent();
|
|
if (IsValid(pRootComponent)) {
|
|
USceneComponent* pParent = pRootComponent->GetAttachParent();
|
|
if (IsValid(pParent)) {
|
|
transform = pParent->GetComponentToWorld().Inverse();
|
|
}
|
|
}
|
|
|
|
ACesiumGeoreference* pGeoreference = this->GetGeoreference();
|
|
if (!IsValid(pGeoreference)) {
|
|
UE_LOG(
|
|
LogCesium,
|
|
Error,
|
|
TEXT("ACesiumSunSky %s can't find an ACesiumGeoreference"),
|
|
*this->GetName());
|
|
return;
|
|
}
|
|
|
|
UCesiumEllipsoid* pEllipsoid = pGeoreference->GetEllipsoid();
|
|
check(IsValid(pEllipsoid));
|
|
|
|
FVector location = transform.TransformPosition(getViewLocation(pWorld));
|
|
FVector llh =
|
|
pGeoreference->TransformUnrealPositionToLongitudeLatitudeHeight(location);
|
|
|
|
// An atmosphere of this radius should circumscribe all Earth terrain.
|
|
double maxRadius =
|
|
pEllipsoid->GetMaximumRadius() + this->CircumscribedGroundHeight * 1000.0;
|
|
|
|
if (llh.Z / 1000.0 > this->CircumscribedGroundThreshold) {
|
|
this->SetSkyAtmosphereGroundRadius(
|
|
this->SkyAtmosphere,
|
|
maxRadius * this->_computeScale() / 1000.0);
|
|
} else {
|
|
// Find the ellipsoid radius 100m below the surface at this location. See
|
|
// the comment at the top of this file.
|
|
glm::dvec3 ecef = pEllipsoid->GetNativeEllipsoid().cartographicToCartesian(
|
|
CesiumGeospatial::Cartographic::fromDegrees(llh.X, llh.Y, -100.0));
|
|
double minRadius = glm::length(ecef);
|
|
|
|
if (llh.Z / 1000.0 < this->InscribedGroundThreshold) {
|
|
this->SetSkyAtmosphereGroundRadius(
|
|
this->SkyAtmosphere,
|
|
minRadius * this->_computeScale() / 1000.0);
|
|
} else {
|
|
double t =
|
|
((llh.Z / 1000.0) - this->InscribedGroundThreshold) /
|
|
(this->CircumscribedGroundThreshold - this->InscribedGroundThreshold);
|
|
double radius = glm::mix(minRadius, maxRadius, t);
|
|
this->SetSkyAtmosphereGroundRadius(
|
|
this->SkyAtmosphere,
|
|
radius * this->_computeScale() / 1000.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACesiumSunSky::EstimateTimeZoneForLongitude(double InLongitude) {
|
|
this->TimeZone = FMath::Clamp(InLongitude, -180.0, 180.0) / 15.0;
|
|
this->UpdateSun();
|
|
}
|
|
|
|
void ACesiumSunSky::GetHMSFromSolarTime(
|
|
double InSolarTime,
|
|
int32& Hour,
|
|
int32& Minute,
|
|
int32& Second) {
|
|
Hour = FMath::TruncToInt(InSolarTime) % 24;
|
|
Minute = FMath::TruncToInt((InSolarTime - Hour) * 60) % 60;
|
|
|
|
// Convert hours + minutes so far to seconds, and subtract from InSolarTime.
|
|
Second = FMath::RoundToInt((InSolarTime - Hour - Minute / 60) * 3600) % 60;
|
|
}
|
|
|
|
bool ACesiumSunSky::IsDST(
|
|
bool DSTEnable,
|
|
int32 InDSTStartMonth,
|
|
int32 InDSTStartDay,
|
|
int32 InDSTEndMonth,
|
|
int32 InDSTEndDay,
|
|
int32 InDSTSwitchHour) const {
|
|
if (!DSTEnable) {
|
|
return false;
|
|
}
|
|
int32 hour, minute, second;
|
|
this->GetHMSFromSolarTime(SolarTime, hour, minute, second);
|
|
|
|
// Editor will crash if we create an invalid FDateTime, so validate these
|
|
// settings first
|
|
if (!FDateTime::Validate(Year, Month, Day, hour, minute, second, 0)) {
|
|
return false;
|
|
}
|
|
FDateTime current = FDateTime(Year, Month, Day, hour, minute, second);
|
|
FDateTime dstStart =
|
|
FDateTime(Year, InDSTStartMonth, InDSTStartDay, InDSTSwitchHour);
|
|
FDateTime dstEnd =
|
|
FDateTime(Year, InDSTEndMonth, InDSTEndDay, InDSTSwitchHour);
|
|
return current >= dstStart && current <= dstEnd;
|
|
}
|
|
|
|
void ACesiumSunSky::SetSkyAtmosphereGroundRadius(
|
|
USkyAtmosphereComponent* Sky,
|
|
double Radius) {
|
|
// Only update if there's a significant change to be made
|
|
float radiusFloat = float(Radius);
|
|
if (Sky && !FMath::IsNearlyEqualByULP(radiusFloat, Sky->BottomRadius)) {
|
|
Sky->BottomRadius = radiusFloat;
|
|
Sky->MarkRenderStateDirty();
|
|
UE_LOG(LogCesium, Verbose, TEXT("GroundRadius now %f"), Sky->BottomRadius);
|
|
}
|
|
}
|