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,164 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user