diff --git a/src/MrGameEng.Graphics/Lighting/DayNight.cs b/src/MrGameEng.Graphics/Lighting/DayNight.cs
index 8f09853..5792d66 100644
--- a/src/MrGameEng.Graphics/Lighting/DayNight.cs
+++ b/src/MrGameEng.Graphics/Lighting/DayNight.cs
@@ -52,6 +52,26 @@ public sealed class DayNight
/// Light intensity at a world point in [0, 1]. Global today; local (with shadows) later.
public float SampleAt(Vector2 world) => Daylight;
+ ///
+ /// Offset, in grid cells, of the shadow an occluder casts under the current sun: opposite the
+ /// sun's east–west position and longest near sunrise/sunset (low sun), shrinking to zero at noon
+ /// and at night. Feeds the lightmap's directional shadow pass; caps
+ /// the dawn/dusk shadow length. Tilted slightly "south" (down) so shadows fall in front of objects.
+ ///
+ public Vector2 SunShadow(float maxLength)
+ {
+ var day = (_calendar.DayProgress - 0.25f) / 0.5f; // daytime fraction over [06:00, 18:00]
+ if (day <= 0f || day >= 1f)
+ {
+ return Vector2.Zero; // night — no sun; the ambient floor handles darkness
+ }
+
+ var altitude = MathF.Sin(day * MathF.PI); // 0 at dawn/dusk, 1 at noon
+ var direction = new Vector2(2f * day - 1f, 0.4f); // sun east→west ⇒ shadow west→east, tilted south
+ direction.Normalize();
+ return direction * (maxLength * (1f - altitude));
+ }
+
/// Ambient tint for the scene: night color at night, day color at noon, eased between.
public Color Ambient
{
diff --git a/src/MrGameEng.Graphics/Lighting/LightmapBuilder.cs b/src/MrGameEng.Graphics/Lighting/LightmapBuilder.cs
index b0dbdbf..4816e23 100644
--- a/src/MrGameEng.Graphics/Lighting/LightmapBuilder.cs
+++ b/src/MrGameEng.Graphics/Lighting/LightmapBuilder.cs
@@ -1,3 +1,5 @@
+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.
@@ -16,8 +18,10 @@ public static class LightmapBuilder
///
/// Fills (length ×) with
- /// ambient light, shading occluder cells, then adds each point light with grid-traced occlusion.
- /// Values end clamped to [0, 1].
+ /// 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,
@@ -25,7 +29,9 @@ public static class LightmapBuilder
int height,
float ambient,
ReadOnlySpan occluders,
- IReadOnlyList lights
+ IReadOnlyList lights,
+ Vector2 sunShadow = default,
+ float sunShadowStrength = 0f
)
{
for (var i = 0; i < light.Length; i++)
@@ -33,6 +39,8 @@ public static class LightmapBuilder
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)
@@ -73,6 +81,62 @@ public static class LightmapBuilder
}
}
+ // Направленная тень от солнца: каждый окклюдер (гора/зрелая крона) отбрасывает тень вдоль
+ // вектора 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(
diff --git a/src/MrGameEng.Graphics/Lighting/LightmapSystems.cs b/src/MrGameEng.Graphics/Lighting/LightmapSystems.cs
index 1fd2679..7fc0855 100644
--- a/src/MrGameEng.Graphics/Lighting/LightmapSystems.cs
+++ b/src/MrGameEng.Graphics/Lighting/LightmapSystems.cs
@@ -15,6 +15,8 @@ public sealed class LightmapSystem : BaseSystem
{
private const int RebuildEvery = 6; // ~10 Гц при 60 fps
private const float NightFloor = 0.18f; // ночь тусклая, но не чёрная (лунный свет)
+ private const float MaxShadowCells = 7f; // макс. длина тени от солнца (на рассвете/закате)
+ private const float SunShadowStrength = 0.5f; // насколько темнеет клетка у основания тени
private readonly Lightmap _lightmap;
private readonly DayNight _dayNight;
@@ -77,7 +79,9 @@ public sealed class LightmapSystem : BaseSystem
_lightmap.Height,
ambient,
_occluders(),
- _lights
+ _lights,
+ _dayNight.SunShadow(MaxShadowCells),
+ SunShadowStrength
);
_lightmap.Upload();
}
diff --git a/tests/MrGameEng.Graphics.Tests/DayNightTests.cs b/tests/MrGameEng.Graphics.Tests/DayNightTests.cs
index e21b126..0e50340 100644
--- a/tests/MrGameEng.Graphics.Tests/DayNightTests.cs
+++ b/tests/MrGameEng.Graphics.Tests/DayNightTests.cs
@@ -1,3 +1,4 @@
+using Microsoft.Xna.Framework;
using MrGameEng.Core;
using MrGameEng.Lighting;
using Xunit;
@@ -39,6 +40,25 @@ public class DayNightTests
Assert.True(dawn < mid && mid < noon, "daylight should rise from dawn to noon");
}
+ [Fact]
+ public void SunShadow_ZeroAtNight_PointsWestInMorning_EastInAfternoon_ShortAtNoon()
+ {
+ var (clock, dayNight) = Make(); // 1s = 1 in-game hour
+
+ Assert.Equal(Vector2.Zero, dayNight.SunShadow(8f)); // 00:00 — night, no sun
+
+ clock.Advance(7f); // 07:00 — low morning sun in the east
+ var morning = dayNight.SunShadow(8f);
+ Assert.True(morning.X < 0f, "morning shadow points west");
+ Assert.True(morning.Length() > 2f, "low sun casts a long shadow");
+
+ clock.Advance(5f); // 12:00 — sun overhead
+ Assert.True(dayNight.SunShadow(8f).Length() < 1f, "noon sun casts almost no shadow");
+
+ clock.Advance(5f); // 17:00 — afternoon sun in the west
+ Assert.True(dayNight.SunShadow(8f).X > 0f, "afternoon shadow points east");
+ }
+
[Fact]
public void Ambient_DarkerAtNightThanAtNoon()
{
diff --git a/tests/MrGameEng.Graphics.Tests/LightmapBuilderTests.cs b/tests/MrGameEng.Graphics.Tests/LightmapBuilderTests.cs
index 02172fa..d9b475d 100644
--- a/tests/MrGameEng.Graphics.Tests/LightmapBuilderTests.cs
+++ b/tests/MrGameEng.Graphics.Tests/LightmapBuilderTests.cs
@@ -1,3 +1,4 @@
+using Microsoft.Xna.Framework;
using MrGameEng.Lighting;
using Xunit;
@@ -54,6 +55,29 @@ public class LightmapBuilderTests
Assert.Equal(0f, light[5], 5); // за препятствием — тень
}
+ [Fact]
+ public void SunShadow_DarkensCellsInTheShadowDirection_FadingFromTheCaster()
+ {
+ var occ = new bool[7];
+ occ[3] = true; // occluder in the middle
+ var light = new float[7];
+ LightmapBuilder.Build(
+ light,
+ 7,
+ 1,
+ 1f,
+ occ,
+ [],
+ sunShadow: new Vector2(3f, 0f),
+ sunShadowStrength: 0.6f
+ );
+
+ Assert.Equal(1f, light[2], 5); // toward the sun (opposite the shadow) — unshadowed
+ Assert.Equal(LightmapBuilder.OccluderShade, light[3], 5); // occluder cell stays self-shaded
+ Assert.True(light[4] < 1f); // in shadow
+ Assert.True(light[4] < light[6]); // darker near the caster, fading along the shadow
+ }
+
[Fact]
public void Values_StayWithinUnitRange()
{