Phase D: spatial lighting with shadows; local light drives growth

Bump the engine submodule to 79d406a (2D lightmap). The world scene now uses
UseLighting instead of the per-sprite ambient: a lightmap multiplies day/night
× occlusion over the world, so forests cast soft shade and the world darkens
spatially toward night. Occluders are mountains (new TerrainDef.BlocksLight)
plus mature trees, rebuilt from the live population. PlantGrowthSystem samples
the local light per plant (Lighting.SampleAt), so undergrowth under canopy
grows slower. A dev-console 'light' command places a point light at the cursor
to show cast shadows at night. Completes the plant ecosystem (A–D).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 18:40:31 +03:00
co-authored by Claude Opus 4.8
parent a04e867e87
commit 49936a1de3
5 changed files with 87 additions and 9 deletions
+1 -1
View File
@@ -17,6 +17,6 @@
{ "chance": 0.35, "options": ["TreeOakA", "TreeOakB", "TreeBirchA", "TreeGrayPineA"] }
] },
{ "defName": "Mountain", "label": "terrain.mountain", "maxHeight": 1.01, "color": [136, 132, 128],
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2 }
"surface": "terrain/surfaces/roughhewnrock", "fertility": 0.2, "blocksLight": true }
]
}
+1 -1
Submodule engine updated: d0104df304...79d406a9f4
+3
View File
@@ -27,6 +27,9 @@ public sealed class TerrainDef : Def
/// <summary>Суша: здесь появляются жители, животные и растения.</summary>
public bool IsLand { get; init; }
/// <summary>Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней.</summary>
public bool BlocksLight { get; init; }
/// <summary>Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл.</summary>
public string? Surface { get; init; }
+79 -4
View File
@@ -53,6 +53,8 @@ public sealed class WorldScene : Scene
private PlantSet _plants = null!;
private float[] _cellFertility = [];
private bool[] _cellLand = [];
private bool[] _cellOccluderBase = []; // горы (статично из террейна)
private bool[] _cellOccluder = []; // горы + зрелые деревья (пересобирается лайтмапом)
/// <summary>Новый мир из конфига.</summary>
public WorldScene(WorldConfig config)
@@ -84,7 +86,7 @@ public sealed class WorldScene : Scene
var calendar = Context.UseCalendar(SecondsPerDay);
var climate = Context.UseClimate(ClimateSettings.Default);
var dayNight = this.UseDayNight(renderer); // мир темнеет ночью — амбиент идёт в рендер
var dayNight = new DayNight(calendar, DayNightSettings.Default);
// Рельеф детерминирован сидом. Растения: новый мир — скаттер из сида; загрузка — из сейва.
_plants = new PlantSet(content, atlases, device);
@@ -101,6 +103,18 @@ public sealed class WorldScene : Scene
RestorePlants(content);
}
// Освещение: лайтмап (день/ночь × окклюзия от гор/крон + точечные) множится поверх мира,
// а Lighting.SampleAt даёт локальный свет системе роста (подлесок под кронами растёт хуже).
var lighting = this.UseLighting(
renderer,
dayNight,
_config.Width,
_config.Height,
CellSize,
Vector2.Zero,
BuildOccluders
);
var camera = Store.CreateEntity(new Camera(_bounds.Center, zoom: 1f, bounds: _bounds));
// HUD, полоса скорости и меню-пауза в одной корневой панели.
@@ -120,8 +134,31 @@ public sealed class WorldScene : Scene
this.UseInspector(renderer);
var console = this.UseDevConsole();
RegisterCommands(console, content, atlases);
console.Register(
"light",
"light [radius] — place a point light at the cursor (night shadows demo)",
(c, args) =>
{
var radius =
args.Length > 0
? float.Parse(args[0], System.Globalization.CultureInfo.InvariantCulture)
: 120f;
var mouse = Mouse.GetState();
var world = renderer.ScreenToWorld(new Vector2(mouse.X, mouse.Y));
Store.CreateEntity(
Transform2D.At(world),
new PointLight
{
Radius = radius,
Color = Color.White,
Intensity = 0.9f,
}
);
c.WriteLine($"light at {world.X:0},{world.Y:0} r{radius:0}");
}
);
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, dayNight, CellSize));
UpdateSystems.Add(new PlantGrowthSystem(_plants, calendar, climate, lighting, CellSize));
UpdateSystems.Add(
new PlantLifecycleSystem(
Store,
@@ -193,8 +230,11 @@ public sealed class WorldScene : Scene
_config.SmoothPasses
);
var grid = new TileGrid(_config.Width, _config.Height);
_cellFertility = new float[_config.Width * _config.Height];
_cellLand = new bool[_config.Width * _config.Height];
var cells = _config.Width * _config.Height;
_cellFertility = new float[cells];
_cellLand = new bool[cells];
_cellOccluderBase = new bool[cells];
_cellOccluder = new bool[cells];
for (var x = 0; x < _config.Width; x++)
{
for (var y = 0; y < _config.Height; y++)
@@ -204,6 +244,7 @@ public sealed class WorldScene : Scene
var cell = y * _config.Width + x;
_cellFertility[cell] = terrain.Fertility;
_cellLand[cell] = terrain.IsLand;
_cellOccluderBase[cell] = terrain.BlocksLight;
if (scatterRandom is not null)
{
ScatterSpawner.Spawn(
@@ -355,6 +396,40 @@ public sealed class WorldScene : Scene
}
}
// Текущая сетка окклюдеров для лайтмапа: статичные горы + клетки со зрелыми деревьями.
private bool[] BuildOccluders()
{
Array.Copy(_cellOccluderBase, _cellOccluder, _cellOccluder.Length);
Store
.Query<PlantGrowth, Transform2D>()
.ForEachEntity(
(ref PlantGrowth grow, ref Transform2D transform, Entity _) =>
{
var species = _plants[grow.Species];
if (
species.Def.TrunkRadiusCells <= 0f
|| grow.Stage < species.Stages.Length - 1
)
{
return; // затеняют только зрелые деревья (со стволом)
}
var cx = Math.Clamp(
(int)(transform.Position.X / CellSize),
0,
_config.Width - 1
);
var cy = Math.Clamp(
(int)(transform.Position.Y / CellSize),
0,
_config.Height - 1
);
_cellOccluder[cy * _config.Width + cx] = true;
}
);
return _cellOccluder;
}
private void Switch(Scene scene)
{
if (!Context.Scenes.IsTransitioning)
+3 -3
View File
@@ -19,7 +19,7 @@ public sealed class PlantGrowthSystem(
PlantSet plants,
Calendar calendar,
Climate climate,
DayNight dayNight,
Lighting lighting,
int cellSize
) : QuerySystem<PlantGrowth, PlantGenome, Sprite, Transform2D>
{
@@ -31,8 +31,7 @@ public sealed class PlantGrowthSystem(
return; // пауза — рост стоит
}
// В фазе B свет и температура глобальные — считаем раз за кадр (фаза D даст локальный свет).
var light = dayNight.Intensity;
// Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
var temperature = climate.Temperature;
foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks)
@@ -52,6 +51,7 @@ public sealed class PlantGrowthSystem(
}
ref var gene = ref dna[i];
var light = lighting.SampleAt(t[i].Position);
var rate =
gene.Vigor
* Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance)