Add first pass sky lighting

This commit is contained in:
2026-05-16 01:32:48 -07:00
parent 06508061da
commit dddac97658
7 changed files with 311 additions and 21 deletions
+1 -1
View File
@@ -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] 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 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. - [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. - [ ] Add audio cues for weather.
## 0.1.D Single Biome MVP Map ## 0.1.D Single Biome MVP Map
Binary file not shown.
+9
View File
@@ -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 protection level. The dev HUD shows current shelter protection so weather
pressure can be tuned during MVP tests. 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 The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses
Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the
current temperature, daily low/high, precipitation, wind, humidity, cloud cover, current temperature, daily low/high, precipitation, wind, humidity, cloud cover,
+11 -18
View File
@@ -68,25 +68,11 @@ DEMO_ACTORS = [
"rotation": unreal.Rotator(0.0, 135.0, 0.0), "rotation": unreal.Rotator(0.0, 135.0, 0.0),
}, },
{ {
"label": "AGR_DemoSun", "label": "AGR_DemoSkyLightingController",
"class": unreal.DirectionalLight, "class": unreal.AgrarianSkyLightingController,
"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,
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0), "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
"fixed_z": 12000.0, "fixed_z": 12000.0,
"rotation": unreal.Rotator(0.0, 0.0, 0.0), "rotation": unreal.Rotator(-42.0, -35.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),
}, },
{ {
"label": "AGR_DemoNoticeActor", "label": "AGR_DemoNoticeActor",
@@ -97,6 +83,12 @@ DEMO_ACTORS = [
}, },
] ]
LEGACY_DEMO_LIGHTING_LABELS = {
"AGR_DemoSun",
"AGR_DemoSkyLight",
"AGR_DemoFog",
}
BIOME_RESOURCE_ACTORS = [ BIOME_RESOURCE_ACTORS = [
{ {
@@ -330,7 +322,7 @@ def spawn_foliage_actor(height_values):
reserved_points = [ reserved_points = [
spec["location_xy"] spec["location_xy"]
for spec in DEMO_ACTORS 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( foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
@@ -405,6 +397,7 @@ def main():
raise RuntimeError(f"Could not load map: {MAP_PATH}") raise RuntimeError(f"Could not load map: {MAP_PATH}")
labels = {spec["label"] for spec in DEMO_ACTORS} 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 BIOME_RESOURCE_ACTORS)
labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS) labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS)
labels.add(FOLIAGE_LABEL) labels.add(FOLIAGE_LABEL)
+66
View File
@@ -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;
};