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>
57 lines
2.5 KiB
GLSL
57 lines
2.5 KiB
GLSL
#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);
|
||
}
|