feat(sky): the night sky wheels with the Dereth clock; uniform time fade with a scoped twilight band

Rotation (user-directed): the procedural starfield rotates once per
Dereth day (~2 real hours - constellations visibly wheel through a
night) about a celestial pole ~41 deg above the northern horizon, plus
dayOfYear/360 of seasonal drift so the 360-day year changes the night
sky. One SkyParams float (272-byte block, layout test re-pinned)
carries dayFraction + dayOfYear/360 from the world clock; sky.frag
applies a Rodrigues rotation to the sample direction so stars and
mottle turn together. Impossible with retail's static stretched layer.

Fade rework (the 2026-08-23 two-screenshot gate finding): the
per-vertex vTint signal carried the sun-facing product and blanked
stars across the entire twilight half of the sky. The fade now reads
the UNIFORM ambient term - identical star visibility in every compass
direction, same dusk-to-dawn schedule - with one deliberate exception:
a thin suppression band hugging the low sky toward the sun's azimuth
while the sun term is strong, so stars still wash out inside the
actual twilight glow.

Guards updated (rotation anchor, uniform-fade anchor, 272-byte layout);
both sky SPIR-V hashes re-pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 18:21:17 +02:00
parent 91084b9a83
commit 0dac024cdb
10 changed files with 98 additions and 15 deletions

View file

@ -52,8 +52,8 @@ public sealed class VulkanShaderManifestTests
// override, retail's GameSky::Draw @0x00506FF0 rule (see
// SkyFogRuleTests). A deliberate default-path change, reviewed
// with the world-fog-range fix in the same commit.
["sky.frag.spv"] = "e307a7f3cf84aada86db7c2859c55841a650b46732f12149986562cef0a90ca8",
["sky.vert.spv"] = "3b51945fa4ff1be1604144df92866bdd47aade22f9dd90267591ef36adb28cde",
["sky.frag.spv"] = "83a7b4e265f7df4726d716a812ebaa74ba31ea47036bc31005cd8dc1d8981967",
["sky.vert.spv"] = "988eff4e106422ca483d665ba25818e8394737a247e9bae346db3220ac31fd28",
["terrain_modern.frag.spv"] = "7b3cdb01b837ed77ee20559a81c1ce5c9d5395300efcc072560ab0be3c5a1af9",
["terrain_modern.vert.spv"] = "9f4cb221ea6aed94a8d23af6cb8e3f3ed96c3cce6e50d135a72d3b55667b1557",
["ui_text.frag.spv"] = "37a281bf80441cb425eaa3ad8e0b3a43cfa21b74b60973ed4201718b9dc102df",

View file

@ -21,7 +21,12 @@ public sealed class EnhancedNightSkyRuleTests
string code = StripLineComments(frag);
Assert.Contains("if (uParamA > 0.5)", code, StringComparison.Ordinal);
Assert.Contains("nightSky(normalize(vDir), uint(uParamB))", code, StringComparison.Ordinal);
Assert.Contains("nightSky(rdir, uint(uParamB))", code, StringComparison.Ordinal);
// The fade must stay UNIFORM across the sky (the first gate's
// per-vertex vTint signal blanked the whole twilight half) and the
// starfield must wheel with the Dereth clock.
Assert.Contains("dot(uAmbientColor, vec3(0.299, 0.587, 0.114))", code, StringComparison.Ordinal);
Assert.Contains("fract(uNightSkyRotationTurns)", code, StringComparison.Ordinal);
// Screen-pixel star sizing is the reason this exists — the stretched
// texture flaw must not creep back in via a fixed-size grid, and the
// cube-face fwidth seams must not return (the 2026-08-23 gate's

View file

@ -8,7 +8,7 @@ namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Campaign V slice V6e: the sky's dozen loose uniforms became one std140
/// uniform block, and this is the test that keeps the CPU struct and the GLSL
/// block describing the same 256 bytes.
/// block describing the same 272 bytes.
///
/// <para>It exists because the failure mode is silent. std140 gives a
/// <c>vec3</c> 16-byte alignment while only using 12, so the block deliberately
@ -45,6 +45,12 @@ public sealed class SkyParamsLayoutTests
[InlineData("UvScroll", 240)]
[InlineData("ApplyFog", 248)]
[InlineData("SurfOpacity", 252)]
// IA-26: the enhanced night sky's rotation turns plus its three pad
// words, filling the seventeenth vec4 exactly.
[InlineData("NightSkyRotationTurns", 256)]
[InlineData("NsPadA", 260)]
[InlineData("NsPadB", 264)]
[InlineData("NsPadC", 268)]
public void EveryMemberSitsWhereStd140PutsIt(string member, int expectedOffset)
{
Assert.Equal(
@ -60,7 +66,7 @@ public sealed class SkyParamsLayoutTests
?.GetRawConstantValue()
?? throw new InvalidOperationException("SkyParams.SizeInBytes is missing."));
Assert.Equal(256, declared);
Assert.Equal(272, declared);
Assert.Equal(declared, Marshal.SizeOf(SkyParamsType));
// std140 rounds a block up to its largest member's alignment (16).
Assert.Equal(0, declared % 16);