Compare commits
2
Commits
38bdfbbb81
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f3931f85 | ||
|
|
96e19c7c61 |
@@ -52,6 +52,26 @@ public sealed class DayNight
|
|||||||
/// <summary>Light intensity at a world point in <c>[0, 1]</c>. Global today; local (with shadows) later.</summary>
|
/// <summary>Light intensity at a world point in <c>[0, 1]</c>. Global today; local (with shadows) later.</summary>
|
||||||
public float SampleAt(Vector2 world) => Daylight;
|
public float SampleAt(Vector2 world) => Daylight;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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; <paramref name="maxLength"/> caps
|
||||||
|
/// the dawn/dusk shadow length. Tilted slightly "south" (down) so shadows fall in front of objects.
|
||||||
|
/// </summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Ambient tint for the scene: night color at night, day color at noon, eased between.</summary>
|
/// <summary>Ambient tint for the scene: night color at night, day color at noon, eased between.</summary>
|
||||||
public Color Ambient
|
public Color Ambient
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
|
||||||
namespace MrGameEng.Lighting;
|
namespace MrGameEng.Lighting;
|
||||||
|
|
||||||
/// <summary>One point light projected onto the light grid: a cell position, a radius in cells and an intensity.</summary>
|
/// <summary>One point light projected onto the light grid: a cell position, a radius in cells and an intensity.</summary>
|
||||||
@@ -16,8 +18,10 @@ public static class LightmapBuilder
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fills <paramref name="light"/> (length <paramref name="width"/>×<paramref name="height"/>) with
|
/// Fills <paramref name="light"/> (length <paramref name="width"/>×<paramref name="height"/>) with
|
||||||
/// ambient light, shading occluder cells, then adds each point light with grid-traced occlusion.
|
/// ambient light, shading occluder cells, casts each occluder's directional sun shadow along
|
||||||
/// Values end clamped to <c>[0, 1]</c>.
|
/// <paramref name="sunShadow"/> (cells), then adds each point light with grid-traced occlusion.
|
||||||
|
/// Values end clamped to <c>[0, 1]</c>. A zero <paramref name="sunShadow"/> or
|
||||||
|
/// <paramref name="sunShadowStrength"/> skips the directional pass (e.g. at night/noon).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Build(
|
public static void Build(
|
||||||
float[] light,
|
float[] light,
|
||||||
@@ -25,7 +29,9 @@ public static class LightmapBuilder
|
|||||||
int height,
|
int height,
|
||||||
float ambient,
|
float ambient,
|
||||||
ReadOnlySpan<bool> occluders,
|
ReadOnlySpan<bool> occluders,
|
||||||
IReadOnlyList<LightSample> lights
|
IReadOnlyList<LightSample> lights,
|
||||||
|
Vector2 sunShadow = default,
|
||||||
|
float sunShadowStrength = 0f
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < light.Length; i++)
|
for (var i = 0; i < light.Length; i++)
|
||||||
@@ -33,6 +39,8 @@ public static class LightmapBuilder
|
|||||||
light[i] = occluders[i] ? ambient * OccluderShade : ambient;
|
light[i] = occluders[i] ? ambient * OccluderShade : ambient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CastSunShadows(light, width, height, occluders, sunShadow, sunShadowStrength);
|
||||||
|
|
||||||
foreach (var l in lights)
|
foreach (var l in lights)
|
||||||
{
|
{
|
||||||
if (l.Radius <= 0f || l.Intensity <= 0f)
|
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<bool> 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(
|
private static bool Visible(
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ namespace MrGameEng.Lighting;
|
|||||||
public sealed class LightmapSystem : BaseSystem
|
public sealed class LightmapSystem : BaseSystem
|
||||||
{
|
{
|
||||||
private const int RebuildEvery = 6; // ~10 Гц при 60 fps
|
private const int RebuildEvery = 6; // ~10 Гц при 60 fps
|
||||||
private const float NightFloor = 0.18f; // ночь тусклая, но не чёрная (лунный свет)
|
private const float NightFloor = 0.24f; // ночь тусклая, но не чёрная (лунный свет) — чуть светлее, чем было
|
||||||
|
private const float MaxShadowCells = 7f; // макс. длина тени от солнца (на рассвете/закате)
|
||||||
|
private const float SunShadowStrength = 0.5f; // насколько темнеет клетка у основания тени
|
||||||
|
|
||||||
private readonly Lightmap _lightmap;
|
private readonly Lightmap _lightmap;
|
||||||
private readonly DayNight _dayNight;
|
private readonly DayNight _dayNight;
|
||||||
@@ -77,7 +79,9 @@ public sealed class LightmapSystem : BaseSystem
|
|||||||
_lightmap.Height,
|
_lightmap.Height,
|
||||||
ambient,
|
ambient,
|
||||||
_occluders(),
|
_occluders(),
|
||||||
_lights
|
_lights,
|
||||||
|
_dayNight.SunShadow(MaxShadowCells),
|
||||||
|
SunShadowStrength
|
||||||
);
|
);
|
||||||
_lightmap.Upload();
|
_lightmap.Upload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Core;
|
using MrGameEng.Core;
|
||||||
using MrGameEng.Lighting;
|
using MrGameEng.Lighting;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -39,6 +40,25 @@ public class DayNightTests
|
|||||||
Assert.True(dawn < mid && mid < noon, "daylight should rise from dawn to noon");
|
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]
|
[Fact]
|
||||||
public void Ambient_DarkerAtNightThanAtNoon()
|
public void Ambient_DarkerAtNightThanAtNoon()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
using MrGameEng.Lighting;
|
using MrGameEng.Lighting;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -54,6 +55,29 @@ public class LightmapBuilderTests
|
|||||||
Assert.Equal(0f, light[5], 5); // за препятствием — тень
|
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]
|
[Fact]
|
||||||
public void Values_StayWithinUnitRange()
|
public void Values_StayWithinUnitRange()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user