sky(phase-2): retail-verbatim per-vertex lighting via Surface.Luminosity
Phase 2 of the sky port. Empirically confirmed from the Phase 1 dump
(ACDREAM_DUMP_SKY=1 on the live Dereth region): retail distinguishes
self-illuminated sky meshes from lit ones by the `Surface.Luminosity`
FLOAT field (0..1), NOT by the `SurfaceType.Luminous` flag bit (none of
Dereth's sky meshes have the flag set).
Observed values on the 4 currently-visible sky GfxObjs:
GfxObj 0x010015EE (dome, 4 surfaces) Luminosity = 1.0
GfxObj 0x010015EF (upper cloud) Luminosity = 0.0
GfxObj 0x01004C36 (lower drift cloud) Luminosity = 0.0
GfxObj 0x01001348 (sun/moon additive) Luminosity = 1.0
Retail uses this as an emissive coefficient in the per-vertex lighting
formula (decompiled chunk_00500000.c:7535 FUN_00508010 + chunk_00530000.c
AdjustPlanes per-vertex math):
tint = clamp(vec3(Luminosity) + AmbColor*AmbBright
+ max(dot(N, -sunDir), 0) * DirColor*DirBright,
0.0, 1.0)
fragment = texture * tint
When Luminosity=1.0 the clamp saturates → full texture brightness
regardless of time of day (dome gradient preserved; sun/moon always
bright). When Luminosity=0.0 only the ambient + diffuse term drives the
tint, so clouds pick up the time-of-day ambient (purple at midnight
per AmbColor=(200,100,255)×AmbBright=0.4 ≈ (0.31,0.16,0.40); warm tan
at dusk; pale-cool at noon).
Also empirically confirmed: raw SkyObjectReplace Transparent/Luminosity
/MaxBright are in 0..100 percent range (observed 11, 15, 22, 66, 100,
and -1 sentinel). The `/100` divide in SkyDescLoader (eeae83a) is
retail-correct; `_DAT_007a1870` in the decompile must be 0.01f.
Code changes:
- src/AcDream.Core/Meshing/GfxObjSubMesh.cs: new `Luminosity` field on
the per-submesh record (0..1, defaults to 0 for non-sky meshes).
- src/AcDream.Core/Meshing/GfxObjMesh.cs: pull Surface.Luminosity when
building submeshes (alongside existing Translucency capture).
- src/AcDream.App/Rendering/Sky/SkyRenderer.cs:
- SubMeshGpu gains SurfLuminosity, propagated from GfxObjSubMesh.
- Render() pushes uAmbientColor/uSunColor/uSunDir once per frame from
the interpolated keyframe; uEmissive once per submesh.
- uTint uniform removed (replaced by the vTint varying computed in
the vertex shader).
- src/AcDream.App/Rendering/Shaders/sky.vert: computes vTint per-vertex
using the retail AdjustPlanes formula.
- src/AcDream.App/Rendering/Shaders/sky.frag: consumes vTint, drops
uTint uniform. uLuminosity (the per-keyframe SkyObjectReplace
override) still applied as a final scalar multiply.
Expected visual difference from Phase 1 baseline:
- Dome gradient: IDENTICAL (Luminosity=1 saturates).
- Sun / moon: IDENTICAL (Luminosity=1 saturates, additive blend).
- Clouds: now tinted by time of day. Midnight → purple haze. Noon →
pale cool. Dusk → warm tan.
Open questions (unchanged from Phase 1 doc):
- Does the 15s LightTickSize throttling need porting? Phase 3.
- Does FUN_00532440 (AdjustPlanes per-cell terrain relight) need
porting for non-sky geometry to follow the sky? Phase 3.
Build + 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
58afd4850f
commit
aa2e20a42e
5 changed files with 152 additions and 78 deletions
|
|
@ -192,13 +192,22 @@ public static class GfxObjMesh
|
|||
|
||||
// Resolve Surface.Type flags when a DatCollection is available
|
||||
// so the renderer can split the draw into opaque and translucent
|
||||
// passes.
|
||||
// passes. Also capture Surface.Luminosity (self-illumination
|
||||
// coefficient — the FLOAT field, NOT the SurfaceType.Luminous
|
||||
// flag bit). This is the retail signal used to make the sky
|
||||
// dome / sun / moon texture-passthrough while clouds pick up
|
||||
// the time-of-day ambient tint (see
|
||||
// docs/research/2026-04-23-sky-retail-verbatim.md §6).
|
||||
var translucency = TranslucencyKind.Opaque;
|
||||
var luminosity = 0f;
|
||||
if (dats is not null)
|
||||
{
|
||||
var surface = dats.Get<Surface>(surfaceId);
|
||||
if (surface is not null)
|
||||
{
|
||||
translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type);
|
||||
luminosity = surface.Luminosity;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new GfxObjSubMesh(
|
||||
|
|
@ -207,6 +216,7 @@ public static class GfxObjMesh
|
|||
Indices: kvp.Value.Indices.ToArray())
|
||||
{
|
||||
Translucency = translucency,
|
||||
Luminosity = luminosity,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -18,4 +18,25 @@ public sealed record GfxObjSubMesh(
|
|||
/// that don't supply dat access compile and pass unchanged.
|
||||
/// </summary>
|
||||
public TranslucencyKind Translucency { get; init; } = TranslucencyKind.Opaque;
|
||||
|
||||
/// <summary>
|
||||
/// Self-illumination strength of the Surface (<c>Surface.Luminosity</c>
|
||||
/// field, 0..1 fraction — NOT the <c>SurfaceType.Luminous</c> flag bit).
|
||||
/// Retail uses this as an emissive coefficient in the per-vertex
|
||||
/// lighting formula:
|
||||
/// <code>
|
||||
/// tint = clamp(vec3(Luminosity) + AmbColor + diffuse * DirColor, 0, 1)
|
||||
/// fragment = texture * tint
|
||||
/// </code>
|
||||
/// For Dereth's sky meshes, the DOME (0x010015EE) and SUN/MOON
|
||||
/// (0x01001348) have <c>Luminosity=1.0</c> (self-illuminated — emissive
|
||||
/// saturates the lighting math so the baked texture always renders
|
||||
/// at full brightness). CLOUDS (0x010015EF, 0x01004C36) have
|
||||
/// <c>Luminosity=0.0</c> (lit by ambient+diffuse — pick up the
|
||||
/// time-of-day tint). See
|
||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6.
|
||||
/// Defaults to 0.0 (fully lit) so non-sky meshes render through the
|
||||
/// normal lighting path without change.
|
||||
/// </summary>
|
||||
public float Luminosity { get; init; } = 0f;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue