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; } } } }