feat(sky): pack-gated procedural night sky replacing the stretched DAT star layer; register IA-26

User-directed enhancement ('I want the night sky to look very good'):
retail's star layer is one small texture stretched over a 10-poly dome
cap, so stars smear regardless of source-image quality. With the
Atmospheric render pack active, sky.frag now renders the star layer
(GfxObj 0x010015EF, identical in all 20 Dereth day groups) as a fully
procedural sky computed from the view direction: hash-derived stars on
a cube-face grid in three density tiers plus sparse diffraction-spiked
standouts, sized in SCREEN pixels via derivatives so they stay crisp at
any resolution and FOV, over the user-approved 0.4-1.3% cool mottle
(gen_starfield2.py seed 11, approved 2026-08-23). The draw is forced
additive; the day/night fade rides the star layer's existing retail
lighting product so the schedule matches the authored keyframes. Pack
inactive = retail look byte-untouched.

EnhancedNightSkyRuleTests pins the uParamA gate, the exact star-layer
id, the forced-additive draw, and the pack-runtime wiring; sky shader
SPIR-V recompiled and re-pinned. Hermetic App suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 17:55:20 +02:00
parent 6d8f165d38
commit 508cefdeb1
11 changed files with 302 additions and 11 deletions

View file

@ -209,7 +209,7 @@ public sealed unsafe partial class SkyRenderer
/// those binds are what select the descriptor scope the sections must land in
/// (plan §5.5.14 item 2).</para>
/// </summary>
private void DrawSubMeshRhi(SubMeshGpu sub, uint textureSlot)
private void DrawSubMeshRhi(SubMeshGpu sub, uint textureSlot, bool nightSky = false)
{
if (sub.IndexCount == 0 || sub.VertexBuffer is null || sub.IndexBuffer is null)
return;
@ -220,12 +220,16 @@ public sealed unsafe partial class SkyRenderer
?? throw new InvalidOperationException(
"SkyRenderer requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
encoder.BindPipeline(sub.IsAdditive ? _additivePipeline! : _alphaPipeline!);
// The enhanced night sky ADDS starlight over the dome, so its draw
// rides the additive pipeline even though the authored star surface
// is alpha-blended (register IA-26).
encoder.BindPipeline(nightSky || sub.IsAdditive ? _additivePipeline! : _alphaPipeline!);
var pushConstants = new GpuPushConstants
{
// uViewProjection is unread by sky.vert — the sky carries its own
// camera-anchored view and dome projection in SkyParams — so the
// block's only live member here is the texture slot.
// live members here are the texture slot and the night-sky
// ParamA/ParamB pair (sky.frag's uParamA gate + seed).
ViewProjection = System.Numerics.Matrix4x4.Identity,
DrawIdOffset = 0,
LightingMode = 0,
@ -233,8 +237,8 @@ public sealed unsafe partial class SkyRenderer
LightDebug = 0,
TextureIndexA = textureSlot,
TextureIndexB = 0,
ParamA = 0f,
ParamB = 0f,
ParamA = nightSky ? 1f : 0f,
ParamB = NightSkySeed,
};
encoder.SetPushConstants(in pushConstants);
encoder.BindVertexBuffer(0, sub.VertexBuffer, 0);

View file

@ -89,6 +89,27 @@ public sealed partial class SkyRenderer : IDisposable
public float Near { get; set; } = 0.1f;
public float Far { get; set; } = 1_000_000f;
/// <summary>
/// Dereth's star layer — the one sky GfxObj (verified identical across
/// all 20 day groups) whose draw the enhanced night sky replaces.
/// </summary>
private const uint StarLayerGfxObjId = 0x010015EFu;
/// <summary>
/// User-directed enhancement gate (register IA-26): when this returns
/// true, the star layer renders as sky.frag's procedural night sky
/// (view-direction stars, pixel-crisp at any resolution) instead of the
/// authored stretched texture. Wired by composition to "the Atmospheric
/// render pack is active"; null/false keeps the retail look. Evaluated
/// per draw — never capture a stale pre-session snapshot
/// (claude-memory/feedback_resolve_deferred_funcs_per_call.md).
/// </summary>
internal Func<bool>? EnhancedNightSkyActive { 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;
/// <summary>
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
/// every <c>SkyObject</c> with <c>Properties &amp; 0x04 == 0</c>).
@ -426,7 +447,9 @@ public sealed partial class SkyRenderer : IDisposable
|| obj.TexVelocityX != 0f
|| obj.TexVelocityY != 0f;
uint slot = TextureTableSlot(sub.SurfaceId, needsRepeat);
DrawSubMeshRhi(sub, slot);
bool nightSky = gfxObjId == StarLayerGfxObjId
&& (EnhancedNightSkyActive?.Invoke() ?? false);
DrawSubMeshRhi(sub, slot, nightSky);
}
}
}