using Friflo.Engine.ECS; using Friflo.Engine.ECS.Systems; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MrGameEng.Graphics; namespace MrGameEng.Lighting; /// /// 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. /// 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 _occluders; private readonly float _cellSize; private readonly Vector2 _origin; private readonly ArchetypeQuery _query; private readonly List _lights = []; private int _frame; internal LightmapSystem( EntityStore store, Lightmap lightmap, DayNight dayNight, Func occluders, float cellSize, Vector2 origin ) { _lightmap = lightmap; _dayNight = dayNight; _occluders = occluders; _cellSize = cellSize; _origin = origin; _query = store.Query(); } /// 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(); } } /// /// 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). /// 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)), ]; } /// 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; } }