Files
mrgameeng/src/MrGameEng.Graphics/Lighting/LightmapBuilder.cs
T
Leonid Pershin 96e19c7c61 Lighting: directional sun shadows from the day/night sun angle
DayNight.SunShadow returns a cell offset opposite the sun's east-west position, longest near sunrise/sunset (low sun) and zero at noon/night, tilted slightly south so shadows fall in front of occluders. LightmapBuilder gains a directional shadow pass (MaxShadowCells length cap, SunShadowStrength darkening); LightmapSystem feeds it the current sun shadow each rebuild. Tests cover SunShadow's day-arc behaviour and the builder's directional darkening.
2026-06-14 07:04:11 +03:00

185 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Xna.Framework;
namespace MrGameEng.Lighting;
/// <summary>One point light projected onto the light grid: a cell position, a radius in cells and an intensity.</summary>
public readonly record struct LightSample(int X, int Y, float Radius, float Intensity);
/// <summary>
/// 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 <see cref="Lightmap"/> turns the grid into a
/// texture and the simulation samples it for local light.
/// </summary>
public static class LightmapBuilder
{
/// <summary>How much of the ambient light reaches a cell that is itself an occluder (canopy/shade).</summary>
public const float OccluderShade = 0.35f;
/// <summary>
/// Fills <paramref name="light"/> (length <paramref name="width"/>×<paramref name="height"/>) with
/// ambient light, shading occluder cells, casts each occluder's directional sun shadow along
/// <paramref name="sunShadow"/> (cells), then adds each point light with grid-traced occlusion.
/// Values end clamped to <c>[0, 1]</c>. A zero <paramref name="sunShadow"/> or
/// <paramref name="sunShadowStrength"/> skips the directional pass (e.g. at night/noon).
/// </summary>
public static void Build(
float[] light,
int width,
int height,
float ambient,
ReadOnlySpan<bool> occluders,
IReadOnlyList<LightSample> 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<bool> 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<bool> 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;
}
}
}
}