acdream/src/AcDream.App/Rendering/Shaders/sky.vert
Erik aa2e20a42e 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>
2026-04-23 18:19:22 +02:00

57 lines
2.5 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#version 430 core
// Sky mesh vertex shader — computes the per-vertex lighting tint that
// gives retail its time-of-day variation:
//
// tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor,
// 0.0, 1.0)
//
// This is the retail-verbatim AdjustPlanes formula ported from the
// decompiled D3D fixed-function lighting. The `emissive` scalar is the
// Surface.Luminosity FLOAT field (NOT the SurfaceType.Luminous flag bit) —
// for Dereth's sky meshes, the DOME + SUN/MOON have emissive=1.0
// (texture-passthrough regardless of lighting), while CLOUDS have
// emissive=0.0 (lit normally, so they pick up the ambient tint that
// produces retail's purple-haze night / warm-tan dusk / pale-cool noon).
//
// See docs/research/2026-04-23-sky-retail-verbatim.md §6 for the full
// decompile trail and field citations.
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTex;
uniform mat4 uModel; // per-object arc transform
uniform mat4 uSkyView; // camera view with M41..M43 = 0
uniform mat4 uSkyProjection; // near=0.1, far=1e6
uniform vec2 uUvScroll; // cumulative TexVelocityX/Y * time
// Per-frame lighting — keyframe-interpolated values pushed before each
// Render() call. AmbColor × AmbBright and DirColor × DirBright are
// pre-multiplied by SkyDescLoader so the shader feeds them straight in.
uniform vec3 uAmbientColor; // AmbColor × AmbBright
uniform vec3 uSunColor; // DirColor × DirBright
uniform vec3 uSunDir; // unit vector FROM surface TO sun
// Per-submesh: Surface.Luminosity (0..1 self-illumination scalar).
uniform float uEmissive;
out vec2 vTex;
out vec3 vTint;
void main() {
vTex = aTex + uUvScroll;
gl_Position = uSkyProjection * uSkyView * uModel * vec4(aPos, 1.0);
// uModel for sky is scale * RotZ(-heading) * RotY(-rotation) — pure
// orthonormal rotation, so mat3(uModel) correctly transforms normals
// without needing transpose(inverse(...)).
vec3 worldNormal = normalize(mat3(uModel) * aNormal);
// Per-vertex lighting. `emissive` is broadcast scalar → vec3. For
// emissive=1.0 the clamp saturates to white regardless of ambient/sun
// (the retail "unlit" mesh). For emissive=0.0 only the ambient + sun
// term drives `tint` (the retail "lit" mesh, e.g. clouds).
float diff = max(dot(worldNormal, uSunDir), 0.0);
vec3 lit = vec3(uEmissive) + uAmbientColor + diff * uSunColor;
vTint = clamp(lit, 0.0, 1.0);
}