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] 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.
+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
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,
+11 -18
View File
@@ -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)
+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;
};