diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index bfb78468..3dc8f5d7 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -333,6 +333,18 @@ internal sealed class FrameRootCompositionPhase skyForNightSky.EnhancedNightSkyActive = () => nightSkyController.ActiveRuntime?.Descriptor.Id == AcDream.App.Rendering.Packs.BuiltInAtmosphericRenderPack.Id; + // The starfield wheels with the Dereth clock: one turn per + // day about the celestial pole plus ~1 degree/night of + // seasonal drift (dayOfYear/360) — sky.frag's rotation block. + var nightSkyTime = d.WorldTime; + skyForNightSky.NightSkyRotationTurns = () => + { + var cal = nightSkyTime.CurrentCalendar; + int dayOfYear = (int)cal.Month + * AcDream.Core.World.DerethDateTime.DaysInAMonth + + (cal.Day - 1); + return (float)(nightSkyTime.DayFraction + dayOfYear / 360.0); + }; } } diff --git a/src/AcDream.App/Rendering/Shaders/sky.frag b/src/AcDream.App/Rendering/Shaders/sky.frag index a511ef4b..e56e4cae 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.frag +++ b/src/AcDream.App/Rendering/Shaders/sky.frag @@ -43,6 +43,10 @@ layout(std140, ACDREAM_UBO_SET binding = 4) uniform SkyParams { vec2 uUvScroll; float uApplyFog; float uSurfOpacity; + float uNightSkyRotationTurns; // enhanced night sky (IA-26): sky rotation + float uNsPadA; + float uNsPadB; + float uNsPadC; }; struct Light { @@ -186,18 +190,50 @@ vec3 nightSky(vec3 dir, uint seed) void main() { if (uParamA > 0.5) { - // The star layer's own retail lighting product (Emissive 0 -> - // ambient + diffuse) is the day/night signal: ~0.1 at deep night, - // ~1.0 at noon. Stars at full strength only under a dark sky. - float dayL = dot(vTint, vec3(0.299, 0.587, 0.114)); - float night = 1.0 - smoothstep(0.18, 0.45, dayL); + // Day/night fade from the UNIFORM ambient term only — the first + // gate's vTint signal carried the per-vertex sun-facing product and + // blanked stars across the whole twilight half of the sky + // (2026-08-23 screenshots). Ambient is one value for the sky, so + // star visibility is even everywhere; the twilight glow gets its + // own tightly-scoped suppression below. + float dayL = dot(uAmbientColor, vec3(0.299, 0.587, 0.114)); + float night = 1.0 - smoothstep(0.10, 0.32, dayL); if (night < 0.004) { // Daytime: the whole lattice would multiply to zero — skip it // so the enhanced sky costs nothing outside dusk-to-dawn. fragColor = vec4(0.0, 0.0, 0.0, 1.0); return; } - vec3 sky = nightSky(normalize(vDir), uint(uParamB)); + + vec3 d = normalize(vDir); + + // Subtle twilight suppression: only the low sky (bottom ~19 deg) + // toward the sun's azimuth, and only while the sun term is strong + // (dusk/dawn) — stars near the bright horizon wash out, the rest + // of the sky keeps its full carpet. + vec3 dflat = normalize(vec3(d.xy, 0.0) + vec3(1e-5, 0.0, 0.0)); + vec3 sflat = normalize(vec3(uSunDir.xy, 0.0) + vec3(1e-5, 0.0, 0.0)); + float glowSide = max(dot(dflat, sflat), 0.0); + float lowSky = 1.0 - clamp(d.z * 3.0, 0.0, 1.0); + float twilight = clamp( + dot(uSunColor, vec3(0.299, 0.587, 0.114)) * uDiffuseFactor, + 0.0, 1.0); + night *= 1.0 - 0.85 * twilight * glowSide * glowSide * lowSky; + + // The sky wheels: one revolution per Dereth day about a celestial + // pole ~41 deg above the northern horizon (+Y north, +Z up), plus + // ~1 deg/night of seasonal drift folded into the turns value + // (dayFraction + dayOfYear/360). Rodrigues rotation of the sample + // direction; stars and mottle turn together. + const vec3 NS_POLE = vec3(0.0, 0.6598, 0.7514); + float ang = 6.28318530718 * fract(uNightSkyRotationTurns); + float ca = cos(ang); + float sa = sin(ang); + vec3 rdir = d * ca + + cross(NS_POLE, d) * sa + + NS_POLE * dot(NS_POLE, d) * (1.0 - ca); + + vec3 sky = nightSky(rdir, uint(uParamB)); // Additive pipeline (forced for this draw): rgb adds over the dome. fragColor = vec4(sky * night, 1.0); return; diff --git a/src/AcDream.App/Rendering/Shaders/sky.vert b/src/AcDream.App/Rendering/Shaders/sky.vert index 28a94d1a..e21fcdee 100644 --- a/src/AcDream.App/Rendering/Shaders/sky.vert +++ b/src/AcDream.App/Rendering/Shaders/sky.vert @@ -67,6 +67,10 @@ layout(std140, ACDREAM_UBO_SET binding = 4) uniform SkyParams { vec2 uUvScroll; float uApplyFog; // 1 for foggable layers; raw-additive keeps fog off float uSurfOpacity; // final surface opacity multiplier from the CPU + float uNightSkyRotationTurns; // enhanced night sky (IA-26): sky rotation + float uNsPadA; + float uNsPadB; + float uNsPadC; }; // Shared SceneLighting UBO — we need uFogParams.xy (fog start/end) to diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index dd84a830..b4eaf1b0 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -311,12 +311,12 @@ "stages": [ { "stage": "vert", - "sourceSha256": "9102b156bd4fd667831353640b2feee2ddde66e88579a4e425566c9b67304b64", + "sourceSha256": "80c190d5544433bd6b08810c1e5040cec5090fe91839f436728d5454ed7f3c31", "compiled": true }, { "stage": "frag", - "sourceSha256": "e7ccdb4d5fece2ceeb48eb43aaa827424df2b68614987eeb3dfb7880dff8ea83", + "sourceSha256": "76481dcc1a321c65308bf9c081254978ee7f12413166df5d32a1606228bdc768", "compiled": true } ] diff --git a/src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv index f6fc7441..2914922e 100644 Binary files a/src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv and b/src/AcDream.App/Rendering/Shaders/spv/sky.frag.spv differ diff --git a/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv index e69c1a43..3cff9d37 100644 Binary files a/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv and b/src/AcDream.App/Rendering/Shaders/spv/sky.vert.spv differ diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index aec455b6..84975d70 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -106,6 +106,13 @@ public sealed partial class SkyRenderer : IDisposable /// internal Func? EnhancedNightSkyActive { get; set; } + /// + /// Dereth-clock rotation source for the enhanced night sky, in turns + /// (dayFraction + dayOfYear/360). Composition wires it to the world + /// time service; evaluated once per sky pass. + /// + internal Func? NightSkyRotationTurns { get; set; } + /// Deterministic seed for the procedural sky's hash grids — /// the approved-look generator's seed (gen_starfield2.py, 2026-08-23). private const float NightSkySeed = 11f; @@ -209,6 +216,8 @@ public sealed partial class SkyRenderer : IDisposable // that FOV here, including the near-180-degree teleport transition. var skyProj = SkyProjection.WithDepthRange(camera.Projection, Near, Far); + _params.NightSkyRotationTurns = NightSkyRotationTurns?.Invoke() ?? 0f; + // View with translation zeroed — keeps the sky at camera origin // regardless of camera position in the world. var skyView = camera.View; @@ -726,9 +735,20 @@ public sealed partial class SkyRenderer : IDisposable public Vector2 UvScroll; // 240 public float ApplyFog; // 248 public float SurfOpacity; // 252 + /// + /// Whole-sky rotation in TURNS for the enhanced night sky (IA-26): + /// dayFraction + dayOfYear/360, so the procedural starfield wheels + /// once per Dereth day about the celestial pole and drifts ~1 degree + /// per night through the 360-day year (seasonal constellations). + /// Zero when the pack is inactive; the retail path never reads it. + /// + public float NightSkyRotationTurns; // 256 + public float NsPadA; // 260 + public float NsPadB; // 264 + public float NsPadC; // 268 - /// 256 — the std140 size of the block, a whole number of vec4s. - public const int SizeInBytes = 256; + /// 272 — the std140 size of the block, a whole number of vec4s. + public const int SizeInBytes = 272; } private sealed class SubMeshGpu diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs index 6a54192b..b33558fc 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs @@ -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", diff --git a/tests/AcDream.App.Tests/Rendering/Sky/EnhancedNightSkyRuleTests.cs b/tests/AcDream.App.Tests/Rendering/Sky/EnhancedNightSkyRuleTests.cs index 48ad7a82..9a4fac2b 100644 --- a/tests/AcDream.App.Tests/Rendering/Sky/EnhancedNightSkyRuleTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Sky/EnhancedNightSkyRuleTests.cs @@ -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 diff --git a/tests/AcDream.App.Tests/Rendering/SkyParamsLayoutTests.cs b/tests/AcDream.App.Tests/Rendering/SkyParamsLayoutTests.cs index 7a9a0a82..c9127818 100644 --- a/tests/AcDream.App.Tests/Rendering/SkyParamsLayoutTests.cs +++ b/tests/AcDream.App.Tests/Rendering/SkyParamsLayoutTests.cs @@ -8,7 +8,7 @@ namespace AcDream.App.Tests.Rendering; /// /// 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. /// /// It exists because the failure mode is silent. std140 gives a /// vec3 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);