using Microsoft.Xna.Framework;
namespace MrGameEng.Lighting;
/// One point light projected onto the light grid: a cell position, a radius in cells and an intensity.
public readonly record struct LightSample(int X, int Y, float Radius, float Intensity);
///
/// Builds a per-cell light grid (CPU, GPU-free, testable): an ambient base dimmed under occluders,
/// plus point lights that attenuate with distance and are blocked by occluders between the source
/// and the cell (grid-traced shadows). Pure data — a turns the grid into a
/// texture and the simulation samples it for local light.
///
public static class LightmapBuilder
{
/// How much of the ambient light reaches a cell that is itself an occluder (canopy/shade).
public const float OccluderShade = 0.35f;
///
/// Fills (length ×) with
/// ambient light, shading occluder cells, casts each occluder's directional sun shadow along
/// (cells), then adds each point light with grid-traced occlusion.
/// Values end clamped to [0, 1]. A zero or
/// skips the directional pass (e.g. at night/noon).
///
public static void Build(
float[] light,
int width,
int height,
float ambient,
ReadOnlySpan occluders,
IReadOnlyList lights,
Vector2 sunShadow = default,
float sunShadowStrength = 0f
)
{
for (var i = 0; i < light.Length; i++)
{
light[i] = occluders[i] ? ambient * OccluderShade : ambient;
}
CastSunShadows(light, width, height, occluders, sunShadow, sunShadowStrength);
foreach (var l in lights)
{
if (l.Radius <= 0f || l.Intensity <= 0f)
{
continue;
}
var r = (int)MathF.Ceiling(l.Radius);
var minX = Math.Max(0, l.X - r);
var maxX = Math.Min(width - 1, l.X + r);
var minY = Math.Max(0, l.Y - r);
var maxY = Math.Min(height - 1, l.Y + r);
for (var y = minY; y <= maxY; y++)
{
for (var x = minX; x <= maxX; x++)
{
var dx = x - l.X;
var dy = y - l.Y;
var d = MathF.Sqrt(dx * dx + dy * dy);
if (d > l.Radius)
{
continue;
}
if (!Visible(l.X, l.Y, x, y, occluders, width))
{
continue; // в тени за препятствием
}
light[y * width + x] += l.Intensity * (1f - d / l.Radius);
}
}
}
for (var i = 0; i < light.Length; i++)
{
light[i] = Math.Clamp(light[i], 0f, 1f);
}
}
// Направленная тень от солнца: каждый окклюдер (гора/зрелая крона) отбрасывает тень вдоль
// вектора sunShadow (в клетках). Затемнение гуще у основания и тает к концу тени; сами клетки-
// окклюдеры не трогаем (они уже затенены). Окклюдеры разрежены, так что проход дёшев.
private static void CastSunShadows(
float[] light,
int width,
int height,
ReadOnlySpan occluders,
Vector2 sunShadow,
float strength
)
{
if (strength <= 0f)
{
return;
}
var steps = (int)MathF.Ceiling(sunShadow.Length());
if (steps <= 0)
{
return;
}
var stepX = sunShadow.X / steps;
var stepY = sunShadow.Y / steps;
for (var oy = 0; oy < height; oy++)
{
for (var ox = 0; ox < width; ox++)
{
if (!occluders[oy * width + ox])
{
continue;
}
for (var s = 1; s <= steps; s++)
{
var cx = ox + (int)MathF.Round(stepX * s);
var cy = oy + (int)MathF.Round(stepY * s);
if (cx < 0 || cx >= width || cy < 0 || cy >= height)
{
break;
}
var index = cy * width + cx;
if (occluders[index])
{
continue; // тень проходит над другими окклюдерами — они и так тёмные
}
var falloff = 1f - (float)(s - 1) / steps; // гуще у основания тени
light[index] *= 1f - strength * falloff;
}
}
}
}
// Есть ли прямая видимость между клетками: проводим линию (Брезенхем) и проверяем
// промежуточные клетки на окклюдер (концы исключены).
private static bool Visible(
int x0,
int y0,
int x1,
int y1,
ReadOnlySpan occluders,
int width
)
{
var dx = Math.Abs(x1 - x0);
var dy = Math.Abs(y1 - y0);
var sx = x0 < x1 ? 1 : -1;
var sy = y0 < y1 ? 1 : -1;
var err = dx - dy;
var x = x0;
var y = y0;
while (true)
{
if (x == x1 && y == y1)
{
return true;
}
if ((x != x0 || y != y0) && occluders[y * width + x])
{
return false;
}
var e2 = 2 * err;
if (e2 > -dy)
{
err -= dy;
x += sx;
}
if (e2 < dx)
{
err += dx;
y += sy;
}
}
}
}