fix(sky): seamless night-sky star lattice — no face seams, no flare, round pixel-exact stars
The first night-sky gate (2026-08-23 screenshot) showed three defects:
glowing beams along the cube-face boundaries (fwidth blowup where
adjacent pixels land on different faces lights every neighbourhood star
solid), diffraction-spiked standouts the user rejects ('that is in a
photo only, not in real sky'), and ellipse-stretched stars from scalar
length(fwidth) sizing at oblique view angles.
One rewrite removes all three: stars now live on a seamless 3D lattice
over the unit sphere (no faces, so no seams by construction), each star
resolved through an exact tangent-plane -> screen-pixel 2x2 solve of
the direction derivatives (perfectly round, true pixel sizing at every
view angle, sharper cores), spikes deleted in favour of a soft round
halo on the bright tiers. Guard test updated to pin the new anchors and
forbid both fwidth-face grids and spikes; sky.frag.spv re-pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
508cefdeb1
commit
b8cfee6d08
5 changed files with 58 additions and 46 deletions
|
|
@ -108,60 +108,65 @@ float nsVnoise(vec3 p, uint seed)
|
|||
return acc;
|
||||
}
|
||||
|
||||
vec3 nsStars(vec2 fuv, uint faceSeed, float cells, float density,
|
||||
float bMin, float bMax, float sizePx, bool spikes)
|
||||
vec3 nsStarTier(vec3 dir, vec3 ex, vec3 ey, uint seed, float cells,
|
||||
float density, float bMin, float bMax, float sizePx,
|
||||
float haloAmp)
|
||||
{
|
||||
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);
|
||||
// Seamless 3D lattice on the unit sphere: no face boundaries, so no
|
||||
// seam artefacts by construction (the 2026-08-23 gate screenshot's
|
||||
// glowing "Y" was cube-face fwidth blowup — do not bring faces back).
|
||||
vec3 g = dir * cells;
|
||||
vec3 cellF = floor(g);
|
||||
|
||||
// Exact tangent-plane -> screen-pixel solve: 2x2 normal equations of
|
||||
// the direction derivatives. Stars come out perfectly ROUND and sized
|
||||
// in true screen pixels at every view angle — a length(fwidth(...))
|
||||
// scalar stretches them into ellipses at oblique angles.
|
||||
float a = dot(ex, ex);
|
||||
float b = dot(ex, ey);
|
||||
float c = dot(ey, ey);
|
||||
float det = max(a * c - b * b, 1e-14);
|
||||
|
||||
vec3 acc = vec3(0.0);
|
||||
for (int dz = -1; dz <= 1; ++dz)
|
||||
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))));
|
||||
vec3 cc = cellF + vec3(float(dx), float(dy), float(dz));
|
||||
uint h = nsPcg(seed
|
||||
^ nsPcg(uint(int(cc.x) + 512)
|
||||
^ nsPcg(uint(int(cc.y) + 512)
|
||||
^ nsPcg(uint(int(cc.z) + 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);
|
||||
uint h6 = nsPcg(h5);
|
||||
vec3 sdir = normalize(cc + vec3(nsRand(h2), nsRand(h3), nsRand(h4)));
|
||||
vec3 v = sdir - dir * dot(sdir, dir); // tangent-plane offset
|
||||
float bx = dot(v, ex);
|
||||
float by = dot(v, ey);
|
||||
vec2 sPx = vec2(bx * c - by * b, by * a - bx * b) / det;
|
||||
float d2 = dot(sPx, sPx);
|
||||
if (d2 > 400.0) continue; // > 20 px: contributes nothing
|
||||
float t = nsRand(h5);
|
||||
float br = mix(bMin, bMax, t * t * t);
|
||||
vec3 tint = nsTint(nsRand(h6));
|
||||
// Round gaussian core; optional soft round halo on the bright tiers.
|
||||
// NO diffraction spikes / lens flare — user-directed 2026-08-23:
|
||||
// spikes are a photographic artefact, not a naked-eye sky.
|
||||
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;
|
||||
star += haloAmp * exp(-d2 / (24.0 * sizePx * sizePx));
|
||||
acc += br * 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));
|
||||
vec3 ex = dFdx(dir);
|
||||
vec3 ey = dFdy(dir);
|
||||
|
||||
// Faint cool background mottle (0.4%..1.3% - the user-approved level).
|
||||
float n = 0.55 * nsVnoise(dir * 3.0, seed ^ 0x9E3779B9u)
|
||||
|
|
@ -169,11 +174,12 @@ vec3 nightSky(vec3 dir, uint seed)
|
|||
+ 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);
|
||||
// The star carpet: three density tiers plus sparse bright standouts,
|
||||
// all crisp round points (see nsStarTier).
|
||||
rgb += nsStarTier(dir, ex, ey, seed ^ 0x1B873593u, 110.0, 0.85, 0.05, 0.35, 0.50, 0.0);
|
||||
rgb += nsStarTier(dir, ex, ey, seed ^ 0xCC9E2D51u, 48.0, 0.45, 0.20, 0.70, 0.65, 0.0);
|
||||
rgb += nsStarTier(dir, ex, ey, seed ^ 0x27D4EB2Fu, 18.0, 0.30, 0.50, 1.40, 0.90, 0.05);
|
||||
rgb += nsStarTier(dir, ex, ey, seed ^ 0x165667B1u, 6.0, 0.08, 2.00, 4.00, 1.40, 0.10);
|
||||
return rgb;
|
||||
}
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@
|
|||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "bf5bc353959636b1c80a9f72dc918cffdd33fb6b7b655149de40422030b4f51a",
|
||||
"sourceSha256": "a6efd09f396b17ae6a19ffc4be400ac9b035056cbad26aee3447459cd98341f0",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -52,7 +52,7 @@ 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"] = "8105984072fc1b9075b5efdffd75d959c4087710d354d50a0e00ee0500462ca7",
|
||||
["sky.frag.spv"] = "2d2de4080c7f4b0885aee4441e37a1725d3f30661b45773bcb39383674b1718b",
|
||||
["sky.vert.spv"] = "3b51945fa4ff1be1604144df92866bdd47aade22f9dd90267591ef36adb28cde",
|
||||
["terrain_modern.frag.spv"] = "7b3cdb01b837ed77ee20559a81c1ce5c9d5395300efcc072560ab0be3c5a1af9",
|
||||
["terrain_modern.vert.spv"] = "9f4cb221ea6aed94a8d23af6cb8e3f3ed96c3cce6e50d135a72d3b55667b1557",
|
||||
|
|
|
|||
|
|
@ -23,8 +23,14 @@ public sealed class EnhancedNightSkyRuleTests
|
|||
Assert.Contains("if (uParamA > 0.5)", code, StringComparison.Ordinal);
|
||||
Assert.Contains("nightSky(normalize(vDir), uint(uParamB))", 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.
|
||||
Assert.Contains("fwidth(g)", code, StringComparison.Ordinal);
|
||||
// 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
|
||||
// glowing "Y"): the crispness anchor is the seamless 3D-lattice
|
||||
// tangent-plane solve on the direction derivatives.
|
||||
Assert.Contains("dFdx(dir)", code, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("fwidth(g)", code, StringComparison.Ordinal);
|
||||
// No diffraction spikes — user-directed: flare is photographic.
|
||||
Assert.DoesNotContain("spike", code, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue