diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 12:10:42 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 12:10:42 +0800 |
| commit | a70a057fce7afa5787b2d4a037d7016f1120ef6a (patch) | |
| tree | ce5a24cf07dbbfd46cee48cbec561cc23349f48e /src/LivingVillage.Desktop/VillageArt.fs | |
| parent | 3cd5a635d12474647c4cf288a5cc4acb4e0eb1b5 (diff) | |
| download | living-village-a70a057fce7afa5787b2d4a037d7016f1120ef6a.tar.gz | |
feat(desktop): P29 512 聚落组团 + 夜景灯光径向光晕
- MapGen:仅大图新增 ClusterSeed(group seeding)与组团布屋——512 沿横向石板路
(主街+河岸路)成排成组:组内 2-4 座/1 格间隙、组间 4-6 格空档,宽度 2/3/4 分层;
垂柳改为确定性成丛(2-3 株/丛、丛间 3-5 格空档)。独立 clusterRng 不触碰
structureRng,默认图仍不消费 RNG,64x48=6042052 / 256x192=1284656962 锁定不变。
- VillageArt:新增 lanternFalloff(二次 1-(d/r)^2,半径 0 处最大、边缘 0、严格单调无平台);
lanternGlowLayers 按该衰减逐环累积 alpha;drawNightGlows 对桥头灯笼+民居门窗画半径
4 tile 的柔和径向光晕,整图取证与实机 DrawWorldView 共用同一套纯函数。
- 回归 +6:组团行/成丛/衰减/光源确定性;Desktop 151、Kernel 92 全绿,Release 0/0,
final_digest=953775FAEB2F… 3 次一致。证据帧见 art/p29-*。
Diffstat (limited to 'src/LivingVillage.Desktop/VillageArt.fs')
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 85 |
1 files changed, 75 insertions, 10 deletions
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 9a56de4..3e52546 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -444,10 +444,25 @@ module VillageArt = if half > 0 then yield (centerX - half, centerY + dy, half * 2 + 1) ] - /// Soft radial lantern glow drawn as concentric filled circles. Every ring carries a - /// low, premultiplied warm alpha so the rings sum to `peakAlpha` at the core and thin - /// out toward the rim; because each ring's rgb is premultiplied, the core can never - /// saturate to an opaque white block the way straight rgba layers did. + /// P29 夜景灯光径向衰减:半径 0 处为 1(最亮),距离达到 radius 处为 0;中间按二次 + /// `1 - (d/r)^2` 平滑递减,严格单调、无平台(既没有硬边方斑,也不会出现等亮台阶)。 + /// 纯函数、无时钟/随机源,供灯笼/窗光光晕与单测共用。 + let lanternFalloff (distance: float32) (radius: float32) : float32 = + if radius <= 0.0f then + 0.0f + elif distance <= 0.0f then + 1.0f + elif distance >= radius then + 0.0f + else + let t = distance / radius + 1.0f - t * t + + /// Soft radial lantern glow drawn as concentric filled circles. Each ring's alpha is the + /// incremental step of `lanternFalloff` at that ring radius, so the accumulated warm alpha + /// follows the smooth quadratic falloff from the core out to `radius` instead of stepping or + /// flattening. Because every ring's rgb is premultiplied by its own low alpha, the core can + /// never saturate to an opaque white block the way straight rgba layers did. let lanternGlowLayers (centerX: int) (centerY: int) @@ -460,12 +475,20 @@ module VillageArt = if radius <= 0 || rings <= 0 || peakAlpha <= 0 then [] else - let perRing = max 1 (peakAlpha / rings) - let glowColor = premultiply red green blue perRing + let radiusF = float32 radius + let targetAlpha (ringRadius: int) = + let falloff = lanternFalloff (float32 ringRadius) radiusF + int (Math.Round(float peakAlpha * float falloff, MidpointRounding.AwayFromZero)) + let mutable previousTarget = 0 [ for ring in 0 .. rings - 1 do let ringRadius = max 1 (radius * (rings - ring) / rings) - for (x, y, width) in ellipseRuns centerX centerY ringRadius ringRadius do - yield (Microsoft.Xna.Framework.Rectangle(x, y, width, 1), glowColor) ] + let target = targetAlpha ringRadius + let increment = max 0 (target - previousTarget) + previousTarget <- max previousTarget target + if increment > 0 then + let glowColor = premultiply red green blue increment + for (x, y, width) in ellipseRuns centerX centerY ringRadius ringRadius do + yield (Microsoft.Xna.Framework.Rectangle(x, y, width, 1), glowColor) ] /// Characters spawn at tile centres; push the sprite down half a tile so its feet rest /// on the tile's ground line instead of floating at the centre. @@ -685,6 +708,41 @@ module VillageArt = [ (x, List.min ys); (x, List.max ys) ]) |> List.distinct + /// P29 夜景灯光源瓦片:桥头灯笼 + 民居门窗暖光(确定性纯函数,便于单测)。 + /// 每户至少门格发光;变体 A 再取第一扇横向窗,变体 B(整面格栅窗)与门同格故不另加。 + let nightLightTiles (map: MapGen.Result) : (int * int) list = + let lanterns = bridgeLanternTiles map + let houseLights = + map.Buildings + |> List.collect (fun building -> + let windowTiles = + if houseFacadeVariant building = 0 then + [ for x in building.Left .. building.Left + building.Width - 1 do + if x <> building.DoorX && (x - building.Left) % 2 = 0 then yield (x, building.DoorY) ] + else + [ (building.DoorX, building.DoorY) ] + (building.DoorX, building.DoorY) :: windowTiles) + lanterns @ houseLights |> List.distinct + + /// P29 夜景光晕叠加层:对每个灯光源画一圈柔和径向衰减(半径 3-6 tile), + /// 由 `lanternGlowLayers` 保证无硬方斑、无等亮台阶。纯绘制,不改瓦片/可达性。 + let drawNightGlows + (spriteBatch: SpriteBatch) + (pixel: Texture2D) + (map: MapGen.Result) + (radiusPixels: int) + (peakAlpha: int) + (tileX: int -> int) + (tileY: int -> int) + (cell: int) : unit = + if peakAlpha > 0 && radiusPixels > 0 then + let half = cell / 2 + for (lightX, lightY) in nightLightTiles map do + let centerX = tileX lightX + half + let centerY = tileY lightY + half + for (rect, color) in lanternGlowLayers centerX centerY radiusPixels 8 255 214 150 peakAlpha do + spriteBatch.Draw(pixel, rect, color) + let drawWorld (spriteBatch: SpriteBatch) (textures: ArtTextures) @@ -835,7 +893,8 @@ module VillageArt = (viewportWidth: int) (viewportHeight: int) (map: MapGen.Result) - (tint: XnaColor) : int = + (tint: XnaColor) + (glowScale: float32) : int = let scale = min (float32 viewportWidth / float32 (max 1 (map.Width * Sim.tilePixels))) (float32 viewportHeight / float32 (max 1 (map.Height * Sim.tilePixels))) @@ -854,6 +913,9 @@ module VillageArt = let code = map.Tiles.[y * map.Width + x] spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, cell, cell), tileSourceRectangle (spriteOf code), tint) drawMapStructures spriteBatch textures pixel tileX tileY cell map + if glowScale > 0.0f then + // 半径 4 tile 的柔和径向衰减;随 cell 缩放,整图/放大帧观感一致。 + drawNightGlows spriteBatch pixel map (4 * cell) (int (150.0f * glowScale)) tileX tileY cell map.Width * map.Height /// P20 证据用:按相机做视口裁剪,只绘制可见瓦片(1:1 像素),返回实际绘制数。 @@ -866,7 +928,8 @@ module VillageArt = (viewportHeight: int) (map: MapGen.Result) (tick: int64) - (tint: XnaColor) : int = + (tint: XnaColor) + (glowScale: float32) : int = let x0, y0, x1, y1 = MapGen.visibleTileRange map.Width map.Height (int camera.X) (int camera.Y) viewportWidth viewportHeight let tileX x = x * Sim.tilePixels - int camera.X @@ -887,6 +950,8 @@ module VillageArt = spriteBatch.Draw(textures.TileAtlas, XnaRectangle(tileX x, tileY y, Sim.tilePixels, Sim.tilePixels), tileSourceRectangle (spriteOf x y code), tint) drawn <- drawn + 1 drawMapStructures spriteBatch textures pixel tileX tileY Sim.tilePixels map + if glowScale > 0.0f then + drawNightGlows spriteBatch pixel map (4 * Sim.tilePixels) (int (150.0f * glowScale)) tileX tileY Sim.tilePixels drawn let drawCharacter |
