namespace LivingVillage.Desktop open System open Microsoft.Xna.Framework open Microsoft.Xna.Framework.Graphics open LivingVillage.Kernel /// P47 场景细节渲染层:把 `SceneDetail` 的纯函数结果落成画面。 /// 倒影为每个光源正下方一条暖色垂直光柱(核心 + 外晕,离光源越远越淡), /// 岸芦苇/漂浮元素用既有瓦片精灵与原创点阵图集,近景屋顶按统一光源分受光面/背光面。 /// 全部只读,不改瓦片/可达性。 module SceneDetailRender = let private tileX (camera: Vector2) (x: int) = x * Sim.tilePixels - int camera.X let private tileY (camera: Vector2) (y: int) = y * Sim.tilePixels - int camera.Y /// 河道倒影:每个光源在正下方水面格画一段暖色垂直光柱(外晕 20px + 核心 10px), /// alpha 随离光源距离递减、逐格 ±1px 微抖,连成一条可辨认的暖色垂直条纹 /// (明显区别于蓝灰横向波纹)。 let private drawReflections (spriteBatch: SpriteBatch) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (tick: int64) = for (x, y, alpha, _) in SceneDetail.reflectionStreaks map do let wobble = SceneDetail.reflectionWobbleAt tick x y let cx = tileX camera x let cy = tileY camera y let halo = VillageArt.premultiply 255 150 70 (alpha * 3 / 5) let core = VillageArt.premultiply 255 208 120 alpha spriteBatch.Draw(pixel, Rectangle(cx + 4 + wobble, cy, 24, Sim.tilePixels), halo) spriteBatch.Draw(pixel, Rectangle(cx + 10 + wobble, cy, 12, Sim.tilePixels), core) /// 岸边芦苇加密度:用既有芦苇精灵补绘一层贴水芦苇。 let private drawBankReeds (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (camera: Vector2) (map: MapGen.Result) = match textures.Cc0TileAtlas with | Some cc0 -> for (x, y) in SceneDetail.bankReedTiles map do spriteBatch.Draw(cc0, Rectangle(tileX camera x, tileY camera y, Sim.tilePixels, Sim.tilePixels), Nullable(VillageArt.cc0TileRect VillageArt.cc0TileIndexReeds), Color.White) | None -> let source = VillageArt.atlasTileRectangle 18 for (x, y) in SceneDetail.bankReedTiles map do spriteBatch.Draw(textures.TileAtlas, Rectangle(tileX camera x, tileY camera y, Sim.tilePixels, Sim.tilePixels), Nullable(source), Color.White) /// 漂浮元素:原创点阵鸭/叶,按 tick 慢漂移。 let private drawFloaters (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (camera: Vector2) (map: MapGen.Result) (tick: int64) = for (x, y, kind) in SceneDetail.floatingProps map do let seed = x * 31 + y * 7 let dx, dy = SceneDetail.floatDrift tick seed let frame = SceneDetail.floatFrame tick seed let kindIndex = match kind with | SceneDetail.DuckFloat -> FloaterArt.duckIndex | SceneDetail.FallenLeaf -> FloaterArt.leafIndex let source = FloaterArt.sourceRectangle kindIndex frame spriteBatch.Draw( textures.FloaterAtlas, Rectangle(tileX camera x + dx + (Sim.tilePixels - FloaterArt.cellWidth) / 2, tileY camera y + dy + (Sim.tilePixels - FloaterArt.cellHeight) / 2, FloaterArt.cellWidth, FloaterArt.cellHeight), Nullable(source), Color.White) /// 近景楼层层错落:屏幕空间前景屋顶带。统一光源来自左上,故每段屋顶 /// 左半受光(暖亮)、右半背光(压暗),并整体投影到右下(shadowOffsetPx)。 let private drawForeground (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (pixel: Texture2D) (viewportWidth: int) (viewportHeight: int) (tick: int64) = let ridgeSource = VillageArt.atlasTileRectangle 8 let eaveSource = VillageArt.atlasTileRectangle 9 let shadowColor = VillageArt.premultiply 8 12 18 120 let litColor = VillageArt.premultiply 255 236 196 46 let darkColor = VillageArt.premultiply 6 10 18 110 let dx, dy = SceneDetail.shadowOffsetPx for (x, y, w) in SceneDetail.foregroundRooftops viewportWidth viewportHeight tick do // 统一方向的投影:先把 eave 的暗色镜像平移一份到右下,再画本体。 spriteBatch.Draw(textures.TileAtlas, Rectangle(x + dx, y + dy, w, Sim.tilePixels), Nullable(eaveSource), shadowColor) spriteBatch.Draw(textures.TileAtlas, Rectangle(x, y - Sim.tilePixels, w, Sim.tilePixels), Nullable(ridgeSource), Color.White) spriteBatch.Draw(textures.TileAtlas, Rectangle(x, y, w, Sim.tilePixels), Nullable(eaveSource), Color.White) // 受光面 / 背光面:左半加暖亮、右半压暗,形成两档以上 luma 差。 let half = max 1 (w / 2) spriteBatch.Draw(pixel, Rectangle(x, y - Sim.tilePixels, half, 2 * Sim.tilePixels), litColor) spriteBatch.Draw(pixel, Rectangle(x + half, y - Sim.tilePixels, w - half, 2 * Sim.tilePixels), darkColor) /// P78 炊烟:生成器世界每户屋顶脊上方一缕,帧号驱动升腾/摇曳,夜帧更醒目。 /// 复用江南图集的 22-24 三相位烟精灵,只读、不改瓦片。 let private drawSmoke (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (camera: Vector2) (map: MapGen.Result) (tick: int64) (nightStrength: float32) = let alpha = SceneDetail.smokeAlphaAt nightStrength let size = SceneDetail.smokeCellSize nightStrength let source = VillageArt.atlasTileRectangle (22 + SceneDetail.smokeFrame tick) let color = if nightStrength > 0.5f then VillageArt.premultiply 238 230 214 alpha else VillageArt.premultiply 222 226 228 alpha for (x, y) in SceneDetail.smokeSourceTiles map do let seed = x * 29 + y * 11 let dx, dy = SceneDetail.smokeDrift tick seed let cx = tileX camera x + (Sim.tilePixels - size) / 2 + dx let cy = tileY camera y + (Sim.tilePixels - size) / 2 + dy spriteBatch.Draw(textures.TileAtlas, Rectangle(cx, cy, size, size), source, color) /// P78 夜帧水面逐格微光斑:帧号驱动、位置确定性,暗部因此有两档以上层次而非整片死黑。 let private drawNightWater (spriteBatch: SpriteBatch) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (tick: int64) (nightStrength: float32) = for (x, y) in SceneDetail.waterGlintTiles map do let alpha = SceneDetail.waterGlintAlpha nightStrength (SceneDetail.waterGlintPhase tick x y) if alpha > 0 then let wobble = SceneDetail.waterGlintOffset tick x y let cx = tileX camera x let cy = tileY camera y let soft = VillageArt.premultiply 150 190 232 (alpha / 3) let cool = VillageArt.premultiply 198 224 255 alpha spriteBatch.Draw(pixel, Rectangle(cx + 6 + wobble, cy + 12, 20, 4), soft) spriteBatch.Draw(pixel, Rectangle(cx + 10 + wobble, cy + 13, 11, 2), cool) /// P78 桥头灯柱窄光晕:只围灯柱一圈(半径 1 tile),夜里给灯柱一圈暖光而不铺满河面。 let private drawBridgeHalos (spriteBatch: SpriteBatch) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (nightStrength: float32) = let peak = int (78.0f * max 0.0f (min 1.0f nightStrength)) if peak > 0 then let half = Sim.tilePixels / 2 let radius = SceneDetail.bridgeHaloRadiusTiles * Sim.tilePixels for (x, y) in SceneDetail.bridgeLanternTiles map do let cx = tileX camera x + half let cy = tileY camera y + half for (rect, color) in VillageArt.lanternGlowLayers cx cy radius 5 255 214 150 peak do spriteBatch.Draw(pixel, rect, color) /// P79 夜帧炊烟增强:烟柱(3 段向上堆叠)+ 外发光。只在夜帧叠加绘制,白天帧不受影响。 let private drawNightSmoke (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (tick: int64) (nightStrength: float32) = let alpha = SceneDetail.nightSmokeAlphaAt nightStrength let glowPeak = SceneDetail.nightSmokeGlowPeakAt nightStrength let source = VillageArt.atlasTileRectangle (22 + SceneDetail.smokeFrame tick) let cell = SceneDetail.smokeCellSize nightStrength for (x, y) in SceneDetail.smokeSourceTiles map do let seed = x * 29 + y * 11 let sway, rise = SceneDetail.smokeDrift tick seed let baseX = tileX camera x + (Sim.tilePixels - cell) / 2 let baseY = tileY camera y + (Sim.tilePixels - cell) / 2 // 外发光:暖色径向柔光,让烟柱在夜色里浮起。 for (rect, glowColor) in VillageArt.lanternGlowLayers (baseX + cell / 2) (baseY + rise) SceneDetail.nightSmokeGlowRadius 4 255 236 200 glowPeak do spriteBatch.Draw(pixel, rect, glowColor) // 烟柱:自屋顶向上堆叠,越上越淡,并在每段叠一枚中性柔光团(外发光), // 弥补烟精灵本身 alpha 很低的问题,使夜色里每一缕烟柱都清晰可辨。 for segment in 0 .. SceneDetail.nightSmokeSegments - 1 do let segmentAlpha = max 1 (alpha * (SceneDetail.nightSmokeSegments - segment) / SceneDetail.nightSmokeSegments) let segmentColor = VillageArt.premultiply 246 242 232 segmentAlpha let sx = baseX + sway * (segment + 1) / 2 let sy = baseY + rise - segment * SceneDetail.nightSmokeSegmentLift spriteBatch.Draw(textures.TileAtlas, Rectangle(sx, sy, cell, cell), source, segmentColor) let puffPeak = max 1 (glowPeak * (SceneDetail.nightSmokeSegments - segment) / (SceneDetail.nightSmokeSegments + 1)) for (rect, puffColor) in VillageArt.lanternGlowLayers (sx + cell / 2) (sy + cell / 2) 12 3 238 242 248 puffPeak do spriteBatch.Draw(pixel, rect, puffColor) /// P79 夜景光晕去叠:每个光源最多 2 层(内芯 + 外环)、层间暗隙、半径收紧到 3 tile。 let private drawNightHalos (spriteBatch: SpriteBatch) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (includeRoadLanterns: bool) (nightStrength: float32) = let peak = SceneDetail.nightHaloPeakAlphaAt nightStrength if peak > 0 then let half = Sim.tilePixels / 2 let radius = SceneDetail.nightHaloRadiusTiles * Sim.tilePixels for (lx, ly) in VillageArt.nightLightTilesWith includeRoadLanterns map do let cx = tileX camera lx + half let cy = tileY camera ly + half for (rect, color) in VillageArt.haloRingLayers cx cy radius peak do spriteBatch.Draw(pixel, rect, color) /// P78 夜帧加成层(水面光斑 + 桥头窄光晕):在 veil 之后调用,故不被夜色乘暗、仍醒目。 let drawNight (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (pixel: Texture2D) (camera: Vector2) (map: MapGen.Result) (tick: int64) (nightStrength: float32) = drawNightWater spriteBatch pixel camera map tick nightStrength drawBridgeHalos spriteBatch pixel camera map nightStrength drawNightHalos spriteBatch pixel camera map textures.Cc0TileAtlas.IsSome nightStrength drawNightSmoke spriteBatch textures pixel camera map tick nightStrength /// 一次性绘制全部 P47 场景细节(倒影 → 岸边芦苇 → 漂浮元素 → 近景前景层 → P78 炊烟)。 let draw (spriteBatch: SpriteBatch) (textures: VillageArt.ArtTextures) (pixel: Texture2D) (camera: Vector2) (viewportWidth: int) (viewportHeight: int) (map: MapGen.Result) (tick: int64) (nightStrength: float32) = drawReflections spriteBatch pixel camera map tick drawBankReeds spriteBatch textures camera map drawFloaters spriteBatch textures camera map tick drawForeground spriteBatch textures pixel viewportWidth viewportHeight tick drawSmoke spriteBatch textures camera map tick nightStrength