summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 02:13:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 02:13:00 +0800
commit5a908a17335ff291e2d8f2fb3a1ff0ab57770c80 (patch)
treea01b4131d6470d99a03ebfa45e5a6d6456cb20af
parent37953a4b58dfc5f3b8aedf10ad6f1474c2960767 (diff)
downloadliving-village-5a908a17335ff291e2d8f2fb3a1ff0ab57770c80.tar.gz
fix(desktop): P19 白天色调去偏黄(day tint 中性白平衡,黄昏暖橙保留)p19-daylight-taming
-rw-r--r--docs/维护说明.md27
-rw-r--r--src/LivingVillage.Desktop.Tests/DesktopTests.fs17
-rw-r--r--src/LivingVillage.Desktop.Tests/PrototypeTests.fs12
-rw-r--r--src/LivingVillage.Desktop/M6Presentation.fs10
4 files changed, 62 insertions, 4 deletions
diff --git a/docs/维护说明.md b/docs/维护说明.md
index 14d11f6..b22c6bf 100644
--- a/docs/维护说明.md
+++ b/docs/维护说明.md
@@ -227,6 +227,24 @@ dotnet src/LivingVillage.Headless/bin/Release/net8.0/LivingVillage.Headless.dll
- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
一致、`performance_determinism=PASS`。
+## P19 批验:白天偏黄过曝治理(daylight 色调路径)
+
+- line-level 定位:offender 唯一是 `M6Presentation.worldTintAtWarmth`(`M6Presentation.fs:81-87`)的 **day 基线**
+ —— `lerp 248 136 blend`(:84)与 `lerp 224 184 blend`(:85)在 `blend=0`(正午)仍给出 `(255,248,224)`:蓝通道
+ 被压约 12%,整场景乘性偏黄。黄昏暖色链在白天并不生效,已逐一排除:`transitionWarmth`(:51-60)bump 仅在
+ 4.0–6.5 与 19.25–21.75;`goldenVeilAlpha`(:65)正午 = 0;`LanternGlow = max blend (warmth*0.8)`(:101)
+ 正午 = 0;`lightingBlend`(:36-43)正午 = 0。故「白天偏黄」= day world-tint 基线,与黄昏暖窗无关。
+- 修复:day 基线改中性白 `(255,255,255)`,乘性恒等(geometry 保留 true white balance,无伪色彩);night 基线
+ `(118,136,184)` 与 `warmth` 暖移不变,黄昏/清晨暖橙完全保留。
+- 回归:新增 `NoonLightingIsWarmthFreeWithNeutralWhiteBalance`(DesktopTests);PrototypeTests 增白天
+ 10:00–15:00 平台断言(warmth=0、通道中性、veil=0、glow=0);Desktop 108→109,Kernel 89 不变。
+- 证据:`/tmp/opencode/lv-p19-art/`(`p19-noon-12h-0{1,2}.png`、`p19-dusk-20h45-01.png`、
+ `p19-night-00h30-01.png`、`p19-before-noon-yellow.png` vs `p19-after-noon-neutral.png`)。画面均值:
+ 正午前 `(71,109,66)` R−B=+5.1(偏黄)→ 正午后 `(70,109,75)` R−B=−5.3(中性);黄昏 `(146,113,61)`
+ R−B=+85(暖橙保留);深夜 `(34,60,56)` 冷调;白块回归 0 命中。
+- 门槛:clean rebuild 0 警告 0 错误;`final_digest=953775FAEB2FDDE97289491AA260BD8D390C571E48A7A13AD2CB6FB7124F7F6C`
+ 一致、`performance_determinism=PASS`。
+
## 已验证命令
```bash
@@ -265,6 +283,13 @@ for pair in day:12.0 dusk:20.75 night:0.0; do name=${pair%%:*}; hour=${pair##*:}
LV_AUTOPLAY_START_HOUR=$hour LV_AUTOPLAY_RECORD=1 \
LV_RECORD_DIR=/tmp/opencode/lv-p17-art/$name LV_RECORD_EVERY=2 \
dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
+# P19 白天色温取证(正午 12 / 黄昏 20.75 / 深夜 0.5 三帧对比)
+for pair in noon:12.0 dusk:20.75 night:0.5; do name=${pair%%:*}; hour=${pair##*:}; \
+ timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
+ LV_AUTOPLAY_START_HOUR=$hour LV_AUTOPLAY_RECORD=1 \
+ LV_RECORD_DIR=/tmp/opencode/lv-p19-art/$name LV_RECORD_EVERY=2 \
+ dotnet src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll; done
+
# P18 黄昏暖色取证(20:45 真实帧;深夜把 20.75 换成 0)
timeout 100 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
LV_AUTOPLAY_START_HOUR=20.75 LV_AUTOPLAY_RECORD=1 \
@@ -304,6 +329,8 @@ python3 scripts/check-no-white-blocks.py evidence/night-run
- `/tmp/opencode/lv-p13/evidence/`(P13:`walk-idle-frames.png`、`water-3phase.png`、
`smoke-3phase.png`、`water-2frame.png`、`smoke-2frame.png`、`day-panorama.png`、
`night-lantern.png`、`day-night-compare.png`、`walk-real-run.png`、`day-run/`、`night-run/`)
+- `/tmp/opencode/lv-p19-art/`(P19:`p19-noon-12h-0{1,2}.png`、`p19-dusk-20h45-01.png`、
+ `p19-night-00h30-01.png`、`p19-before-noon-yellow.png`/`p19-after-noon-neutral.png`;同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p18-art/`(P18:`p18-dusk-20h45-0{1,2}.png`、`p18-night-deep-01.png`、
`p18-before-old-dusk-blue.png`/`p18-after-new-dusk-warm.png`、`sample-dusk.mp4`;同一份拷入 `evidence/`)
- `/tmp/opencode/lv-p17-art/`(P17:`p17-day-12h-0{1,2}.png`、`p17-dusk-20h45-0{1,2}.png`、
diff --git a/src/LivingVillage.Desktop.Tests/DesktopTests.fs b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
index 00e445b..b78f4e0 100644
--- a/src/LivingVillage.Desktop.Tests/DesktopTests.fs
+++ b/src/LivingVillage.Desktop.Tests/DesktopTests.fs
@@ -143,6 +143,23 @@ type DesktopTests () =
Assert.IsTrue(day.WorldTint.G > day.WorldTint.B)
[<TestMethod>]
+ member _.NoonLightingIsWarmthFreeWithNeutralWhiteBalance () =
+ let noonTick = 12L * Sim.ticksPerDay / 24L
+ let noon = M6Presentation.profileAtTick noonTick
+
+ Assert.AreEqual<M6DayNight>(Day, noon.Mode)
+ Assert.AreEqual<float32>(0.0f, noon.SkyWarmth)
+ Assert.AreEqual<float32>(0.0f, noon.LanternGlow)
+ Assert.AreEqual<int>(0, M6Presentation.goldenVeilAlpha noonTick)
+ // P19 offender was the day world tint attenuating blue (255/248/224); it is neutral now.
+ Assert.AreEqual<int>(255, int noon.WorldTint.R)
+ Assert.AreEqual<int>(int noon.WorldTint.R, int noon.WorldTint.G)
+ Assert.AreEqual<int>(int noon.WorldTint.G, int noon.WorldTint.B)
+ // The sky stays a plain blue, never warm.
+ Assert.IsTrue(noon.Background.B > noon.Background.G)
+ Assert.IsTrue(noon.Background.B > noon.Background.R)
+
+ [<TestMethod>]
member _.DaylightAutoplayBootstrapStartsAtDawnAndPreservesDefaultStart () =
let dawnTick = 6L * Sim.ticksPerDay / 24L
let daylightWorld = WorldBootstrap.initialWorldForDaylight true 42UL Sim.npcCount
diff --git a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
index 895dff8..e58139c 100644
--- a/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
+++ b/src/LivingVillage.Desktop.Tests/PrototypeTests.fs
@@ -897,6 +897,18 @@ type PrototypeTests () =
Assert.IsTrue(warmDusk.Background.B < (M6Presentation.backgroundAtBlend warmDusk.Blend).B)
Assert.IsTrue(warmDusk.WorldTint.R > (M6Presentation.worldTintAtBlend warmDusk.Blend).R)
Assert.IsTrue(warmDusk.WorldTint.B < (M6Presentation.worldTintAtBlend warmDusk.Blend).B)
+
+ // P19: no daylight warm cast. Noon (and the whole 10:00-15:00 plateau) must be warmth-free
+ // and channel-neutral, so the world renders at a true white balance instead of yellow.
+ for hour in 10 .. 15 do
+ let daylight = atHour hour
+ Assert.IsTrue(abs (daylight.SkyWarmth) < 1e-6f, sprintf "hour %d must carry no warmth" hour)
+ Assert.IsTrue((int daylight.WorldTint.R) = (int daylight.WorldTint.G), sprintf "hour %d world tint must be neutral" hour)
+ Assert.IsTrue((int daylight.WorldTint.G) = (int daylight.WorldTint.B), sprintf "hour %d world tint must be neutral" hour)
+ Assert.IsTrue((M6Presentation.goldenVeilAlpha (int64 hour * ticksPerHour)) = 0, sprintf "hour %d must have no golden veil" hour)
+ Assert.IsTrue(abs (daylight.LanternGlow) < 1e-6f, sprintf "hour %d must have no lantern glow" hour)
+ let noon = atHour 12
+ Assert.IsTrue((int noon.WorldTint.R) = 255, "neutral day tint should be full white (no attenuation)")
// Additive veil: positive and strongest at the golden peaks, zero at noon/midnight.
Assert.AreEqual<int>(0, M6Presentation.goldenVeilAlpha (int64 12 * ticksPerHour))
Assert.AreEqual<int>(0, M6Presentation.goldenVeilAlpha 0L)
diff --git a/src/LivingVillage.Desktop/M6Presentation.fs b/src/LivingVillage.Desktop/M6Presentation.fs
index 25737d7..90d2e65 100644
--- a/src/LivingVillage.Desktop/M6Presentation.fs
+++ b/src/LivingVillage.Desktop/M6Presentation.fs
@@ -76,13 +76,15 @@ module M6Presentation =
lerp 160 25 blend + int (20.0f * warmth),
lerp 210 55 blend - int (90.0f * warmth))
- /// World tint also warms (red up, green slightly down, blue down) so tiles, props and
- /// characters all lean golden before the additive veil is layered on top.
+ /// World tint multiplies the whole scene. The day baseline is neutral white so noon keeps
+ /// a true white balance (the old day base 255/248/224 clipped blue by ~12% and made daylight
+ /// look yellow); night stays cool, and the golden shade only appears through `warmth`, which
+ /// is a pure function that is zero at noon and outside the dawn/dusk windows.
let worldTintAtWarmth (blend: float32) (warmth: float32) : Color =
Color(
lerp 255 118 blend + int (70.0f * warmth),
- lerp 248 136 blend - int (10.0f * warmth),
- lerp 224 184 blend - int (75.0f * warmth))
+ lerp 255 136 blend - int (10.0f * warmth),
+ lerp 255 184 blend - int (75.0f * warmth))
/// Warmth-free base colours (kept for callers that only have a blend value).
let backgroundAtBlend (blend: float32) : Color = backgroundAtWarmth blend 0.0f