summaryrefslogtreecommitdiff
path: root/src/LivingVillage.Desktop/VillageArt.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 12:10:42 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 12:10:42 +0800
commita70a057fce7afa5787b2d4a037d7016f1120ef6a (patch)
treece5a24cf07dbbfd46cee48cbec561cc23349f48e /src/LivingVillage.Desktop/VillageArt.fs
parent3cd5a635d12474647c4cf288a5cc4acb4e0eb1b5 (diff)
downloadliving-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.fs85
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