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
|
|
@ -1,24 +1,28 @@
|
|||
#version 430 core
|
||||
// Sky mesh fragment shader — sample the object's diffuse texture with
|
||||
// the scrolled UVs from the vertex stage. Unlit: sky meshes ARE the
|
||||
// gradient (r12 §2.2), not a surface lit by the sun.
|
||||
// Sky mesh fragment shader — retail-verbatim composite:
|
||||
//
|
||||
// The per-keyframe replace override can dim the mesh (Transparent) or
|
||||
// brighten it (Luminosity); those two floats arrive as uTransparency /
|
||||
// uLuminosity uniforms.
|
||||
// fragment.rgb = texture.rgb * vTint * uLuminosity + lightning_flash
|
||||
// fragment.a = texture.a * (1 - uTransparency)
|
||||
//
|
||||
// vTint arrives from the vertex shader with retail's per-vertex lighting
|
||||
// baked in (emissive + ambient + diffuse × sun, clamped to [0,1]).
|
||||
// uLuminosity is the per-keyframe SkyObjectReplace override (0..1
|
||||
// fraction after the /100 scale in SkyDescLoader) — NOT to be confused
|
||||
// with the Surface.Luminosity that feeds uEmissive in the vertex shader.
|
||||
// uTransparency is the per-keyframe SkyObjectReplace alpha-fade.
|
||||
//
|
||||
// See docs/research/2026-04-23-sky-retail-verbatim.md §6 + §7.
|
||||
|
||||
in vec2 vTex;
|
||||
in vec3 vTint;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2D uDiffuse;
|
||||
uniform float uTransparency; // 0 = fully visible, 1 = invisible
|
||||
uniform float uLuminosity; // 1 = normal, >1 makes the mesh glow
|
||||
uniform vec4 uTint; // per-object color tint (default white)
|
||||
uniform float uTransparency; // 0 = fully visible, 1 = fully transparent
|
||||
uniform float uLuminosity; // 1.0 = normal; <1 dims per SkyObjectReplace
|
||||
|
||||
// Shared SceneLighting UBO — we only need the fog parameters to let the
|
||||
// horizon band of the sky blend smoothly into the scene's fog color at
|
||||
// the far edge, and the lightning flash to give storms their signature
|
||||
// strobe.
|
||||
// Shared SceneLighting UBO — only need fog/flash channel for the
|
||||
// client-driven lightning strobe. Sun/ambient already baked into vTint.
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
|
|
@ -36,24 +40,20 @@ layout(std140, binding = 1) uniform SceneLighting {
|
|||
void main() {
|
||||
vec4 sampled = texture(uDiffuse, vTex);
|
||||
|
||||
// Apply tint + luminosity. Retail's SkyObjReplace.Luminosity can push
|
||||
// above 1 to make the sun mesh brighter than its texture; r12 §2.3.
|
||||
vec3 rgb = sampled.rgb * uTint.rgb * uLuminosity;
|
||||
// Composite: texture × per-vertex lighting × per-keyframe dim.
|
||||
vec3 rgb = sampled.rgb * vTint * uLuminosity;
|
||||
|
||||
// Lightning additive bump — makes the sky itself flash during storms.
|
||||
// Retail's lightning is a near-white strobe; a dim grey bump doesn't
|
||||
// read as lightning. Keep a faint blue tint so it still feels electric
|
||||
// rather than pure-white daylight.
|
||||
// Lightning additive bump (client-side during storm keyframes).
|
||||
float flash = uFogParams.z;
|
||||
rgb += flash * vec3(1.5, 1.5, 1.8);
|
||||
|
||||
// Soft clamp to let Luminosity/flash slightly over-bright. During a
|
||||
// lightning flash, raise the ceiling so the strobe actually blows out
|
||||
// instead of getting capped mid-rise.
|
||||
// Soft clamp. Normal frame: cap at 1.2 so emissive meshes have a
|
||||
// small highlight margin. During a flash the ceiling relaxes so the
|
||||
// strobe actually blows out instead of getting pinned mid-rise.
|
||||
float cap = mix(1.2, 3.0, clamp(flash, 0.0, 1.0));
|
||||
rgb = min(rgb, vec3(cap));
|
||||
|
||||
float a = sampled.a * (1.0 - uTransparency) * uTint.a;
|
||||
float a = sampled.a * (1.0 - uTransparency);
|
||||
if (a < 0.01) discard;
|
||||
fragColor = vec4(rgb, a);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,57 @@
|
|||
#version 430 core
|
||||
// Sky mesh vertex shader — each celestial object is a GfxObj mesh
|
||||
// (sun billboard, cloud sheet, moon, star dome) rendered at large
|
||||
// distance with depth writes disabled. The view matrix has its
|
||||
// translation zeroed so the sky stays camera-centered; the projection
|
||||
// matrix has a huge far plane so 1e6-metre-away sky meshes never clip.
|
||||
// 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 (r12 §2.1)
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,18 +74,21 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
/// terrain / meshes / debug lines / overlay land on top.
|
||||
///
|
||||
/// <para>
|
||||
/// <paramref name="keyframe"/> is accepted for forward-compatibility
|
||||
/// with the retail-verbatim per-vertex lighting path (see
|
||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c>). It is
|
||||
/// NOT currently consumed by the shader — sky meshes render at
|
||||
/// <c>uTint = white</c> (texture passthrough). A prior experiment
|
||||
/// multiplied alpha-blended submeshes by <c>keyframe.AmbientColor</c>
|
||||
/// to tint clouds; this dimmed the sky dome's baked gradient
|
||||
/// (user-verified regression) and was reverted. Retail actually
|
||||
/// routes sky meshes through the normal mesh pipeline with
|
||||
/// Surface.Type.Luminous controlling lit-vs-unlit per submesh; the
|
||||
/// correct port lives downstream in Phase 2 once we have the live
|
||||
/// Surface flags dumped.
|
||||
/// Each submesh renders with retail's per-vertex lighting formula:
|
||||
/// <c>tint = clamp(emissive + ambient + max(dot(N, -sunDir), 0) * sunColor, 0, 1)</c>
|
||||
/// where <c>emissive</c> is the submesh's <c>Surface.Luminosity</c>
|
||||
/// float (1.0 for dome + sun + moon → texture passthrough via
|
||||
/// saturation; 0.0 for clouds → get the full time-of-day tint).
|
||||
/// <paramref name="keyframe"/> supplies the AmbientColor and SunColor
|
||||
/// already pre-multiplied by AmbBright / DirBright (loader-side).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// See <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6 for
|
||||
/// the full decompile citation. The empirical Dereth dump (
|
||||
/// <c>ACDREAM_DUMP_SKY=1</c>, logged 2026-04-23) confirmed the
|
||||
/// <c>SurfaceType.Luminous</c> flag bit is NOT set on any Dereth sky
|
||||
/// mesh — the differentiator is the <c>Surface.Luminosity</c> FLOAT
|
||||
/// field.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void Render(
|
||||
|
|
@ -116,23 +119,24 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_shader.SetMatrix4("uSkyView", skyView);
|
||||
_shader.SetMatrix4("uSkyProjection", skyProj);
|
||||
|
||||
// Retail per-vertex lighting inputs (AdjustPlanes formula).
|
||||
// AmbColor/SunColor are already × AmbBright/DirBright from
|
||||
// SkyDescLoader. SunDir is the unit vector FROM surface TO sun
|
||||
// derived from the keyframe's DirHeading/DirPitch.
|
||||
_shader.SetVec3("uAmbientColor", keyframe.AmbientColor);
|
||||
_shader.SetVec3("uSunColor", keyframe.SunColor);
|
||||
_shader.SetVec3("uSunDir",
|
||||
AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe));
|
||||
|
||||
// Save + override GL state.
|
||||
_gl.DepthMask(false);
|
||||
_gl.Disable(EnableCap.DepthTest);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
// Default blend — overridden per-submesh inside the inner loop based
|
||||
// on the Surface's TranslucencyKind + Luminous flag. Sun/moon/stars
|
||||
// in retail use Additive (their texture has a black background and a
|
||||
// bright body painted on top; additive blending ignores the black and
|
||||
// lets the body glow over the sky gradient). Clouds use AlphaBlend.
|
||||
// Without per-object blend, sun renders as "black square with sun in
|
||||
// it" because our default alpha-blend treats the black background as
|
||||
// opaque. See SurfaceType enum (DatReaderWriter.Enums.SurfaceType):
|
||||
// Additive = 0x10000 → GL_ONE / GL_ONE
|
||||
// Luminous = 0x40 → additive for sky purposes
|
||||
// Alpha = 0x100 / Translucent = 0x10 → GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA
|
||||
// Base1ClipMap = 0x04 → alpha with shader discard
|
||||
// Default blend — overridden per-submesh inside the inner loop.
|
||||
// Additive surfaces (sun/moon/stars via SurfaceType.Additive =
|
||||
// 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
|
||||
// with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
|
||||
// Look up the keyframe's override list so we can apply
|
||||
|
|
@ -183,17 +187,6 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_shader.SetVec2("uUvScroll", new Vector2(uOffset, vOffset));
|
||||
_shader.SetFloat("uTransparency", transparent);
|
||||
_shader.SetFloat("uLuminosity", luminosity);
|
||||
// uTint stays white: retail renders sky meshes as texture
|
||||
// passthrough (the gradient lives in the mesh texture, not in
|
||||
// a shader ambient multiply). D3DRS_AMBIENT is set to 0 once
|
||||
// at retail device-init and never changes per-frame — verified
|
||||
// in chunk_005A0000.c (state 0x8b = 139, only external caller
|
||||
// is the default-reset at line 704). The "cloud tint" effect
|
||||
// comes from per-vertex lighting on non-Luminous submeshes
|
||||
// routed through the normal mesh pipeline. That path is
|
||||
// Phase 2 — see docs/research/2026-04-23-sky-retail-verbatim.md
|
||||
// §6 + §10 and the hunt-B finding at 2026-04-23-sky-decompile-hunt-B.md.
|
||||
_shader.SetVec4("uTint", Vector4.One);
|
||||
|
||||
EnsureMeshUploaded(gfxObjId);
|
||||
if (!_gpuByGfxObj.TryGetValue(gfxObjId, out var subMeshes)) continue;
|
||||
|
|
@ -202,20 +195,25 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
{
|
||||
// Per-submesh blend mode: sun/moon/stars are Additive
|
||||
// (SurfaceType.Additive = 0x10000), clouds are AlphaBlend,
|
||||
// sky dome is either Opaque or AlphaBlend depending on the
|
||||
// dat. We map Opaque to a passthrough SrcAlpha/InvSrcAlpha
|
||||
// with alpha=1, which is equivalent to not blending. This
|
||||
// split is architecturally correct (sun's additive blend
|
||||
// stops its black texture background from occluding the
|
||||
// sky dome behind it) but is NOT how retail does it —
|
||||
// retail routes sky meshes through the normal mesh pipe
|
||||
// where Surface flags dictate blend state per primitive.
|
||||
// See FUN_00508010 (chunk_00500000.c:7535).
|
||||
// sky dome is Base1Image (Opaque, mapped to
|
||||
// SrcAlpha/InvSrcAlpha for a no-op blend at alpha=1).
|
||||
// See FUN_00508010 (chunk_00500000.c:7535) for the retail
|
||||
// pattern — retail routes sky meshes through the normal
|
||||
// mesh pipeline where Surface flags dictate state.
|
||||
if (sub.IsAdditive)
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
|
||||
else
|
||||
_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
|
||||
|
||||
// Per-submesh emissive (Surface.Luminosity FLOAT field —
|
||||
// 1.0 for dome + sun + moon, 0.0 for clouds). The vertex
|
||||
// shader saturates the lighting math when emissive=1.0 so
|
||||
// self-illuminated meshes render at full texture brightness
|
||||
// regardless of time of day; emissive=0.0 meshes get the
|
||||
// full `ambient + diffuse × sun` tint (producing retail's
|
||||
// purple night clouds / warm dusk clouds / pale noon clouds).
|
||||
_shader.SetFloat("uEmissive", sub.SurfLuminosity);
|
||||
|
||||
uint tex = _textures.GetOrUpload(sub.SurfaceId);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
|
@ -403,6 +401,7 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
IndexCount = sm.Indices.Length,
|
||||
SurfaceId = sm.SurfaceId,
|
||||
IsAdditive = isAdditive,
|
||||
SurfLuminosity = sm.Luminosity,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -428,10 +427,19 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
public int IndexCount;
|
||||
public uint SurfaceId;
|
||||
/// <summary>
|
||||
/// True if the Surface's flags indicate additive blending should be
|
||||
/// used (SurfaceType.Additive OR SurfaceType.Luminous). Computed
|
||||
/// once at upload; avoids a per-frame dat lookup.
|
||||
/// True if the Surface's <c>SurfaceType.Additive</c> flag (0x10000)
|
||||
/// is set. Drives the blend func switch (GL_ONE vs GL_ONE_MINUS_SRC_ALPHA).
|
||||
/// Computed once at upload; avoids a per-frame dat lookup.
|
||||
/// </summary>
|
||||
public bool IsAdditive;
|
||||
/// <summary>
|
||||
/// <c>Surface.Luminosity</c> float (0..1 — NOT the SurfaceType.Luminous
|
||||
/// flag bit). Passed to the sky fragment shader as <c>uEmissive</c>;
|
||||
/// when 1.0 it saturates the lighting math so the mesh renders at
|
||||
/// full texture brightness (dome, sun). When 0.0 the mesh picks up
|
||||
/// the time-of-day ambient+diffuse tint (clouds). See
|
||||
/// <c>docs/research/2026-04-23-sky-retail-verbatim.md</c> §6.
|
||||
/// </summary>
|
||||
public float SurfLuminosity;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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