The portal swirl's magenta light (and the viewer fill) read as a tight, concentrated pool vs retail's soft, room-wide tint. Cause: acdream applied its STATIC dat-bake falloff (1/d^3 distance-cube + range x1.3) to ALL point lights, including dynamic ones. Retail draws dynamic lights through the D3D hardware path (config_hardware_light 0x0059ad30): a point light gets Attenuation1=1 => att = 1/d (inverse-linear), plain Lambert, range x1.5 (rangeAdjust 0x00820cc4). Split the two paths by a per-light IsDynamic flag: - LightSource.IsDynamic; packed into GlobalLight.coneAngleEtc.y (binding=4). - LightInfoLoader.Load(isDynamic) => range x1.5 + flag (server-object/portal lights via the live spawn path); dat-static lights keep x1.3 (default). - Viewer fill + weenie/portal lights = dynamic; dat torches = static. - mesh_modern.vert pointContribution: dynamic branch = 1/d att, plain Lambert, hard cutoff, no per-light cap (D3D accumulates then saturates via the existing min(pointAcc,1)); static branch = the unchanged wrap/norm bake. This is the portal half of #143 (the magenta light itself now registers + reaches the walls via the prior weenie-light + landblock-key fix). Refines AP-35: point lights now split static-bake (1/d^3) vs dynamic-hardware (1/d) by path. Verified: portal light now range=9 (6x1.5), magenta spreads softly; shader compiles clean; static torches unchanged (range 5.2/6.5/7.8). User-confirmed the portal matches retail and the torch-lit interior did not over-brighten. Core lighting 44/44, App 476 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2.4 KiB
C#
56 lines
2.4 KiB
C#
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; y = #143 dynamic flag; zw reserved)
|
||
buffer[o + 12] = L.ConeAngle;
|
||
buffer[o + 13] = L.IsDynamic ? 1f : 0f; // shader: 1/d D3D attenuation vs static 1/d³
|
||
}
|
||
return n;
|
||
}
|
||
}
|