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:
parent
91084b9a83
commit
0dac024cdb
10 changed files with 98 additions and 15 deletions
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -311,12 +311,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "9102b156bd4fd667831353640b2feee2ddde66e88579a4e425566c9b67304b64",
|
||||
"sourceSha256": "80c190d5544433bd6b08810c1e5040cec5090fe91839f436728d5454ed7f3c31",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "e7ccdb4d5fece2ceeb48eb43aaa827424df2b68614987eeb3dfb7880dff8ea83",
|
||||
"sourceSha256": "76481dcc1a321c65308bf9c081254978ee7f12413166df5d32a1606228bdc768",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -106,6 +106,13 @@ public sealed partial class SkyRenderer : IDisposable
|
|||
/// </summary>
|
||||
internal Func<bool>? EnhancedNightSkyActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal Func<float>? NightSkyRotationTurns { get; set; }
|
||||
|
||||
/// <summary>Deterministic seed for the procedural sky's hash grids —
|
||||
/// the approved-look generator's seed (gen_starfield2.py, 2026-08-23).</summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public float NightSkyRotationTurns; // 256
|
||||
public float NsPadA; // 260
|
||||
public float NsPadB; // 264
|
||||
public float NsPadC; // 268
|
||||
|
||||
/// <summary>256 — the std140 size of the block, a whole number of vec4s.</summary>
|
||||
public const int SizeInBytes = 256;
|
||||
/// <summary>272 — the std140 size of the block, a whole number of vec4s.</summary>
|
||||
public const int SizeInBytes = 272;
|
||||
}
|
||||
|
||||
private sealed class SubMeshGpu
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue