merge: bring main (A7 lighting Fix A–D + UN-7 + #140 Fix D) into the D.5 branch
Integrates main's 19 commits (A7 outdoor/indoor torch lighting Fix A/B/C/D, GlobalLightPacker, shader updates, UN-7) under the D.5 toolbar/item-model stack (D.5.1/D.5.2/D.5.4/D.5.3a). Auto-merged cleanly except docs/ISSUES.md. Conflict resolved: both lineages used #140 for different issues. Kept main's #140 = "A7 Fix D" (resolved); renumbered the toolbar/selected-object issue to #141 (note added; this branch's commits/spec still reference #140 — immutable). The register auto-merged (AP-46 cites file:line, not #140; UN-7 keeps #140=Fix D). Build + full suite green on the merged tree (2,713 passed / 4 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
31d7ffd253
27 changed files with 2327 additions and 103 deletions
55
src/AcDream.Core/Lighting/GlobalLightPacker.cs
Normal file
55
src/AcDream.Core/Lighting/GlobalLightPacker.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.Core.Lighting;
|
||||
|
||||
/// <summary>
|
||||
/// Packs a point-light snapshot into the flat float layout the bindless mesh
|
||||
/// shader reads at SSBO binding=4 (<c>mesh_modern.vert</c> <c>GlobalLight gLights[]</c>):
|
||||
/// 16 floats (4 vec4) per light — posAndKind, dirAndRange, colorAndIntensity,
|
||||
/// coneAngleEtc. Pure (no GL), so both <c>WbDrawDispatcher</c> and
|
||||
/// <c>EnvCellRenderer</c> share ONE layout and cannot drift.
|
||||
/// </summary>
|
||||
public static class GlobalLightPacker
|
||||
{
|
||||
public const int FloatsPerLight = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Fill <paramref name="buffer"/> (grown + zero-cleared as needed) with the
|
||||
/// packed snapshot; returns the light count <c>n</c>. The buffer always has at
|
||||
/// least <see cref="FloatsPerLight"/> floats (so a zero-light frame still
|
||||
/// uploads a non-empty SSBO). Callers upload <c>max(n,1) * FloatsPerLight</c> floats.
|
||||
/// </summary>
|
||||
public static int Pack(IReadOnlyList<LightSource>? snapshot, ref float[] buffer)
|
||||
{
|
||||
int n = snapshot?.Count ?? 0;
|
||||
int floatsNeeded = Math.Max(n, 1) * FloatsPerLight;
|
||||
if (buffer.Length < floatsNeeded)
|
||||
buffer = new float[floatsNeeded + FloatsPerLight * 16];
|
||||
Array.Clear(buffer, 0, floatsNeeded);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var L = snapshot![i];
|
||||
int o = i * FloatsPerLight;
|
||||
// posAndKind (xyz world pos, w kind)
|
||||
buffer[o + 0] = L.WorldPosition.X;
|
||||
buffer[o + 1] = L.WorldPosition.Y;
|
||||
buffer[o + 2] = L.WorldPosition.Z;
|
||||
buffer[o + 3] = (int)L.Kind;
|
||||
// dirAndRange (xyz forward, w range)
|
||||
buffer[o + 4] = L.WorldForward.X;
|
||||
buffer[o + 5] = L.WorldForward.Y;
|
||||
buffer[o + 6] = L.WorldForward.Z;
|
||||
buffer[o + 7] = L.Range; // w = Range = Falloff × static_light_factor (1.3), pre-multiplied by LightInfoLoader — NOT the raw dat Falloff
|
||||
// colorAndIntensity (xyz linear colour, w intensity)
|
||||
buffer[o + 8] = L.ColorLinear.X;
|
||||
buffer[o + 9] = L.ColorLinear.Y;
|
||||
buffer[o + 10] = L.ColorLinear.Z;
|
||||
buffer[o + 11] = L.Intensity;
|
||||
// coneAngleEtc (x cone radians; yzw reserved)
|
||||
buffer[o + 12] = L.ConeAngle;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
|
@ -157,4 +157,125 @@ public sealed class LightManager
|
|||
|
||||
_activeCount = baseSlot + filled;
|
||||
}
|
||||
|
||||
// ── Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting ──
|
||||
//
|
||||
// The single global nearest-8-to-VIEWER set above (Tick) is camera-relative:
|
||||
// a wall's brightness changes as the camera moves because the wall's torches
|
||||
// swap in/out of that global top-8. Retail instead picks up-to-8 lights PER
|
||||
// OBJECT by the OBJECT's own position (minimize_object_lighting, 0x0054d480),
|
||||
// so a torch always lights the wall it sits on, camera-independent. The two
|
||||
// members below feed the per-instance light path in WbDrawDispatcher; Tick
|
||||
// remains the source of the legacy single-UBO path + the sun slot.
|
||||
|
||||
/// <summary>Max point/spot lights any one object can be lit by — retail's
|
||||
/// D3D fixed-function 8-light cap (<c>minimize_object_lighting</c>). The sun
|
||||
/// is global, not part of an object's per-object set, so all 8 are point/spot.</summary>
|
||||
public const int MaxLightsPerObject = 8;
|
||||
|
||||
/// <summary>Hard cap on the per-frame global point-light snapshot the shader
|
||||
/// indexes. AC scenes rarely exceed a few dozen lit point lights in view; 128
|
||||
/// is generous. If exceeded, the nearest-to-camera are kept (cold path).</summary>
|
||||
public const int MaxGlobalLights = 128;
|
||||
|
||||
private readonly List<LightSource> _pointSnapshot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame snapshot of lit point/spot lights, stable-indexed for the global
|
||||
/// shader light buffer and for per-object selection: the index of a light here
|
||||
/// IS the index the per-instance light-set SSBO references. Built by
|
||||
/// <see cref="BuildPointLightSnapshot"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyList<LightSource> PointSnapshot => _pointSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild <see cref="PointSnapshot"/> from the registered lit point/spot
|
||||
/// lights. The sun and unlit lights are excluded (the sun is global ambient-
|
||||
/// path; unlit torches contribute nothing). When more than
|
||||
/// <see cref="MaxGlobalLights"/> qualify, keeps the nearest the camera so the
|
||||
/// most relevant lights survive the cap. Call once per frame before
|
||||
/// per-object selection.
|
||||
/// </summary>
|
||||
public void BuildPointLightSnapshot(Vector3 cameraWorldPos)
|
||||
{
|
||||
_pointSnapshot.Clear();
|
||||
foreach (var light in _all)
|
||||
{
|
||||
if (!light.IsLit || light.Kind == LightKind.Directional) continue;
|
||||
light.DistSq = (light.WorldPosition - cameraWorldPos).LengthSquared();
|
||||
_pointSnapshot.Add(light);
|
||||
}
|
||||
if (_pointSnapshot.Count > MaxGlobalLights)
|
||||
{
|
||||
_pointSnapshot.Sort(static (a, b) => a.DistSq.CompareTo(b.DistSq));
|
||||
_pointSnapshot.RemoveRange(MaxGlobalLights, _pointSnapshot.Count - MaxGlobalLights);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select up to <see cref="MaxLightsPerObject"/> point/spot lights from
|
||||
/// <paramref name="snapshot"/> that reach the object sphere
|
||||
/// (<paramref name="center"/>, <paramref name="radius"/>), nearest-first.
|
||||
/// Faithful to retail's <c>minimize_object_lighting</c> (0x0054d480): a light
|
||||
/// is a candidate iff its falloff sphere overlaps the object sphere —
|
||||
/// <c>(light.pos − center)² < (light.Range + radius)²</c> — and when more
|
||||
/// than 8 candidates qualify, the 8 NEAREST the object centre are kept (the
|
||||
/// farthest fall off). <paramref name="light.Range"/> already folds
|
||||
/// <c>static_light_factor</c> (1.3), matching the per-vertex cutoff so a
|
||||
/// selected light always actually contributes in the shader.
|
||||
/// <para>
|
||||
/// Writes indices INTO <paramref name="snapshot"/> to
|
||||
/// <paramref name="outIndices"/> (ascending by distance) and returns the count.
|
||||
/// Pure + static: camera-INDEPENDENT (depends only on the object centre), so a
|
||||
/// static object's set is stable and may be computed once. Unit-testable
|
||||
/// without GL.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static int SelectForObject(
|
||||
IReadOnlyList<LightSource> snapshot,
|
||||
Vector3 center,
|
||||
float radius,
|
||||
Span<int> outIndices)
|
||||
{
|
||||
int cap = Math.Min(outIndices.Length, MaxLightsPerObject);
|
||||
if (cap <= 0) return 0;
|
||||
|
||||
Span<float> keptDistSq = stackalloc float[MaxLightsPerObject];
|
||||
int count = 0;
|
||||
|
||||
for (int li = 0; li < snapshot.Count; li++)
|
||||
{
|
||||
var light = snapshot[li];
|
||||
float reach = light.Range + radius;
|
||||
float dsq = (light.WorldPosition - center).LengthSquared();
|
||||
if (dsq >= reach * reach) continue; // light's sphere doesn't reach the object
|
||||
|
||||
if (count < cap)
|
||||
{
|
||||
int j = count;
|
||||
while (j > 0 && keptDistSq[j - 1] > dsq)
|
||||
{
|
||||
keptDistSq[j] = keptDistSq[j - 1];
|
||||
outIndices[j] = outIndices[j - 1];
|
||||
j--;
|
||||
}
|
||||
keptDistSq[j] = dsq;
|
||||
outIndices[j] = li;
|
||||
count++;
|
||||
}
|
||||
else if (dsq < keptDistSq[cap - 1])
|
||||
{
|
||||
int j = cap - 1;
|
||||
while (j > 0 && keptDistSq[j - 1] > dsq)
|
||||
{
|
||||
keptDistSq[j] = keptDistSq[j - 1];
|
||||
outIndices[j] = outIndices[j - 1];
|
||||
j--;
|
||||
}
|
||||
keptDistSq[j] = dsq;
|
||||
outIndices[j] = li;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,22 +74,15 @@ public readonly record struct SkyKeyframe(
|
|||
/// (see <see cref="SkyStateProvider.RetailSunVector"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// Why <c>|sunVec|</c> instead of <c>DirBright</c> directly: retail's
|
||||
/// <c>PrimD3DRender::UpdateLightsInternal</c> at <c>0x0059b57c</c>
|
||||
/// (decomp line 424118-424119) computes
|
||||
/// <code>D3DLIGHT9.Diffuse.r = sunlight_color.r × sqrt(x²+y²+z²)</code>
|
||||
/// from the sun vector <c>SkyDesc::GetLighting</c> built at
|
||||
/// <c>0x00500ac9</c> (decomp lines 261343-261353):
|
||||
/// <code>
|
||||
/// sunVec.x = sin(H) × DirBright × cos(P)
|
||||
/// sunVec.y = cos(P) // NOT scaled by DirBright
|
||||
/// sunVec.z = DirBright × sin(P)
|
||||
/// </code>
|
||||
/// Because Y is unscaled by <c>DirBright</c>, <c>|sunVec|</c> ≠
|
||||
/// <c>DirBright</c> in general — it varies with sun pitch and heading.
|
||||
/// Using <c>DirBright</c> alone underweighted the warm directional
|
||||
/// term, letting the cool ambient/fog dominate ⇒ acdream rendered
|
||||
/// blue-white at keyframes where retail looked warm-gray.
|
||||
/// <c>|sunVec|</c> is retail's <c>D3DLIGHT9.Diffuse = DirColor × sqrt(x²+y²+z²)</c>
|
||||
/// scaling (<c>PrimD3DRender::UpdateLightsInternal</c> 0x0059b57c, decomp
|
||||
/// 424118-424119) of the WORLD-space sun vector (<c>LScape::sunlight</c>).
|
||||
/// Because <see cref="SkyStateProvider.RetailSunVector"/> is now the
|
||||
/// DirBright-scaled spherical vector (magnitude == DirBright, cdb-verified —
|
||||
/// see that method), <c>|sunVec| == DirBright</c>, so this is effectively
|
||||
/// <c>SunColor = DirColor × DirBright</c>. (A prior bug used the un-transformed
|
||||
/// y=cos(P) vector ⇒ |sunVec|≈1.06 ⇒ the sun was ~4–5× too bright at dawn/dusk;
|
||||
/// [[reference-retail-ambient-values]].)
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Vector3 SunColor => DirColor * SkyStateProvider.RetailSunVector(this).Length();
|
||||
|
|
@ -301,21 +294,35 @@ public sealed class SkyStateProvider
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail's raw sun vector (NOT normalized) — the same vector
|
||||
/// <c>SkyDesc::GetLighting</c> writes at <c>0x00500ac9</c>
|
||||
/// (decomp lines 261343, 261352, 261353):
|
||||
/// Retail's world-space sun vector (NOT normalized): the standard
|
||||
/// spherical-to-cartesian direction (East=x, North=y, Up=z) scaled by
|
||||
/// <c>DirBright</c>:
|
||||
/// <code>
|
||||
/// sunVec.x = sin(H_rad) × DirBright × cos(P_rad)
|
||||
/// sunVec.y = cos(P_rad) // NOT scaled by DirBright
|
||||
/// sunVec.z = DirBright × sin(P_rad)
|
||||
/// sunVec.x = DirBright × cos(P) × sin(H)
|
||||
/// sunVec.y = DirBright × cos(P) × cos(H)
|
||||
/// sunVec.z = DirBright × sin(P)
|
||||
/// </code>
|
||||
/// Y is unscaled by brightness on purpose — that's what makes
|
||||
/// <c>|sunVec|</c> ≠ <c>DirBright</c> in general (the magnitude varies
|
||||
/// with pitch/heading, which is the basis for retail's "sun is brighter
|
||||
/// in some configurations than others" lighting behavior). The shader's
|
||||
/// <c>uSunDir</c> uniform uses the NORMALIZED vector for N·L; the
|
||||
/// magnitude feeds <see cref="SkyKeyframe.SunColor"/> intensity and
|
||||
/// the ambient brightness boost in <see cref="SkyKeyframe.AmbientColor"/>.
|
||||
/// so <c>|sunVec| == DirBright</c> exactly (cos²P·(sin²H+cos²H)+sin²P = 1).
|
||||
///
|
||||
/// <para>
|
||||
/// GROUNDED IN A LIVE cdb CAPTURE (2026-06-18, [[reference-retail-ambient-values]]):
|
||||
/// retail's <c>LScape::sunlight</c> read at a dawn keyframe (H=90°, P=0.9°,
|
||||
/// DirBright≈0.224) = <c>(0.2238, ~0, 0.00352)</c> — y≈0, magnitude 0.224 =
|
||||
/// DirBright. That fed <c>level = 0.2·|sunlight| + ambient_level = 0.2·0.224 +
|
||||
/// 0.40 = 0.445</c>, matching the captured <c>SetWorldAmbientLight</c> level.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// PRIOR BUG: an earlier version returned <c>y = cos(P)</c> (≈1) — the raw
|
||||
/// PRE-transform value the decomp's <c>SkyDesc::GetLighting</c> writes to its
|
||||
/// <c>arg5</c> (0x00500ac9, before <c>LScape::set_sky_position</c>'s world
|
||||
/// transform). Porting that un-transformed vector inflated <c>|sunVec|</c> to
|
||||
/// ~1.06 instead of ~0.22, over-brightening BOTH the ambient boost
|
||||
/// (<see cref="SkyKeyframe.AmbientColor"/>) AND the sun colour
|
||||
/// (<see cref="SkyKeyframe.SunColor"/>) by ~30% vs retail. The world-space
|
||||
/// form above is what <c>LScape::sunlight</c> actually holds at runtime.
|
||||
/// </para>
|
||||
/// The shader uses the NORMALIZED vector for N·L; the magnitude (= DirBright)
|
||||
/// feeds the sun-colour intensity and the ambient brightness boost.
|
||||
/// </summary>
|
||||
public static Vector3 RetailSunVector(SkyKeyframe kf)
|
||||
{
|
||||
|
|
@ -325,9 +332,9 @@ public sealed class SkyStateProvider
|
|||
float sinP = MathF.Sin(p);
|
||||
float B = kf.DirBright;
|
||||
return new Vector3(
|
||||
MathF.Sin(h) * B * cosP, // x = sin(H) × B × cos(P)
|
||||
cosP, // y = cos(P) ← unscaled by B
|
||||
B * sinP); // z = B × sin(P)
|
||||
B * cosP * MathF.Sin(h), // x = DirBright × cos(P) × sin(H)
|
||||
B * cosP * MathF.Cos(h), // y = DirBright × cos(P) × cos(H)
|
||||
B * sinP); // z = DirBright × sin(P)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue