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:
parent
6d8f165d38
commit
508cefdeb1
11 changed files with 302 additions and 11 deletions
|
|
@ -4,8 +4,15 @@
|
|||
in vec2 vTex;
|
||||
in vec3 vTint;
|
||||
in float vFogFactor; // 1 = no fog, 0 = full fog color
|
||||
in vec3 vDir; // model-space view direction (enhanced night sky)
|
||||
out vec4 fragColor;
|
||||
|
||||
// Enhanced night sky (render-pack-gated, register IA-26): uParamA > 0.5 marks
|
||||
// the star-layer draw as the procedural night sky; uParamB carries its seed.
|
||||
// Push-constant-backed like uTextureIndexA (see VulkanGlslPreamble.cs).
|
||||
uniform float uParamA;
|
||||
uniform float uParamB;
|
||||
|
||||
// Campaign V slice V6e: the sky's texture is now read through the shared table
|
||||
// (ACDREAM_SAMPLE_2D, injected by tools/ShaderCompiler/VulkanGlslPreamble.cs)
|
||||
// rather than a `uniform sampler2D` bound to texture unit 0. Vulkan has no
|
||||
|
|
@ -52,7 +59,138 @@ layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
|||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// ============================== enhanced night sky ==========================
|
||||
// User-directed enhancement (register IA-26): when the Atmospheric render pack
|
||||
// is active, the DAT star layer (GfxObj 0x010015EF - a small texture stretched
|
||||
// over a 10-poly dome cap) is replaced by a fully procedural sky computed from
|
||||
// the VIEW DIRECTION. Stars are hash-derived on a cube-face grid, sized in
|
||||
// SCREEN pixels via fwidth, so they stay pixel-crisp at any resolution, FOV,
|
||||
// and view angle - the "stretched image" flaw this exists to remove. The art
|
||||
// direction (density tiers, colour distribution, spiked standouts, faint cool
|
||||
// mottle) is the user-approved 2026-08-23 generator recipe
|
||||
// (scratchpad gen_starfield2.py); the retail default look is untouched.
|
||||
|
||||
uint nsPcg(uint v)
|
||||
{
|
||||
v = v * 747796405u + 2891336453u;
|
||||
v = ((v >> ((v >> 28u) + 4u)) ^ v) * 277803737u;
|
||||
return (v >> 22u) ^ v;
|
||||
}
|
||||
|
||||
float nsRand(uint s) { return float(s) * (1.0 / 4294967296.0); }
|
||||
|
||||
vec3 nsTint(float r)
|
||||
{
|
||||
if (r < 0.04) return vec3(1.00, 0.55, 0.35); // orange-red
|
||||
if (r < 0.12) return vec3(1.00, 0.80, 0.60); // warm
|
||||
if (r < 0.62) return vec3(1.00, 0.97, 0.93); // near-white
|
||||
if (r < 0.88) return vec3(0.90, 0.95, 1.00); // blue-white
|
||||
return vec3(0.70, 0.82, 1.00); // hot blue
|
||||
}
|
||||
|
||||
float nsVnoise(vec3 p, uint seed)
|
||||
{
|
||||
vec3 i = floor(p);
|
||||
vec3 f = fract(p);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
float acc = 0.0;
|
||||
for (int c = 0; c < 8; ++c)
|
||||
{
|
||||
vec3 o = vec3(float(c & 1), float((c >> 1) & 1), float((c >> 2) & 1));
|
||||
vec3 q = i + o;
|
||||
uint h = nsPcg(seed
|
||||
^ nsPcg(uint(int(q.x) + 512)
|
||||
^ nsPcg(uint(int(q.y) + 512)
|
||||
^ uint(int(q.z) + 512))));
|
||||
vec3 w = mix(1.0 - f, f, o);
|
||||
acc += nsRand(h) * w.x * w.y * w.z;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
vec3 nsStars(vec2 fuv, uint faceSeed, float cells, float density,
|
||||
float bMin, float bMax, float sizePx, bool spikes)
|
||||
{
|
||||
vec2 g = fuv * cells;
|
||||
vec2 cellF = floor(g);
|
||||
// Screen pixels per cell-space unit: the crispness anchor.
|
||||
float pxPerCell = 1.0 / max(length(fwidth(g)), 1e-6);
|
||||
vec3 acc = vec3(0.0);
|
||||
for (int dy = -1; dy <= 1; ++dy)
|
||||
for (int dx = -1; dx <= 1; ++dx)
|
||||
{
|
||||
vec2 c = cellF + vec2(float(dx), float(dy));
|
||||
uint h = nsPcg(faceSeed
|
||||
^ nsPcg(uint(int(c.x) + 512)
|
||||
^ nsPcg(uint(int(c.y) + 512)
|
||||
^ uint(cells))));
|
||||
if (nsRand(h) > density) continue;
|
||||
uint h2 = nsPcg(h);
|
||||
uint h3 = nsPcg(h2);
|
||||
uint h4 = nsPcg(h3);
|
||||
uint h5 = nsPcg(h4);
|
||||
vec2 pos = c + vec2(nsRand(h2), nsRand(h3));
|
||||
float t = nsRand(h4);
|
||||
float b = mix(bMin, bMax, t * t * t);
|
||||
vec3 tint = nsTint(nsRand(h5));
|
||||
vec2 dPx = (g - pos) * pxPerCell;
|
||||
float d2 = dot(dPx, dPx);
|
||||
float star = exp(-d2 / (2.0 * sizePx * sizePx));
|
||||
if (spikes)
|
||||
{
|
||||
// Axis-aligned diffraction cross + soft halo for the standouts.
|
||||
float dist = sqrt(d2) + 1e-4;
|
||||
float fall = exp(-dist / (14.0 * sizePx));
|
||||
float arm = exp(-dPx.y * dPx.y * 0.8) + exp(-dPx.x * dPx.x * 0.8);
|
||||
star += 0.35 * fall * arm;
|
||||
star += 0.10 * exp(-d2 / (18.0 * sizePx * sizePx));
|
||||
}
|
||||
acc += b * tint * star;
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
vec3 nightSky(vec3 dir, uint seed)
|
||||
{
|
||||
// Cube-face parameterisation: uniform angular density, no pole pinch.
|
||||
// Face-edge star continuity is not exact (adjacent faces hash their own
|
||||
// grids); the sub-pixel population makes any seam visually negligible.
|
||||
vec3 ad = abs(dir);
|
||||
uint face; vec2 fuv; float ma;
|
||||
if (ad.z >= ad.x && ad.z >= ad.y) { face = dir.z > 0.0 ? 4u : 5u; ma = ad.z; fuv = dir.xy; }
|
||||
else if (ad.x >= ad.y) { face = dir.x > 0.0 ? 0u : 1u; ma = ad.x; fuv = dir.yz; }
|
||||
else { face = dir.y > 0.0 ? 2u : 3u; ma = ad.y; fuv = dir.xz; }
|
||||
fuv = fuv / ma * 0.5 + 0.5;
|
||||
uint fs = nsPcg(seed ^ (face * 0x9E3779B9u));
|
||||
|
||||
// Faint cool background mottle (0.4%..1.3% - the user-approved level).
|
||||
float n = 0.55 * nsVnoise(dir * 3.0, seed ^ 0x9E3779B9u)
|
||||
+ 0.30 * nsVnoise(dir * 7.0, seed ^ 0x85EBCA6Bu)
|
||||
+ 0.15 * nsVnoise(dir * 15.0, seed ^ 0xC2B2AE35u);
|
||||
vec3 rgb = (0.004 + 0.009 * n) * vec3(0.85, 0.92, 1.10);
|
||||
|
||||
// The star carpet: three density tiers plus sparse spiked standouts.
|
||||
rgb += nsStars(fuv, fs, 160.0, 0.90, 0.05, 0.35, 0.55, false);
|
||||
rgb += nsStars(fuv, fs, 64.0, 0.50, 0.20, 0.70, 0.75, false);
|
||||
rgb += nsStars(fuv, fs, 24.0, 0.35, 0.50, 1.40, 1.05, false);
|
||||
rgb += nsStars(fuv, fs, 6.0, 0.10, 2.00, 4.00, 1.80, true);
|
||||
return rgb;
|
||||
}
|
||||
// ============================================================================
|
||||
|
||||
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);
|
||||
vec3 sky = nightSky(normalize(vDir), uint(uParamB));
|
||||
// Additive pipeline (forced for this draw): rgb adds over the dome.
|
||||
fragColor = vec4(sky * night, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec4 sampled = ACDREAM_SAMPLE_2D(uTextureIndexA, vTex);
|
||||
|
||||
vec3 rgb = sampled.rgb * vTint;
|
||||
|
|
|
|||
|
|
@ -115,9 +115,14 @@ out gl_PerVertex {
|
|||
out vec2 vTex;
|
||||
out vec3 vTint;
|
||||
out float vFogFactor; // 1 = no fog (close), 0 = full fog (far)
|
||||
// Model-space position of the (camera-centred) sky vertex — normalised in the
|
||||
// fragment stage it is the view DIRECTION. Only the enhanced night-sky branch
|
||||
// reads it (see sky.frag's uParamA gate).
|
||||
out vec3 vDir;
|
||||
|
||||
void main() {
|
||||
vTex = aTex + uUvScroll;
|
||||
vDir = aPos;
|
||||
gl_Position = uSkyProjection * uSkyView * uModel * vec4(aPos, 1.0);
|
||||
|
||||
// uModel for sky is pure rotation (Z then Y) — orthonormal, so
|
||||
|
|
|
|||
|
|
@ -311,12 +311,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "a89580f54c8d36b33d50b84b9fb61024861c737bb0b5bd7a4df0704bf010c3c3",
|
||||
"sourceSha256": "9102b156bd4fd667831353640b2feee2ddde66e88579a4e425566c9b67304b64",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "b1f3b924af2040c909337c02461ed6795b5320fb04b5bb371ce1546feead567c",
|
||||
"sourceSha256": "bf5bc353959636b1c80a9f72dc918cffdd33fb6b7b655149de40422030b4f51a",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 & 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue