#include "Render/DescentEnvironment.h" #include "Render/DescentLightPool.h" #include "Core/DescentBoot.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentFlyPawn.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/Tuning/DescentTune_Look.h" #include "Debug/DescentStateRegistry.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Camera/PlayerCameraManager.h" #include "Components/DirectionalLightComponent.h" #include "Components/ExponentialHeightFogComponent.h" #include "Components/PostProcessComponent.h" #include "Engine/World.h" #include "EngineUtils.h" #include "GameFramework/PlayerController.h" #include "HAL/IConsoleManager.h" #include "Materials/MaterialInterface.h" #include "Materials/MaterialParameterCollection.h" #include "Materials/MaterialParameterCollectionInstance.h" namespace LTune = Descent::Tune::Look; static TAutoConsoleVariable GLookCvarExposure(TEXT("descent.Look.ExposureEV"), LTune::ExposureEvDefault, TEXT("Manual exposure bias in EV (DESIGN section 7.6). Calibrated once by the look_calibrate scenario.")); static TAutoConsoleVariable GLookCvarBloom(TEXT("descent.Look.Bloom"), 1.0f, TEXT("Bloom strength multiplier, 0..2 (requirement U12: the browser version's bloom was too much). 0 = off.")); ADescentEnvironment::ADescentEnvironment() { PrimaryActorTick.bCanEverTick = false; USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); SetRootComponent(Root); // Volumetric fog only: it exists to give the player's own beam something to scatter in. Its black in-scattering colour and // low density keep its own haze small so it does not double the post-process water (section 7.2). Fog = CreateDefaultSubobject(TEXT("Fog")); Fog->SetupAttachment(Root); Fog->SetFogDensity(LTune::VolFogDensity); Fog->SetFogHeightFalloff(LTune::VolFogHeightFalloff); Fog->SetFogInscatteringColor(FLinearColor::Black); Fog->SetFogMaxOpacity(1.f); Fog->SetVolumetricFog(true); Fog->SetVolumetricFogExtinctionScale(LTune::VolFogExtinctionScale); Fog->SetVolumetricFogAlbedo(FColor(140, 215, 225, 255)); Fog->SetVolumetricFogScatteringDistribution(LTune::VolFogScatteringDistribution); Fog->SetVolumetricFogDistance(LTune::VolFogDistanceUU); Fog->SetVolumetricFogStartDistance(LTune::VolFogStartSwimUU); PostProcess = CreateDefaultSubobject(TEXT("PostProcess")); PostProcess->SetupAttachment(Root); PostProcess->bUnbound = true; PostProcess->BlendWeight = 1.f; FPostProcessSettings& PP = PostProcess->Settings; PP.bOverride_AutoExposureMethod = true; PP.AutoExposureMethod = AEM_Manual; PP.bOverride_AutoExposureApplyPhysicalCameraExposure = true; PP.AutoExposureApplyPhysicalCameraExposure = false; PP.bOverride_AutoExposureBias = true; PP.AutoExposureBias = LTune::ExposureEvDefault; PP.bOverride_BloomMethod = true; PP.BloomMethod = BM_SOG; PP.bOverride_BloomIntensity = true; PP.BloomIntensity = LTune::BloomIntensity; PP.bOverride_BloomThreshold = true; PP.BloomThreshold = LTune::BloomThreshold; PP.bOverride_BloomSizeScale = true; PP.BloomSizeScale = LTune::BloomSizeScale; PP.bOverride_BloomDirtMaskIntensity = true; PP.BloomDirtMaskIntensity = 0.f; PP.bOverride_LensFlareIntensity = true; PP.LensFlareIntensity = 0.f; PP.bOverride_VignetteIntensity = true; PP.VignetteIntensity = LTune::VignetteIntensity; PP.bOverride_FilmGrainIntensity = true; PP.FilmGrainIntensity = LTune::FilmGrainIntensity; PP.bOverride_SceneFringeIntensity = true; PP.SceneFringeIntensity = LTune::SceneFringeIntensity; PP.bOverride_AmbientOcclusionIntensity = true; PP.AmbientOcclusionIntensity = LTune::AoIntensity; PP.bOverride_AmbientOcclusionRadius = true; PP.AmbientOcclusionRadius = LTune::AoRadiusUU; PP.bOverride_MotionBlurAmount = true; PP.MotionBlurAmount = 0.f; Sun = CreateDefaultSubobject(TEXT("Sun")); Sun->SetupAttachment(Root); Sun->SetMobility(EComponentMobility::Movable); Sun->SetLightColor(FLinearColor::FromSRGBColor(FColor::FromHex(TEXT("9fd8d0")))); Sun->SetCastShadows(true); Sun->SetVolumetricScatteringIntensity(static_cast(LTune::SunVolScatter)); Sun->bAtmosphereSunLight = false; Sun->SetIntensity(0.f); Sun->SetVisibility(false); LightPool = CreateDefaultSubobject(TEXT("LightPool")); } ADescentEnvironment* ADescentEnvironment::Get(const UWorld* World) { if (!World) { return nullptr; } for (TActorIterator It(const_cast(World)); It; ++It) { return *It; } return nullptr; } double ADescentEnvironment::SurfaceLightAt(double Z) { return Descent::Clamp(FMath::Exp(-(LTune::SurfaceZ - Z) / LTune::SurfaceScaleM), 0.0, 1.0); } void ADescentEnvironment::BeginPlay() { Super::BeginPlay(); Mpc = LoadObject(nullptr, TEXT("/Game/Materials/MPC_Descent.MPC_Descent"), nullptr, LOAD_NoWarn); if (!Mpc) { UE_LOG(LogDescent, Warning, TEXT("Environment: /Game/Materials/MPC_Descent is missing (run Tools/import_assets.ps1); materials keep their defaults")); } if (UMaterialInterface* Water = LoadObject(nullptr, TEXT("/Game/Materials/PP_Water.PP_Water"), nullptr, LOAD_NoWarn)) { PostProcess->Settings.AddBlendable(Water, 1.f); bBlendableAdded = true; } else { UE_LOG(LogDescent, Warning, TEXT("Environment: /Game/Materials/PP_Water is missing (run Tools/import_assets.ps1); there will be no water")); } if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Environment"); EnvHandle = Sim->RegisterLateStep(EDescentLateStep::Environment, [this](const FDescentSimFrame& F) { UpdateLook(F.Dt); }, Opt); Opt.Owner = TEXT("LightPool"); PoolHandle = Sim->RegisterLateStep(EDescentLateStep::LightPool, [this](const FDescentSimFrame&) { CommitLights(); }, Opt); } UpdateLook(0.0); } void ADescentEnvironment::EndPlay(const EEndPlayReason::Type Reason) { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(EnvHandle); Sim->Unregister(PoolHandle); } Super::EndPlay(Reason); } void ADescentEnvironment::UpdateLook(double Dt) { UWorld* World = GetWorld(); APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; FVector Cam = LastCameraUU; if (PC && PC->PlayerCameraManager) { Cam = PC->PlayerCameraManager->GetCameraLocation(); } LastCameraUU = Cam; const double Zm = Descent::UUToMetres(Cam.Z); SurfaceLight = SurfaceLightAt(Zm); const double L = SurfaceLight; FogDensityValue = LTune::FogBase + LTune::FogLightGain * (1.0 - L); EnvState = Descent::IsDry(World, Cam) ? EDescentEnv::Dry : EDescentEnv::Water; TemperatureC = Descent::AmbientTemperatureAt(World, Cam); // Volumetric fog: on in the sea, faded out over 0.3 s in dry air (section 7.4). const double Target = (EnvState == EDescentEnv::Water && bVolumetricSwitch) ? 1.0 : 0.0; if (Dt <= 0.0) { VolBlend = Target; } else { VolBlend = Descent::Clamp(VolBlend + FMath::Sign(Target - VolBlend) * Dt / LTune::VolFogBlendSec, 0.0, 1.0) ; if (FMath::Abs(Target - VolBlend) < 1e-6) { VolBlend = Target; } } bVolumetricOn = VolBlend > 0.001; Fog->SetVolumetricFog(bVolumetricOn); Fog->SetFogDensity(static_cast(LTune::VolFogDensity * VolBlend)); // Sun: only near the top of the Throat. bSunOn = L >= LTune::SunHiddenBelowL; Sun->SetVisibility(bSunOn); Sun->SetIntensity(static_cast(LTune::SunIntensityPerLight * L)); const FVector SunDir = FVector(LTune::SunDirX, LTune::SunDirY, LTune::SunDirZ).GetSafeNormal(); Sun->SetWorldRotation((-SunDir).Rotation()); // Post process: manual exposure and the reworked bloom come from the two cvars. FPostProcessSettings& PP = PostProcess->Settings; PP.AutoExposureBias = GLookCvarExposure.GetValueOnGameThread(); PP.BloomIntensity = LTune::BloomIntensity * FMath::Clamp(GLookCvarBloom.GetValueOnGameThread(), 0.f, 2.f); // Marine snow is not built in this round: nothing is visible, whatever the switch says. SnowInstancesVisible = 0; // MPC_Descent (section 7.1). if (Mpc && World) { UMaterialParameterCollectionInstance* I = World->GetParameterCollectionInstance(Mpc); const UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(World); const double Light = 0.015 + 1.25 * L; // (0.015 + 1.25 L): 0.5 percent of albedo at depth I->SetScalarParameterValue(TEXT("Time"), Sim ? static_cast(Sim->SimTime()) : 0.f); I->SetScalarParameterValue(TEXT("SurfaceLight"), static_cast(L)); I->SetScalarParameterValue(TEXT("FogDensity"), static_cast(FogDensityValue)); I->SetScalarParameterValue(TEXT("FloorGlow"), static_cast(1.2 * Descent::Smooth(-1300.0, -1640.0, Zm))); I->SetScalarParameterValue(TEXT("WaterAmount"), static_cast(WaterAmount)); I->SetScalarParameterValue(TEXT("SnowAmount"), bSnowSwitch ? 1.f : 0.f); I->SetScalarParameterValue(TEXT("DryCamera"), EnvState == EDescentEnv::Dry ? 1.f : 0.f); I->SetVectorParameterValue(TEXT("WaterTop"), FLinearColor(LTune::WaterTopR, LTune::WaterTopG, LTune::WaterTopB)); I->SetVectorParameterValue(TEXT("WaterDeep"), FLinearColor(LTune::WaterDeepR, LTune::WaterDeepG, LTune::WaterDeepB)); I->SetVectorParameterValue(TEXT("SunDir"), FLinearColor(static_cast(SunDir.X), static_cast(SunDir.Y), static_cast(SunDir.Z))); const float Sky = static_cast(Light / UE_DOUBLE_PI); I->SetVectorParameterValue(TEXT("AmbientWaterSky"), FLinearColor((LTune::AmbientSkyR + 0.1f * static_cast(L)) * Sky, (LTune::AmbientSkyG + 0.1f * static_cast(L)) * Sky, LTune::AmbientSkyB * Sky)); I->SetVectorParameterValue(TEXT("AmbientWaterGround"), FLinearColor(LTune::AmbientGroundR * Sky, LTune::AmbientGroundG * Sky, LTune::AmbientGroundB * Sky)); I->SetVectorParameterValue(TEXT("AmbientDry"), FLinearColor(LTune::AmbientDryR * static_cast(LTune::AmbientDryLevel), LTune::AmbientDryG * static_cast(LTune::AmbientDryLevel), LTune::AmbientDryB * static_cast(LTune::AmbientDryLevel))); } } void ADescentEnvironment::CommitLights() { if (LightPool) { LightPool->Commit(LastCameraUU); } } bool ADescentEnvironment::SetLookParam(const FString& Name, double Value, FString& OutError) { if (Name.Equals(TEXT("Exposure"), ESearchCase::IgnoreCase)) { if (IConsoleVariable* V = IConsoleManager::Get().FindConsoleVariable(TEXT("descent.Look.ExposureEV"))) { V->Set(static_cast(Value), ECVF_SetByConsole); } } else if (Name.Equals(TEXT("Bloom"), ESearchCase::IgnoreCase)) { if (IConsoleVariable* V = IConsoleManager::Get().FindConsoleVariable(TEXT("descent.Look.Bloom"))) { V->Set(static_cast(Value), ECVF_SetByConsole); } } else if (Name.Equals(TEXT("WaterPP"), ESearchCase::IgnoreCase)) { WaterAmount = FMath::Clamp(Value, 0.0, 1.0); } else if (Name.Equals(TEXT("Volumetric"), ESearchCase::IgnoreCase)) { bVolumetricSwitch = Value > 0.5; } else if (Name.Equals(TEXT("Snow"), ESearchCase::IgnoreCase)) { bSnowSwitch = Value > 0.5; } else if (Name.Equals(TEXT("Lamp"), ESearchCase::IgnoreCase)) { for (TActorIterator It(GetWorld()); It; ++It) { It->SetHeadlamp(Value > 0.5); } } else { OutError = FString::Printf(TEXT("unknown look parameter '%s' (Exposure, Bloom, WaterPP, Volumetric, Snow, Lamp)"), *Name); return false; } UpdateLook(0.0); return true; } // Boot (section 1.5 step 2): the environment exists before anything is drawn. DESCENT_BOOT_STEP(SpawnEnvironment, Descent::Boot::Order::SpawnEnvironment) { if (!Ctx.World) { Ctx.Fail(TEXT("no world")); return; } FActorSpawnParameters Params; Params.Name = TEXT("DescentEnvironment"); if (!Ctx.World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, Params)) { Ctx.Fail(TEXT("could not spawn the environment actor")); } } // State section "env" (section 9.4). static FDescentStateProviderRegistrar GEnvSection(TEXT("env"), [](UWorld* World, FJsonObject& Out) { const ADescentEnvironment* E = ADescentEnvironment::Get(World); Out.SetBoolField(TEXT("available"), E != nullptr); if (!E) { return; } Out.SetStringField(TEXT("state"), E->GetEnvState() == EDescentEnv::Dry ? TEXT("Dry") : TEXT("Water")); Out.SetNumberField(TEXT("surface_light"), Descent::Json::Round(E->SurfaceLight)); Out.SetNumberField(TEXT("fog_density"), Descent::Json::Round(E->FogDensityValue)); Out.SetBoolField(TEXT("volumetric_fog"), E->bVolumetricOn); Out.SetNumberField(TEXT("exposure_ev"), Descent::Json::Round(GLookCvarExposure.GetValueOnGameThread())); Out.SetNumberField(TEXT("bloom_intensity"), Descent::Json::Round(LTune::BloomIntensity * GLookCvarBloom.GetValueOnGameThread())); Out.SetNumberField(TEXT("bloom_threshold"), Descent::Json::Round(LTune::BloomThreshold)); Out.SetNumberField(TEXT("snow_instances_visible"), E->SnowInstancesVisible); Out.SetNumberField(TEXT("temperature_c"), Descent::Json::Round(E->TemperatureC)); Out.SetBoolField(TEXT("sun_on"), E->bSunOn); Out.SetNumberField(TEXT("water_pp"), Descent::Json::Round(E->WaterAmount)); Out.SetNumberField(TEXT("light_pool_lit"), E->GetLightPool() ? E->GetLightPool()->NumLit() : 0); });