Extend the Lighting module with a per-cell lightmap. LightmapBuilder (pure, tested) fills an ambient base, shades occluder cells, and adds point lights that attenuate with distance and are blocked by occluders between source and cell (grid-traced soft shadows). Lightmap holds the grid plus a greyscale texture (Upload) and a bilinear SampleAt for the simulation; a PointLight component places lights in the world. LightmapSystem rebuilds the grid a few times a second (ambient from the day/night cycle with a night floor, occluders from a game-supplied grid, point lights from the ECS); LightmapRenderSystem multiplies the lightmap over the world after the sprite flush and under the HUD. Wired via scene.UseLighting(...), sampled through Lighting.SampleAt. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d0104df304
commit
79d406a9f4
@@ -0,0 +1,120 @@
|
||||
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, then adds each point light with grid-traced occlusion.
|
||||
/// Values end clamped to <c>[0, 1]</c>.
|
||||
/// </summary>
|
||||
public static void Build(
|
||||
float[] light,
|
||||
int width,
|
||||
int height,
|
||||
float ambient,
|
||||
ReadOnlySpan<bool> occluders,
|
||||
IReadOnlyList<LightSample> lights
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < light.Length; i++)
|
||||
{
|
||||
light[i] = occluders[i] ? ambient * OccluderShade : ambient;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Есть ли прямая видимость между клетками: проводим линию (Брезенхем) и проверяем
|
||||
// промежуточные клетки на окклюдер (концы исключены).
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user