Add first pass sky lighting
This commit is contained in:
@@ -435,7 +435,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
||||
- [x] Add weather save/load support. Added `LoadCurrentWorld` as the unified persistence load path, restored weather/time before players and world actors, updated the admin load command to use the combined path, and extended the persistence smoke test to prove provider-backed weather metadata survives save/load.
|
||||
- [x] Connect weather to body temperature.
|
||||
- [x] Connect shelter to weather protection. Survival now calculates the best overlapping shelter protection volume, replicates current weather protection, reduces ambient exposure and cold damage by shelter coverage, trends care-history shelter quality toward active protection, and shows shelter protection on the dev HUD.
|
||||
- [ ] Add first-pass sky and lighting.
|
||||
- [x] Add first-pass sky and lighting. Added `AAgrarianSkyLightingController` with movable sun, skylight, and fog components driven by replicated time, local sunrise/sunset, weather state, and provider cloud cover; updated the Ground Zero setup script to place the controller and remove legacy static lighting actors.
|
||||
- [ ] Add audio cues for weather.
|
||||
|
||||
## 0.1.D Single Biome MVP Map
|
||||
|
||||
Binary file not shown.
@@ -155,6 +155,15 @@ percentage, and trend the care-history shelter quality field toward the active
|
||||
protection level. The dev HUD shows current shelter protection so weather
|
||||
pressure can be tuned during MVP tests.
|
||||
|
||||
First-pass sky and lighting use `AAgrarianSkyLightingController`. The controller
|
||||
owns movable sun, skylight, and exponential-height-fog components and reads the
|
||||
replicated `AAgrarianGameState` time, active tile sunrise/sunset, weather state,
|
||||
and mapped cloud cover. It adjusts sun pitch, sun intensity/color, sky-light
|
||||
intensity, and fog density every tick so the Ground Zero demo visually tracks
|
||||
the represented local day/night cycle and current weather without hard-coded
|
||||
static light settings. The Ground Zero map setup script places this controller
|
||||
and removes the earlier static demo sun/skylight/fog actors.
|
||||
|
||||
The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
|
||||
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
|
||||
current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
|
||||
|
||||
@@ -68,25 +68,11 @@ DEMO_ACTORS = [
|
||||
"rotation": unreal.Rotator(0.0, 135.0, 0.0),
|
||||
},
|
||||
{
|
||||
"label": "AGR_DemoSun",
|
||||
"class": unreal.DirectionalLight,
|
||||
"location_xy": unreal.Vector(-22000.0, -9000.0, 0.0),
|
||||
"fixed_z": 35000.0,
|
||||
"rotation": unreal.Rotator(-42.0, -35.0, 0.0),
|
||||
},
|
||||
{
|
||||
"label": "AGR_DemoSkyLight",
|
||||
"class": unreal.SkyLight,
|
||||
"label": "AGR_DemoSkyLightingController",
|
||||
"class": unreal.AgrarianSkyLightingController,
|
||||
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
|
||||
"fixed_z": 12000.0,
|
||||
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
|
||||
},
|
||||
{
|
||||
"label": "AGR_DemoFog",
|
||||
"class": unreal.ExponentialHeightFog,
|
||||
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
|
||||
"fixed_z": 4000.0,
|
||||
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
|
||||
"rotation": unreal.Rotator(-42.0, -35.0, 0.0),
|
||||
},
|
||||
{
|
||||
"label": "AGR_DemoNoticeActor",
|
||||
@@ -97,6 +83,12 @@ DEMO_ACTORS = [
|
||||
},
|
||||
]
|
||||
|
||||
LEGACY_DEMO_LIGHTING_LABELS = {
|
||||
"AGR_DemoSun",
|
||||
"AGR_DemoSkyLight",
|
||||
"AGR_DemoFog",
|
||||
}
|
||||
|
||||
|
||||
BIOME_RESOURCE_ACTORS = [
|
||||
{
|
||||
@@ -330,7 +322,7 @@ def spawn_foliage_actor(height_values):
|
||||
reserved_points = [
|
||||
spec["location_xy"]
|
||||
for spec in DEMO_ACTORS
|
||||
if spec["label"] not in {"AGR_DemoSun", "AGR_DemoSkyLight", "AGR_DemoFog", "AGR_DemoNoticeActor"}
|
||||
if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoNoticeActor"}
|
||||
]
|
||||
|
||||
foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
|
||||
@@ -405,6 +397,7 @@ def main():
|
||||
raise RuntimeError(f"Could not load map: {MAP_PATH}")
|
||||
|
||||
labels = {spec["label"] for spec in DEMO_ACTORS}
|
||||
labels.update(LEGACY_DEMO_LIGHTING_LABELS)
|
||||
labels.update(spec["label"] for spec in BIOME_RESOURCE_ACTORS)
|
||||
labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS)
|
||||
labels.add(FOLIAGE_LABEL)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKY_H = ROOT / "Source" / "AgrarianGame" / "AgrarianSkyLightingController.h"
|
||||
SKY_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianSkyLightingController.cpp"
|
||||
MAP_SETUP = ROOT / "Scripts" / "setup_ground_zero_demo_map.py"
|
||||
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||
|
||||
|
||||
EXPECTED = {
|
||||
SKY_H: [
|
||||
"class AAgrarianSkyLightingController : public AActor",
|
||||
"TObjectPtr<UDirectionalLightComponent> SunLight;",
|
||||
"TObjectPtr<USkyLightComponent> SkyLight;",
|
||||
"TObjectPtr<UExponentialHeightFogComponent> HeightFog;",
|
||||
"void RefreshSkyLighting();",
|
||||
"float CalculateSunAlpha",
|
||||
"float CalculateWeatherCloudAlpha",
|
||||
],
|
||||
SKY_CPP: [
|
||||
"#include \"AgrarianGameState.h\"",
|
||||
"SunLight = CreateDefaultSubobject<UDirectionalLightComponent>",
|
||||
"SkyLight = CreateDefaultSubobject<USkyLightComponent>",
|
||||
"HeightFog = CreateDefaultSubobject<UExponentialHeightFogComponent>",
|
||||
"GameState->SunriseHourLocal",
|
||||
"GameState->SunsetHourLocal",
|
||||
"GameState->ActiveWeatherInputs.CloudCoverPercent",
|
||||
"SunLight->SetWorldRotation",
|
||||
"SunLight->SetIntensity",
|
||||
"SkyLight->SetIntensity",
|
||||
"HeightFog->SetFogDensity",
|
||||
],
|
||||
MAP_SETUP: [
|
||||
"AGR_DemoSkyLightingController",
|
||||
"unreal.AgrarianSkyLightingController",
|
||||
"LEGACY_DEMO_LIGHTING_LABELS",
|
||||
"\"AGR_DemoSun\"",
|
||||
"\"AGR_DemoSkyLight\"",
|
||||
"\"AGR_DemoFog\"",
|
||||
],
|
||||
TDD: [
|
||||
"`AAgrarianSkyLightingController`",
|
||||
"movable sun, skylight, and exponential-height-fog components",
|
||||
],
|
||||
ROADMAP: [
|
||||
"[x] Add first-pass sky and lighting.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
missing = []
|
||||
for path, snippets in EXPECTED.items():
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for snippet in snippets:
|
||||
if snippet not in text:
|
||||
missing.append(f"{path.relative_to(ROOT)}: {snippet}")
|
||||
if missing:
|
||||
raise RuntimeError("Sky lighting controller verification failed: " + "; ".join(missing))
|
||||
print("Agrarian sky lighting controller verification complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright Pacificao. All Rights Reserved.
|
||||
|
||||
#include "AgrarianSkyLightingController.h"
|
||||
|
||||
#include "AgrarianGameState.h"
|
||||
#include "Components/DirectionalLightComponent.h"
|
||||
#include "Components/ExponentialHeightFogComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Components/SkyLightComponent.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
AAgrarianSkyLightingController::AAgrarianSkyLightingController()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
bReplicates = false;
|
||||
|
||||
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
|
||||
RootComponent = SceneRoot;
|
||||
|
||||
SunLight = CreateDefaultSubobject<UDirectionalLightComponent>(TEXT("SunLight"));
|
||||
SunLight->SetupAttachment(SceneRoot);
|
||||
SunLight->SetIntensity(NoonSunIntensity);
|
||||
SunLight->SetLightColor(FLinearColor(1.0f, 0.96f, 0.86f));
|
||||
SunLight->SetMobility(EComponentMobility::Movable);
|
||||
|
||||
SkyLight = CreateDefaultSubobject<USkyLightComponent>(TEXT("SkyLight"));
|
||||
SkyLight->SetupAttachment(SceneRoot);
|
||||
SkyLight->SetIntensity(ClearSkyLightIntensity);
|
||||
SkyLight->SetMobility(EComponentMobility::Movable);
|
||||
|
||||
HeightFog = CreateDefaultSubobject<UExponentialHeightFogComponent>(TEXT("HeightFog"));
|
||||
HeightFog->SetupAttachment(SceneRoot);
|
||||
HeightFog->SetFogDensity(ClearFogDensity);
|
||||
HeightFog->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
void AAgrarianSkyLightingController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
RefreshSkyLighting();
|
||||
}
|
||||
|
||||
void AAgrarianSkyLightingController::Tick(float DeltaSeconds)
|
||||
{
|
||||
Super::Tick(DeltaSeconds);
|
||||
RefreshSkyLighting();
|
||||
}
|
||||
|
||||
void AAgrarianSkyLightingController::RefreshSkyLighting()
|
||||
{
|
||||
const UWorld* World = GetWorld();
|
||||
const AAgrarianGameState* GameState = World ? World->GetGameState<AAgrarianGameState>() : nullptr;
|
||||
if (!GameState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float SunriseHour = GameState->bHasActiveTileSolarData ? GameState->SunriseHourLocal : 6.0f;
|
||||
const float SunsetHour = GameState->bHasActiveTileSolarData ? GameState->SunsetHourLocal : 20.0f;
|
||||
CurrentSunAlpha = CalculateSunAlpha(GameState->WorldHours, SunriseHour, SunsetHour);
|
||||
CurrentWeather = GameState->Weather;
|
||||
CurrentCloudAlpha = CalculateWeatherCloudAlpha(
|
||||
GameState->Weather,
|
||||
GameState->ActiveWeatherInputs.CloudCoverPercent,
|
||||
GameState->ActiveWeatherInputs.bHasProviderData);
|
||||
|
||||
const float WeatherLightMultiplier = FMath::Lerp(1.0f, 0.35f, CurrentCloudAlpha);
|
||||
const float SunIntensity = FMath::Lerp(NightSunIntensity, NoonSunIntensity, CurrentSunAlpha) * WeatherLightMultiplier;
|
||||
const float SkyIntensity = FMath::Lerp(NightSkyLightIntensity, ClearSkyLightIntensity, CurrentSunAlpha) * FMath::Lerp(1.0f, 0.55f, CurrentCloudAlpha);
|
||||
const float FogDensity = FMath::Lerp(ClearFogDensity, StormFogDensity, CurrentCloudAlpha);
|
||||
const float SunPitch = FMath::Lerp(-8.0f, -72.0f, CurrentSunAlpha);
|
||||
|
||||
if (SunLight)
|
||||
{
|
||||
SunLight->SetWorldRotation(FRotator(SunPitch, NorthYawDegrees, 0.0f));
|
||||
SunLight->SetIntensity(SunIntensity);
|
||||
SunLight->SetLightColor(CalculateSunColor(CurrentSunAlpha, CurrentCloudAlpha));
|
||||
}
|
||||
|
||||
if (SkyLight)
|
||||
{
|
||||
SkyLight->SetIntensity(SkyIntensity);
|
||||
}
|
||||
|
||||
if (HeightFog)
|
||||
{
|
||||
HeightFog->SetFogDensity(FogDensity);
|
||||
}
|
||||
}
|
||||
|
||||
float AAgrarianSkyLightingController::CalculateSunAlpha(float HourOfDay, float SunriseHour, float SunsetHour) const
|
||||
{
|
||||
const float NormalizedHour = FMath::Fmod(HourOfDay + 24.0f, 24.0f);
|
||||
const float SafeSunrise = FMath::Fmod(SunriseHour + 24.0f, 24.0f);
|
||||
const float SafeSunset = FMath::Fmod(SunsetHour + 24.0f, 24.0f);
|
||||
const float DayLength = FMath::Max(0.1f, SafeSunset >= SafeSunrise ? SafeSunset - SafeSunrise : (24.0f - SafeSunrise) + SafeSunset);
|
||||
|
||||
float HoursSinceSunrise = NormalizedHour - SafeSunrise;
|
||||
if (HoursSinceSunrise < 0.0f)
|
||||
{
|
||||
HoursSinceSunrise += 24.0f;
|
||||
}
|
||||
|
||||
if (HoursSinceSunrise > DayLength)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float DayProgress = FMath::Clamp(HoursSinceSunrise / DayLength, 0.0f, 1.0f);
|
||||
return FMath::Clamp(FMath::Sin(PI * DayProgress), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
float AAgrarianSkyLightingController::CalculateWeatherCloudAlpha(EAgrarianWeatherType Weather, float ProviderCloudCoverPercent, bool bHasProviderCloudCover) const
|
||||
{
|
||||
float WeatherAlpha = 0.0f;
|
||||
switch (Weather)
|
||||
{
|
||||
case EAgrarianWeatherType::Rain:
|
||||
WeatherAlpha = 0.65f;
|
||||
break;
|
||||
case EAgrarianWeatherType::ColdWind:
|
||||
WeatherAlpha = 0.45f;
|
||||
break;
|
||||
case EAgrarianWeatherType::Storm:
|
||||
WeatherAlpha = 1.0f;
|
||||
break;
|
||||
default:
|
||||
WeatherAlpha = 0.0f;
|
||||
break;
|
||||
}
|
||||
|
||||
if (bHasProviderCloudCover)
|
||||
{
|
||||
WeatherAlpha = FMath::Max(WeatherAlpha, FMath::Clamp(ProviderCloudCoverPercent / 100.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
return WeatherAlpha;
|
||||
}
|
||||
|
||||
FLinearColor AAgrarianSkyLightingController::CalculateSunColor(float SunAlpha, float CloudAlpha) const
|
||||
{
|
||||
const FLinearColor DawnColor(1.0f, 0.62f, 0.38f);
|
||||
const FLinearColor NoonColor(1.0f, 0.96f, 0.86f);
|
||||
const FLinearColor StormColor(0.52f, 0.58f, 0.66f);
|
||||
const FLinearColor TimeColor = FLinearColor::LerpUsingHSV(DawnColor, NoonColor, FMath::Clamp(SunAlpha, 0.0f, 1.0f));
|
||||
return FLinearColor::LerpUsingHSV(TimeColor, StormColor, FMath::Clamp(CloudAlpha, 0.0f, 1.0f));
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright Pacificao. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "AgrarianTypes.h"
|
||||
#include "AgrarianSkyLightingController.generated.h"
|
||||
|
||||
class UDirectionalLightComponent;
|
||||
class UExponentialHeightFogComponent;
|
||||
class USceneComponent;
|
||||
class USkyLightComponent;
|
||||
|
||||
UCLASS(Blueprintable)
|
||||
class AAgrarianSkyLightingController : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AAgrarianSkyLightingController();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
TObjectPtr<USceneComponent> SceneRoot;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
TObjectPtr<UDirectionalLightComponent> SunLight;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
TObjectPtr<USkyLightComponent> SkyLight;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
TObjectPtr<UExponentialHeightFogComponent> HeightFog;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float NoonSunIntensity = 8.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float NightSunIntensity = 0.03f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float ClearSkyLightIntensity = 1.0f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float NightSkyLightIntensity = 0.08f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float ClearFogDensity = 0.008f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float StormFogDensity = 0.05f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Sky")
|
||||
float NorthYawDegrees = -35.0f;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
float CurrentSunAlpha = 0.0f;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
float CurrentCloudAlpha = 0.0f;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Sky")
|
||||
EAgrarianWeatherType CurrentWeather = EAgrarianWeatherType::Clear;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Agrarian|Sky")
|
||||
void RefreshSkyLighting();
|
||||
|
||||
protected:
|
||||
float CalculateSunAlpha(float HourOfDay, float SunriseHour, float SunsetHour) const;
|
||||
float CalculateWeatherCloudAlpha(EAgrarianWeatherType Weather, float ProviderCloudCoverPercent, bool bHasProviderCloudCover) const;
|
||||
FLinearColor CalculateSunColor(float SunAlpha, float CloudAlpha) const;
|
||||
};
|
||||
Reference in New Issue
Block a user