CI / build-test (push) Successful in 1m16s
Night was a touch too dark; raise the ambient moonlight floor a couple points. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
169 lines
6.0 KiB
C#
169 lines
6.0 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.24f; // ночь тусклая, но не чёрная (лунный свет) — чуть светлее, чем было
|
|
private const float MaxShadowCells = 7f; // макс. длина тени от солнца (на рассвете/закате)
|
|
private const float SunShadowStrength = 0.5f; // насколько темнеет клетка у основания тени
|
|
|
|
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,
|
|
_dayNight.SunShadow(MaxShadowCells),
|
|
SunShadowStrength
|
|
);
|
|
_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;
|
|
}
|
|
}
|