1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
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)
/// P78 夜帧加成层(水面光斑 + 桥头窄光晕):在 veil 之后调用,故不被夜色乘暗、仍醒目。
let drawNight
(spriteBatch: SpriteBatch)
(pixel: Texture2D)
(camera: Vector2)
(map: MapGen.Result)
(tick: int64)
(nightStrength: float32) =
drawNightWater spriteBatch pixel camera map tick nightStrength
drawBridgeHalos spriteBatch pixel camera map 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
|