From 49936a1de35df4c8b60714f687ae68766671bfdf Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 12 Jun 2026 18:40:31 +0300 Subject: [PATCH] Phase D: spatial lighting with shadows; local light drives growth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Mods/Core/Defs/terrain.json | 2 +- engine | 2 +- src/LittleSim/Content/GameDefs.cs | 3 + src/LittleSim/Scenes/WorldScene.cs | 83 ++++++++++++++++++++++++-- src/LittleSim/Sim/PlantGrowthSystem.cs | 6 +- 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/Mods/Core/Defs/terrain.json b/Mods/Core/Defs/terrain.json index f2caabf..a53f681 100644 --- a/Mods/Core/Defs/terrain.json +++ b/Mods/Core/Defs/terrain.json @@ -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 } ] } diff --git a/engine b/engine index d0104df..79d406a 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit d0104df3044961f74bda68d3e6cc97dd0d34b326 +Subproject commit 79d406a9f4687fdb47a059a6b1e0593eb33e3061 diff --git a/src/LittleSim/Content/GameDefs.cs b/src/LittleSim/Content/GameDefs.cs index 41c8de0..443f94f 100644 --- a/src/LittleSim/Content/GameDefs.cs +++ b/src/LittleSim/Content/GameDefs.cs @@ -27,6 +27,9 @@ public sealed class TerrainDef : Def /// Суша: здесь появляются жители, животные и растения. public bool IsLand { get; init; } + /// Загораживает свет (горы) — базовый окклюдер для лайтмапа/теней. + public bool BlocksLight { get; init; } + /// Ключ текстуры поверхности для тайловой сцены; null — тонированный тайл. public string? Surface { get; init; } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 9727d26..31dcfaf 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -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 = []; // горы + зрелые деревья (пересобирается лайтмапом) /// Новый мир из конфига. 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() + .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) diff --git a/src/LittleSim/Sim/PlantGrowthSystem.cs b/src/LittleSim/Sim/PlantGrowthSystem.cs index 54e8fb9..c2f4260 100644 --- a/src/LittleSim/Sim/PlantGrowthSystem.cs +++ b/src/LittleSim/Sim/PlantGrowthSystem.cs @@ -19,7 +19,7 @@ public sealed class PlantGrowthSystem( PlantSet plants, Calendar calendar, Climate climate, - DayNight dayNight, + Lighting lighting, int cellSize ) : QuerySystem { @@ -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)