feat(A7): LightBake Core — verified per-vertex static-light burn-in (foundation, not wired)
The faithful fix for the spotty dungeon/house/outdoor lighting is retail's per-vertex static-light bake (D3DPolyRender::SetStaticLightingVertexColors 0x0059cfe0), NOT a per-pixel ramp. This lands the GL-free Core: LightBake.PointContribution / ComputeVertexColor port calc_point_light (0x0059c8b0) VERBATIM — verified against a clean Ghidra decompile (the BN pseudo-C is x87-mangled): half-Lambert wrap with LIGHT_POINT_RANGE=0.75 (0x007e5430), the distsq>1 norm branch, the per-channel min-to-color clamp, and the final [0,1] clamp. static_light_factor=1.3 (0x00820e24) is already folded into LightSource.Range by LightInfoLoader. 7 conformance tests (hand-derived golden values) green. NOT wired yet — the integration (a per-vertex colour attribute on the cell mesh + the bake driver keyed on envCellId + the shader consumption) is the remaining A7 work; see ISSUES.md A7. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e641339e9
commit
3b93f91ebe
2 changed files with 210 additions and 0 deletions
101
src/AcDream.Core/Lighting/LightBake.cs
Normal file
101
src/AcDream.Core/Lighting/LightBake.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Lighting;
|
||||
|
||||
/// <summary>
|
||||
/// Retail per-vertex static-light burn-in. Ported verbatim from
|
||||
/// <c>calc_point_light</c> (acclient 0x0059c8b0), the function retail's
|
||||
/// <c>D3DPolyRender::SetStaticLightingVertexColors</c> (0x0059cfe0) runs over
|
||||
/// EVERY vertex of an EnvCell mesh × EVERY reaching static light, baking the
|
||||
/// result into the vertex diffuse colour ONCE (then the rasteriser Gouraud-
|
||||
/// interpolates it across each triangle and the texture stage modulates it).
|
||||
///
|
||||
/// <para>
|
||||
/// This is the faithful answer to the dungeon "spotlight" look (#133 A7): our
|
||||
/// old per-pixel nearest-8 path lit only the 8 torches nearest the CAMERA and
|
||||
/// re-ranked them every frame (the sliding crescent). The retail bake sums ALL
|
||||
/// reaching lights into the vertex once, keyed on light position not camera —
|
||||
/// uniform, stable, and never blown out (each light is clamped to its own
|
||||
/// colour, then the vertex sum is clamped to [0,1]).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>Constants (decomp-cited, not guessed):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>static_light_factor</c> = 1.3 (0x00820e24) — folded into
|
||||
/// <see cref="LightSource.Range"/> by <c>LightInfoLoader</c>, so
|
||||
/// <c>falloff_eff == light.Range</c> here.</item>
|
||||
/// <item><c>LIGHT_POINT_RANGE</c> = 0.75 (0x007e5430) — the half-Lambert wrap
|
||||
/// uses <c>2·LPR = 1.5</c> as the divisor and <c>(2·LPR − 1) = 0.5</c> as the
|
||||
/// distance bias, so even surfaces angled away from a torch receive some light.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static class LightBake
|
||||
{
|
||||
// calc_point_light literals.
|
||||
private const float TwoLpr = 1.5f; // LIGHT_POINT_RANGE + LIGHT_POINT_RANGE
|
||||
private const float WrapBias = 0.5f; // (2 · LIGHT_POINT_RANGE) − 1.0
|
||||
|
||||
/// <summary>
|
||||
/// Accumulate one static light's contribution into a per-vertex RGB sum,
|
||||
/// exactly as <c>calc_point_light</c> does. Returns the contribution to ADD
|
||||
/// (already per-channel clamped to the light's own colour); the caller sums
|
||||
/// over all reaching lights and clamps the total to [0,1].
|
||||
/// </summary>
|
||||
public static Vector3 PointContribution(
|
||||
Vector3 vtxWorldPos, Vector3 vtxWorldNormal, LightSource light)
|
||||
{
|
||||
// D = light − vertex (FROM vertex TO light), used un-normalised.
|
||||
float dx = light.WorldPosition.X - vtxWorldPos.X;
|
||||
float dy = light.WorldPosition.Y - vtxWorldPos.Y;
|
||||
float dz = light.WorldPosition.Z - vtxWorldPos.Z;
|
||||
|
||||
float distsq = dx * dx + dy * dy + dz * dz;
|
||||
float dist = MathF.Sqrt(distsq);
|
||||
float falloffEff = light.Range; // = Falloff × static_light_factor(1.3)
|
||||
if (dist >= falloffEff || falloffEff <= 1e-4f)
|
||||
return Vector3.Zero;
|
||||
|
||||
// Half-Lambert wrap: (1/1.5)·(N·D + 0.5·dist), N un-normalised vertex normal.
|
||||
float wrap = (1f / TwoLpr) *
|
||||
(vtxWorldNormal.X * dx + vtxWorldNormal.Y * dy + vtxWorldNormal.Z * dz
|
||||
+ WrapBias * dist);
|
||||
if (wrap <= 0f)
|
||||
return Vector3.Zero;
|
||||
|
||||
// norm branch — ported EXACTLY (changes the near-vs-far falloff shape).
|
||||
float norm = distsq > 1f ? distsq * dist : dist;
|
||||
float scale = (1f - dist / falloffEff) * light.Intensity * (wrap / norm);
|
||||
|
||||
// Per channel: contribution clamped to the light's own colour (a single
|
||||
// light can never push a channel past its colour — the no-blowout ceiling).
|
||||
return new Vector3(
|
||||
MathF.Min(scale * light.ColorLinear.X, light.ColorLinear.X),
|
||||
MathF.Min(scale * light.ColorLinear.Y, light.ColorLinear.Y),
|
||||
MathF.Min(scale * light.ColorLinear.Z, light.ColorLinear.Z));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bake the full per-vertex colour by summing every reaching lit point/spot
|
||||
/// light, then clamping to [0,1] (the <c>SetStaticLightingVertexColors</c>
|
||||
/// final clamp). Directional lights are skipped — they are handled by the
|
||||
/// sun path, not the static burn-in.
|
||||
/// </summary>
|
||||
public static Vector3 ComputeVertexColor(
|
||||
Vector3 vtxWorldPos, Vector3 vtxWorldNormal, IReadOnlyList<LightSource> reaching)
|
||||
{
|
||||
float r = 0f, g = 0f, b = 0f;
|
||||
for (int i = 0; i < reaching.Count; i++)
|
||||
{
|
||||
var light = reaching[i];
|
||||
if (!light.IsLit || light.Kind == LightKind.Directional) continue;
|
||||
var c = PointContribution(vtxWorldPos, vtxWorldNormal, light);
|
||||
r += c.X; g += c.Y; b += c.Z;
|
||||
}
|
||||
return new Vector3(
|
||||
Math.Clamp(r, 0f, 1f),
|
||||
Math.Clamp(g, 0f, 1f),
|
||||
Math.Clamp(b, 0f, 1f));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue