CI / build-test (push) Successful in 1m14s
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>
165 lines
5.6 KiB
C#
165 lines
5.6 KiB
C#
using Friflo.Engine.ECS;
|
|
using Friflo.Engine.ECS.Systems;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using MrGameEng.Graphics;
|
|
|
|
namespace MrGameEng.Lighting;
|
|
|
|
/// <summary>
|
|
/// Rebuilds the lightmap a few times a second: ambient from the day/night cycle, occluder cells from
|
|
/// the game-supplied grid and point lights from the ECS, then uploads it to the texture. Throttled by
|
|
/// frame skipping — the light changes slowly, so 10 Hz looks smooth and keeps cost low.
|
|
/// </summary>
|
|
public sealed class LightmapSystem : BaseSystem
|
|
{
|
|
private const int RebuildEvery = 6; // ~10 Гц при 60 fps
|
|
private const float NightFloor = 0.18f; // ночь тусклая, но не чёрная (лунный свет)
|
|
|
|
private readonly Lightmap _lightmap;
|
|
private readonly DayNight _dayNight;
|
|
private readonly Func<bool[]> _occluders;
|
|
private readonly float _cellSize;
|
|
private readonly Vector2 _origin;
|
|
private readonly ArchetypeQuery<Transform2D, PointLight> _query;
|
|
private readonly List<LightSample> _lights = [];
|
|
private int _frame;
|
|
|
|
internal LightmapSystem(
|
|
EntityStore store,
|
|
Lightmap lightmap,
|
|
DayNight dayNight,
|
|
Func<bool[]> occluders,
|
|
float cellSize,
|
|
Vector2 origin
|
|
)
|
|
{
|
|
_lightmap = lightmap;
|
|
_dayNight = dayNight;
|
|
_occluders = occluders;
|
|
_cellSize = cellSize;
|
|
_origin = origin;
|
|
_query = store.Query<Transform2D, PointLight>();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnUpdateGroup()
|
|
{
|
|
if (++_frame < RebuildEvery)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_frame = 0;
|
|
|
|
_lights.Clear();
|
|
foreach (var (transforms, lights, _) in _query.Chunks)
|
|
{
|
|
var t = transforms.Span;
|
|
var l = lights.Span;
|
|
for (var i = 0; i < t.Length; i++)
|
|
{
|
|
if (l[i].Radius <= 0f || l[i].Intensity <= 0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var cx = (int)((t[i].Position.X - _origin.X) / _cellSize);
|
|
var cy = (int)((t[i].Position.Y - _origin.Y) / _cellSize);
|
|
_lights.Add(new LightSample(cx, cy, l[i].Radius / _cellSize, l[i].Intensity));
|
|
}
|
|
}
|
|
|
|
var ambient = NightFloor + (1f - NightFloor) * _dayNight.Intensity;
|
|
LightmapBuilder.Build(
|
|
_lightmap.Light,
|
|
_lightmap.Width,
|
|
_lightmap.Height,
|
|
ambient,
|
|
_occluders(),
|
|
_lights
|
|
);
|
|
_lightmap.Upload();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the lightmap over the world as a single texture quad with multiply blending and linear
|
|
/// filtering — the world darkens by the grid and shadows read soft. Registered after the sprite flush
|
|
/// (so it lands on the drawn scene) and before the screen-space UI (so the HUD stays at full brightness).
|
|
/// </summary>
|
|
public sealed class LightmapRenderSystem : BaseSystem
|
|
{
|
|
private static readonly BlendState Multiply = new()
|
|
{
|
|
ColorSourceBlend = Blend.DestinationColor,
|
|
ColorDestinationBlend = Blend.Zero,
|
|
AlphaSourceBlend = Blend.DestinationAlpha,
|
|
AlphaDestinationBlend = Blend.Zero,
|
|
};
|
|
|
|
private readonly GraphicsDevice _device;
|
|
private readonly Renderer2D _renderer;
|
|
private readonly Lightmap _lightmap;
|
|
private readonly BasicEffect _effect;
|
|
private readonly VertexPositionTexture[] _quad;
|
|
|
|
internal LightmapRenderSystem(GraphicsDevice device, Renderer2D renderer, Lightmap lightmap)
|
|
{
|
|
_device = device;
|
|
_renderer = renderer;
|
|
_lightmap = lightmap;
|
|
_effect = new BasicEffect(device)
|
|
{
|
|
TextureEnabled = true,
|
|
VertexColorEnabled = false,
|
|
World = Matrix.Identity,
|
|
};
|
|
|
|
var x0 = lightmap.Origin.X;
|
|
var y0 = lightmap.Origin.Y;
|
|
var x1 = x0 + lightmap.Width * lightmap.CellSize;
|
|
var y1 = y0 + lightmap.Height * lightmap.CellSize;
|
|
_quad =
|
|
[
|
|
new VertexPositionTexture(new Vector3(x0, y0, 0f), new Vector2(0f, 0f)),
|
|
new VertexPositionTexture(new Vector3(x1, y0, 0f), new Vector2(1f, 0f)),
|
|
new VertexPositionTexture(new Vector3(x0, y1, 0f), new Vector2(0f, 1f)),
|
|
new VertexPositionTexture(new Vector3(x1, y0, 0f), new Vector2(1f, 0f)),
|
|
new VertexPositionTexture(new Vector3(x1, y1, 0f), new Vector2(1f, 1f)),
|
|
new VertexPositionTexture(new Vector3(x0, y1, 0f), new Vector2(0f, 1f)),
|
|
];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnUpdateGroup()
|
|
{
|
|
var camera = _renderer.Camera;
|
|
_effect.View = camera.View;
|
|
_effect.Projection = camera.Projection;
|
|
_effect.Texture = _lightmap.Texture;
|
|
|
|
var previousViewport = _device.Viewport;
|
|
var mapping = camera.Mapping;
|
|
_device.Viewport = new Viewport(
|
|
(int)MathF.Round(mapping.Offset.X),
|
|
(int)MathF.Round(mapping.Offset.Y),
|
|
(int)MathF.Round(camera.VirtualWidth * mapping.Scale),
|
|
(int)MathF.Round(camera.VirtualHeight * mapping.Scale)
|
|
);
|
|
_device.BlendState = Multiply;
|
|
_device.SamplerStates[0] = SamplerState.LinearClamp;
|
|
_device.DepthStencilState = DepthStencilState.None;
|
|
_device.RasterizerState = RasterizerState.CullNone;
|
|
|
|
foreach (var pass in _effect.CurrentTechnique.Passes)
|
|
{
|
|
pass.Apply();
|
|
_device.DrawUserPrimitives(PrimitiveType.TriangleList, _quad, 0, 2);
|
|
}
|
|
|
|
_device.Viewport = previousViewport;
|
|
_device.BlendState = BlendState.AlphaBlend;
|
|
}
|
|
}
|