summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 00:44:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 00:44:00 +0800
commite9d3e224e802ec41b9f565f5e8ed7eec8470a310 (patch)
tree98247fe11fbf22141e0baaf188ddf29a38e0d9b9 /src
parenta5c3400412fbab11d4f4b83a5d0294292eee5f08 (diff)
downloadliving-village-e9d3e224e802ec41b9f565f5e8ed7eec8470a310.tar.gz
feat(desktop): P17 清晨/黄昏光照精修(暖色天际 + 过渡窗口参数化,分数小时平滑)
Diffstat (limited to 'src')
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs44
-rw-r--r--src/LivingVillage.Desktop/M6Presentation.fs69
2 files changed, 80 insertions, 33 deletions
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index b8f23a0..5a153b1 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -858,21 +858,35 @@ type PrototypeTests () =
Assert.AreEqual<float32>(1.0f, (atHour 0).Blend)
Assert.AreEqual<float32>(1.0f, (atHour 2).Blend)
- // Dawn ramp: 4:00 -> 6:00 goes night -> day, monotonic.
- let dawnStart = (atHour 4).Blend
- let dawnMid = (M6Presentation.lightingAtTick (int64 5 * ticksPerHour)).Blend
- let dawnEnd = (atHour 6).Blend
- Assert.AreEqual<float32>(1.0f, dawnStart)
- Assert.AreEqual<float32>(0.5f, dawnMid)
- Assert.AreEqual<float32>(0.0f, dawnEnd)
-
- // Dusk ramp 20:00 -> 22:00 goes day -> night monotonically.
- let duskStart = (atHour 20).Blend
- let duskMid = (M6Presentation.lightingAtTick (int64 21 * ticksPerHour)).Blend
- let duskEnd = (atHour 22).Blend
- Assert.AreEqual<float32>(0.0f, duskStart)
- Assert.AreEqual<float32>(0.5f, duskMid)
- Assert.AreEqual<float32>(1.0f, duskEnd)
+ // P17 window: dawn 4:00 -> 6:30 goes night -> day, monotonic (fractional hours).
+ let dawnMidTick = int64 5 * ticksPerHour + ticksPerHour / 4L // 5:15
+ let dawnEndTick = int64 6 * ticksPerHour + ticksPerHour / 2L // 6:30
+ Assert.AreEqual<float32>(1.0f, (atHour 4).Blend)
+ Assert.AreEqual<float32>(0.5f, (M6Presentation.lightingAtTick dawnMidTick).Blend)
+ Assert.AreEqual<float32>(0.0f, (M6Presentation.lightingAtTick dawnEndTick).Blend)
+
+ // Dusk ramp 19:30 -> 22:00 goes day -> night monotonically.
+ let duskStartTick = int64 19 * ticksPerHour + ticksPerHour / 2L // 19:30
+ let duskMidTick = int64 20 * ticksPerHour + ticksPerHour * 3L / 4L // 20:45
+ Assert.AreEqual<float32>(0.0f, (M6Presentation.lightingAtTick duskStartTick).Blend)
+ Assert.AreEqual<float32>(0.5f, (M6Presentation.lightingAtTick duskMidTick).Blend)
+ Assert.AreEqual<float32>(1.0f, (atHour 22).Blend)
+
+ // Blue hour: the transition lingers past 6:00 and starts before 20:00.
+ Assert.IsTrue((atHour 6).Blend > 0.0f && (atHour 6).Blend < 0.5f)
+ Assert.IsTrue((atHour 20).Blend > 0.0f && (atHour 20).Blend < 0.5f)
+
+ // Golden-hour warmth peaks mid-dawn/mid-dusk and vanishes at noon/midnight.
+ Assert.AreEqual<float32>(0.0f, (atHour 12).SkyWarmth)
+ Assert.AreEqual<float32>(0.0f, (atHour 0).SkyWarmth)
+ Assert.AreEqual<float32>(1.0f, (M6Presentation.lightingAtTick dawnMidTick).SkyWarmth)
+ Assert.AreEqual<float32>(1.0f, (M6Presentation.lightingAtTick duskMidTick).SkyWarmth)
+ Assert.IsTrue((atHour 5).SkyWarmth > 0.5f)
+ Assert.IsTrue((atHour 21).SkyWarmth > 0.5f)
+ // The warm shift pushes the sky redder and less blue than the plain blend.
+ let warmDawn = atHour 5
+ Assert.IsTrue(warmDawn.Background.R > (M6Presentation.backgroundAtBlend warmDawn.Blend).R)
+ Assert.IsTrue(warmDawn.Background.B < (M6Presentation.backgroundAtBlend warmDawn.Blend).B)
// Determinism without randomness.
let once = atHour 13
diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs
index 65300f0..11399c0 100644
--- a/src/LivingVillage.Desktop/M6Presentation.fs
+++ b/src/LivingVillage.Desktop/M6Presentation.fs
@@ -13,7 +13,8 @@ type M6RenderProfile =
Background: Color
WorldTint: Color
Blend: float32
- LanternGlow: float32 }
+ LanternGlow: float32
+ SkyWarmth: float32 }
module M6Presentation =
@@ -22,36 +23,68 @@ module M6Presentation =
| Day -> "白天"
| Night -> "夜晚"
- /// Continuous light blend: 0 = full day, 1 = full night. Ramps are deterministic
- /// pure functions of the simulation tick (dawn 4:00-6:00, dusk 20:00-22:00), so the
- /// boundaries agree with Sim.isNightTick (night >= 22:00 or < 6:00).
+ // Transition windows in float hours. Dawn lingers past 6:00 and dusk begins before
+ // 20:00 so the blue hour is longer and the sky warms before it darkens; the anchors
+ // still agree with Sim.isNightTick (night >= 22:00 or < 6:00) once fully dark.
+ let dawnStartHour = 4.0f
+ let dawnEndHour = 6.5f
+ let duskStartHour = 19.5f
+ let duskEndHour = 22.0f
+
+ /// Continuous light blend: 0 = full day, 1 = full night. Deterministic pure function
+ /// of the simulation tick, parameterised by the dawn/dusk windows above.
let lightingBlend (tick: int64) : float32 =
let hourTicks = Sim.ticksPerDay / 24L
- let hour = float32 (tick / hourTicks)
+ let hour = float32 tick / float32 hourTicks
let clamp01 v = if v < 0.0f then 0.0f elif v > 1.0f then 1.0f else v
- if hour >= 6.0f && hour < 20.0f then 0.0f
- elif hour >= 22.0f then 1.0f
- elif hour < 4.0f then 1.0f
- elif hour < 6.0f then clamp01 (1.0f - (hour - 4.0f) / 2.0f)
- else clamp01 ((hour - 20.0f) / 2.0f)
+ if hour >= dawnEndHour && hour < duskStartHour then 0.0f
+ elif hour >= duskEndHour || hour < dawnStartHour then 1.0f
+ elif hour < dawnEndHour then clamp01 (1.0f - (hour - dawnStartHour) / (dawnEndHour - dawnStartHour))
+ else clamp01 ((hour - duskStartHour) / (duskEndHour - duskStartHour))
+
+ /// Golden-hour warmth (0..1): a triangular bump peaking mid-dawn and mid-dusk, zero at
+ /// noon and midnight. Drives the warm sky/world tint so P17 transition frames read golden.
+ let transitionWarmth (tick: int64) : float32 =
+ let hour = float32 tick / float32 (Sim.ticksPerDay / 24L)
+ let bump (startHour: float32) (endHour: float32) : float32 =
+ if hour <= startHour || hour >= endHour then
+ 0.0f
+ else
+ let mid = (startHour + endHour) * 0.5f
+ let half = (endHour - startHour) * 0.5f
+ 1.0f - abs (hour - mid) / half
+ max (bump dawnStartHour dawnEndHour) (bump duskStartHour duskEndHour)
let private lerp (a: int) (b: int) (blend: float32) : int =
a + int (float32 (b - a) * blend)
- let backgroundAtBlend (blend: float32) : Color =
- Color(lerp 100 18 blend, lerp 160 25 blend, lerp 210 55 blend)
+ /// Blended sky/world colour plus the golden-hour warmth shift (warm channels up, blue down).
+ let backgroundAtWarmth (blend: float32) (warmth: float32) : Color =
+ Color(
+ lerp 100 18 blend + int (52.0f * warmth),
+ lerp 160 25 blend + int (14.0f * warmth),
+ lerp 210 55 blend - int (34.0f * warmth))
+
+ let worldTintAtWarmth (blend: float32) (warmth: float32) : Color =
+ Color(
+ lerp 255 118 blend + int (10.0f * warmth),
+ lerp 248 136 blend - int (6.0f * warmth),
+ lerp 224 184 blend - int (26.0f * warmth))
- let worldTintAtBlend (blend: float32) : Color =
- Color(lerp 255 118 blend, lerp 248 136 blend, lerp 224 184 blend)
+ /// Warmth-free base colours (kept for callers that only have a blend value).
+ let backgroundAtBlend (blend: float32) : Color = backgroundAtWarmth blend 0.0f
+ let worldTintAtBlend (blend: float32) : Color = worldTintAtWarmth blend 0.0f
- /// Pure day-phase lighting: deterministic tint/background plus lantern warm-glow boost.
+ /// Pure day-phase lighting: deterministic tint/background/warmth plus lantern glow.
let lightingAtTick (tick: int64) : M6RenderProfile =
let blend = lightingBlend tick
+ let warmth = transitionWarmth tick
{ Mode = if blend < 0.5f then Day else Night
- Background = backgroundAtBlend blend
+ Background = backgroundAtWarmth blend warmth
Blend = blend
- WorldTint = worldTintAtBlend blend
- LanternGlow = blend }
+ WorldTint = worldTintAtWarmth blend warmth
+ LanternGlow = blend
+ SkyWarmth = warmth }
let profileAtTick (tick: int64) : M6RenderProfile = lightingAtTick tick