fix(sky): drive wrap mode from mesh UV range — fixes Bug B (stars-as-square)
Bug B in docs/research/2026-04-26-sky-investigation-handoff.md: stars
rendered as a small square in one corner of the sky instead of stretching
across the dome.
Root cause: the wrap-mode heuristic at SkyRenderer.cs:234-237 was
"GL_CLAMP_TO_EDGE unless TexVelocity != 0". That heuristic was tuned to
fix a separate symptom (the outer dome 0x010015EE/F0/F1/F2 shows
wall-seam bleed under GL_REPEAT because of bilinear-filter sampling at
texel boundaries). But it misclassified any *static* sky object whose
mesh UVs are deliberately authored outside [0,1] to tile the texture
across the geometry.
The smoking gun: GfxObj 0x010015EF is OI-1 in EVERY DayGroup (always
loaded), has TexVelocity = 0 (no scrolling), and authors UVs in
[0.398, 4.602] (texture tiles ~4× across each face). Under
CLAMP_TO_EDGE the bulk of the inner dome sampled the texture's edge
texels; only the small region where UVs happened to fall in [0,1]
showed actual texture content. Hence "a square in one corner".
Fix:
* GfxObjMesh.Build() now scans the resulting per-vertex UVs and sets
GfxObjSubMesh.NeedsUvRepeat true when any component lies outside
[0,1]. Mesh-time scan, not draw-time guess.
* SubMeshGpu carries the flag through to draw time.
* SkyRenderer uses `sub.NeedsUvRepeat || obj.TexVelocity != 0` to
decide REPEAT vs CLAMP_TO_EDGE. The dome (UVs in [0,1]) keeps
CLAMP — no seam regression. The inner star/sky layer 0x010015EF
(UVs outside [0,1]) gets REPEAT — texture tiles across the dome.
Cloud meshes (UVs outside [0,1] AND non-zero TexVelocity) keep
REPEAT via either branch.
Probe-driven: tools/StarsProbe (committed in 991fb9a) dumps every
SkyObject's geometry + UVs and flags meshes whose UV range exceeds
[0,1]. Run `dotnet run --project tools/StarsProbe -c Release` to
re-derive.
Verified visually by user against the live ACE server in Holtburg —
stars now stretch across the night sky instead of appearing as a
square in one corner. Build green, dotnet test 1222 pass.
Note: this is functionally retail-equivalent for the reported bug but
not the exact retail mechanism. Retail's GameSky::Draw at 0x00506ff0
relies on D3D's global default D3DTADDRESS_WRAP (i.e. REPEAT
everywhere). True retail-faithfulness would require investigating why
our pipeline shows seams on the dome under REPEAT (likely a bilinear
filter / non-seamless texture detail). The data-driven approach taken
here preserves working dome behavior while fixing the broken star
behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
991fb9a222
commit
7b88fde52d
3 changed files with 68 additions and 11 deletions
|
|
@ -222,17 +222,31 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
// Sky meshes need per-object wrap mode. The dome is 5 flat
|
||||
// walls meeting at edges — under GL_REPEAT any UV drift
|
||||
// past [0,1] wraps to the opposite edge of the texture,
|
||||
// drawing a visible line along each wall seam. Static
|
||||
// sky GfxObjs (dome, sun, moon, stars) should use
|
||||
// CLAMP_TO_EDGE to avoid that bleed. Scrolling cloud
|
||||
// layers (TexVelocity != 0) still need REPEAT so the
|
||||
// animated UV offset wraps correctly. Detection heuristic:
|
||||
// non-zero TexVelocity on either axis ⇒ scrolling layer.
|
||||
bool scrolling = obj.TexVelocityX != 0f || obj.TexVelocityY != 0f;
|
||||
int wrapMode = scrolling
|
||||
// Sky meshes need per-object wrap mode driven by the
|
||||
// mesh's authored UV range, not by TexVelocity:
|
||||
// * The outer dome (0x010015EE/F0/F1/F2) authors UVs
|
||||
// strictly in [0,1]. Under GL_REPEAT the bilinear
|
||||
// filter at wall-seam edges would average a texel
|
||||
// near the right edge with one near the left edge of
|
||||
// the texture, drawing a visible "bleed line" along
|
||||
// every dome seam. CLAMP_TO_EDGE avoids that.
|
||||
// * The inner sky/star layer (0x010015EF) and the
|
||||
// cloud meshes (0x010015B6, 0x01004C36 etc) author
|
||||
// UVs that deliberately exceed [0,1] (~0.4..4.6) so
|
||||
// the texture tiles across the geometry. CLAMP_TO_EDGE
|
||||
// would clamp ~99% of the surface to a single edge
|
||||
// texel, leaving only a small "square" where UVs
|
||||
// happen to fall in [0,1] (Bug B in
|
||||
// docs/research/2026-04-26-sky-investigation-handoff.md).
|
||||
// The mesh builder pre-computes NeedsUvRepeat from the
|
||||
// actual UV range so the right answer is data-driven.
|
||||
// Scrolling clouds are also forced to REPEAT (the running
|
||||
// UV offset can drift outside [0,1] regardless of authored
|
||||
// range, and they'd show their own seam bleed otherwise).
|
||||
bool needsRepeat = sub.NeedsUvRepeat
|
||||
|| obj.TexVelocityX != 0f
|
||||
|| obj.TexVelocityY != 0f;
|
||||
int wrapMode = needsRepeat
|
||||
? (int)TextureWrapMode.Repeat
|
||||
: (int)TextureWrapMode.ClampToEdge;
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, wrapMode);
|
||||
|
|
@ -423,6 +437,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
SurfaceId = sm.SurfaceId,
|
||||
IsAdditive = isAdditive,
|
||||
SurfLuminosity = sm.Luminosity,
|
||||
NeedsUvRepeat = sm.NeedsUvRepeat,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -462,5 +477,15 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6.
|
||||
/// </summary>
|
||||
public float SurfLuminosity;
|
||||
/// <summary>
|
||||
/// True when the source mesh's authored UVs exceed [0,1] (e.g.
|
||||
/// the inner sky/star layer 0x010015EF and the cloud meshes —
|
||||
/// they tile their texture across the geometry). The renderer
|
||||
/// must use <c>GL_REPEAT</c> for these or only the small region
|
||||
/// where UVs fall in [0,1] samples the actual texture; the rest
|
||||
/// clamps to the edge texel ("square in one corner" symptom).
|
||||
/// Computed once at mesh build from the actual UV range.
|
||||
/// </summary>
|
||||
public bool NeedsUvRepeat;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,23 @@ public static class GfxObjMesh
|
|||
}
|
||||
}
|
||||
|
||||
// Authored UV range determines the wrap-mode choice in the
|
||||
// sky pass. A mesh whose UVs are strictly in [0,1] (e.g. the
|
||||
// outer dome 0x010015EE) wants CLAMP_TO_EDGE to avoid
|
||||
// bilinear-filter bleed at the wall-seam edges; a mesh whose
|
||||
// UVs deliberately tile (e.g. 0x010015EF, ~0.4..4.6) wants
|
||||
// REPEAT so the texture tiles across the geometry. We make
|
||||
// the call data-driven here rather than guessing from
|
||||
// TexVelocity at draw time. See
|
||||
// docs/research/2026-04-26-sky-investigation-handoff.md (Bug B).
|
||||
bool needsUvRepeat = false;
|
||||
foreach (var v in kvp.Value.Vertices)
|
||||
{
|
||||
if (v.TexCoord.X < 0f || v.TexCoord.X > 1f
|
||||
|| v.TexCoord.Y < 0f || v.TexCoord.Y > 1f)
|
||||
{ needsUvRepeat = true; break; }
|
||||
}
|
||||
|
||||
result.Add(new GfxObjSubMesh(
|
||||
SurfaceId: surfaceId,
|
||||
Vertices: kvp.Value.Vertices.ToArray(),
|
||||
|
|
@ -217,6 +234,7 @@ public static class GfxObjMesh
|
|||
{
|
||||
Translucency = translucency,
|
||||
Luminosity = luminosity,
|
||||
NeedsUvRepeat = needsUvRepeat,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -39,4 +39,18 @@ public sealed record GfxObjSubMesh(
|
|||
/// normal lighting path without change.
|
||||
/// </summary>
|
||||
public float Luminosity { get; init; } = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// True when at least one vertex's UV component lies outside the
|
||||
/// <c>[0, 1]</c> range, meaning the mesh was authored to have its
|
||||
/// texture tile across the geometry (i.e. it expects
|
||||
/// <c>GL_REPEAT</c>/<c>D3DTADDRESS_WRAP</c>). The sky renderer reads
|
||||
/// this to decide between <c>GL_REPEAT</c> (this flag set, or any
|
||||
/// scrolling layer) and <c>GL_CLAMP_TO_EDGE</c> (all UVs strictly
|
||||
/// in <c>[0,1]</c>), which avoids wall-seam bleed on the dome
|
||||
/// (UVs in <c>[0,1]</c>) while still tiling the inner star/cloud
|
||||
/// layers (UVs in <c>[~0.4, ~4.6]</c>) correctly.
|
||||
/// Defaults to false so non-sky consumers get the previous behavior.
|
||||
/// </summary>
|
||||
public bool NeedsUvRepeat { get; init; } = false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue