From fdb28e93af2ee6413659ab4a773f80a9b67a3100 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 28 Sep 2026 15:58:36 +0800 Subject: art(p39): 自绘中式元素补入 cc0 图集(索引 12-18,608x32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/make-cc0-art.py 由 384x32(12格) 扩到 608x32(19格),新增 draw_bridge_center/end、draw_boat、draw_stone_lantern、draw_stall、draw_reeds - 旧索引 0-11 逐字节不变;产物 1523 bytes,sha256 ef86ced2…6adcb,二次生成一致 - docs/素材来源.md 更新为 19 格并新增第 4 节(P39 清单 + 摆放规则 + fallback 差异) --- ...264\240\346\235\220\346\235\245\346\272\220.md" | 41 ++++- scripts/make-cc0-art.py | 201 ++++++++++++++++++++- src/LivingVillage.Desktop/Assets/cc0-tiles.png | Bin 816 -> 1523 bytes 3 files changed, 233 insertions(+), 9 deletions(-) diff --git "a/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" "b/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" index 31494e9..98c2538 100644 --- "a/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" +++ "b/docs/\347\264\240\346\235\220\346\235\245\346\272\220.md" @@ -11,13 +11,13 @@ P34 扩展到水面 / 屋顶 / 墙体 / 栏杆等构件。仍是竖直切片, ```bash python3 scripts/make-cc0-art.py - # -> src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 个 32x32 格) + # -> src/LivingVillage.Desktop/Assets/cc0-tiles.png 608x32 (19 个 32x32 格) # -> src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 变体 x 4 向 x 6 帧, 32x48 格) ``` - `cc0-tiles.png` 前 4 格来自 Kenney(源 16x16,**整数 2x NEAREST** 到 32x32),后 8 格为 - P34 自绘(详见第 3 节);角色图为 Puny 源 16x24 整数 2x 到 32x48。缩放断言见 - `src/LivingVillage.Desktop.Tests/AssetPackTests.fs`。 + `cc0-tiles.png` 前 4 格来自 Kenney(源 16x16,**整数 2x NEAREST** 到 32x32),后 15 格为 + 自绘(P34 的 8 格 + P39 的 7 格,详见第 3、4 节);角色图为 Puny 源 16x24 整数 2x 到 32x48。 + 缩放断言见 `src/LivingVillage.Desktop.Tests/AssetPackTests.fs`。 ## 1. Kenney Tiny Farm(地块) @@ -67,10 +67,41 @@ Kenney Tiny Farm 无合适的瓦屋顶 / 白墙 / 木格窗 / 栏杆 / 水波 / 确定性、可复现,无外部素材。 - 水网/水田(生成器 `code=4`)与样例河道共用同一自绘 2 相位水面。 +## 4. 自绘中式元素补绘(P39,原创,无第三方来源) + +在不引入新付费素材的前提下扩展 cc0 图集自绘部分,让江南味道更足:追加 7 个 32px 自绘格到 +`cc0-tiles.png` **尾部(索引 12-18)**,**旧格 0-11 索引与像素逐字节不变**(向后兼容)。与 P34 +同一江南调色板(青灰瓦、白灰墙、暖木构件、碧水),全部**原创、无外部来源、无需第三方许可**。 +逐格: + +| 索引 | 内容 | 说明 | +| --- | --- | --- | +| 12 | 石桥中段 | 拱桥石桥面 + 木栏杆,下层留透明拱洞露出水波 | +| 13 | 石桥桥台 | 岸边石阶(上桥台阶)+ 端柱 | +| 14 | 乌篷船(左半) | 深色船体 + 竹篷篷顶;浮在水面画法(船帮以下留水面波纹) | +| 15 | 乌篷船(右半) | 与左半连续成 2 格整船 | +| 16 | 石灯笼 | 石座/石身 + 暖色发光窗芯(夜帧接光晕) | +| 17 | 路摊/菜摊 | 红白条纹棚 + 柜台 + 果蔬筐 | +| 18 | 芦苇 | 多株茎秆 + 叶 + 穗,贴水岸格 | + +- 自绘代码位于 `scripts/make-cc0-art.py`(`draw_bridge_center`/`draw_bridge_end`/`draw_boat`/ + `draw_stone_lantern`/`draw_stall`/`draw_reeds`),确定性、可复现,无外部素材;全部在最终 1x + 分辨率下直接落像素(无重采样),故 2x NEAREST 放大后无混色。 +- 生成器:`python3 scripts/make-cc0-art.py` -> `cc0-tiles.png` **608x32(19 格)**; + 二次运行 byte-identical。 +- 摆放规则(`src/LivingVillage.Desktop/VillageArt.fs`,均为 (map, seed) 纯函数,不写瓦片、不改可达性): + - 桥(12/13):每座桥列的最上一格铺中段、再上一格铺桥台——桥跨河,桥台贴岸。 + - 船(14/15):开阔水面泊 2 格船,优先靠近某座桥(桥/船/苇常同框),距离平局按 hash 裁决。 + - 石灯笼(16):沿主路中心线每隔 12 列一盏;并入 `nightLightTiles`,复用 P29/P32 夜晚光晕路径。 + - 路摊(17):主路灯笼之间(错开 6 列)一格,仅美术,不接交互逻辑。 + - 芦苇(18):陆地格且上下邻格为水(贴岸),每 4 列一格。 + - **fallback(非 cc0)模式不摆放这些新元素**,仍走原程序绘制江南图集。 + + ## 说明 - Kenney/Puny 原始 zip 均随仓库记录于 `third_party/`(`PROVENANCE.md` 为总来源记录),仓库 分发的只是上述再生成图集,不声称拥有未包含素材;自绘构件为原创。 - 版权/许可复核:见 `third_party/` 内原始文件与发布页;重跑 `make-cc0-art.py` 前后对 `third_party/*.zip` 重新 `sha256sum` 校验。 -- 范围仍为切片:动画全套 / 全村 / 更多中式元素补绘留待后续。 +- 范围仍为切片:动画全套 / 全村 / 更多中式元素补绘留待后续。 \ No newline at end of file diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py index 44bd013..7c5868c 100644 --- a/scripts/make-cc0-art.py +++ b/scripts/make-cc0-art.py @@ -4,7 +4,7 @@ Reads the provenanced CC0 packs in third_party/ (Kenney Tiny Farm tiles, Puny Characters) and emits deterministic atlases for the prototype: - src/LivingVillage.Desktop/Assets/cc0-tiles.png 384x32 (12 tiles, 32px each) + src/LivingVillage.Desktop/Assets/cc0-tiles.png 608x32 (19 tiles, 32px each) src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 variants x 4 directions x 6 frames, 32x48 cells) Ground tiles (indices 0-3) come from Kenney Tiny Farm via integer 2x NEAREST of @@ -13,6 +13,14 @@ art (it is a farm pack), so indices 4-11 are *self-drawn* 32px pixel art in a consistent Jiangnan palette (white plaster, dark slate tile, warm wood). Each tile's provenance is recorded in docs/素材来源.md. +P39 (indices 12-18) appends more self-drawn Jiangnan elements after the P34 +block, keeping every older index byte-for-byte backwards compatible: + 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right + 16 stone lantern / 17 market stall / 18 reeds +Bridge/boat/lantern/stall/reeds are all self-drawn (Kenney Tiny Farm has none +of them), same palette, same 32px grid, NEAREST-safe (no resampling: drawn at +final 1x resolution, so pixel edges are exact). + Mapping strategy (16 -> 32): integer 2x nearest-neighbour upscale of every source sprite; ground tiles fill the 32x32 cell exactly; Puny sprites keep their 16x24-ish content band (2x -> 32x48) anchored to the cell bottom. @@ -197,8 +205,181 @@ def draw_door(c, ox): c.fill(ox + 20, 17, 3, 3, (222, 190, 120, 255)) +# --- P39 self-drawn Jiangnan elements (indices 12-18) ------------------------- +STONE = (150, 150, 142, 255) +STONE_DARK = (104, 106, 102, 255) +STONE_HI = (196, 196, 188, 255) +BRIDGE_DECK = (128, 128, 122, 255) +BOAT_HULL = (58, 48, 40, 255) +BOAT_HULL_DARK = (38, 32, 27, 255) +BOAT_ROOF = (46, 54, 42, 255) +BOAT_ROOF_HI = (76, 88, 66, 255) +LANTERN_PAPER = (228, 196, 128, 255) +LANTERN_GLOW = (255, 208, 150, 255) +LANTERN_GLOW_HI = (255, 232, 190, 255) +LANTERN_STONE = (168, 166, 156, 255) +REED_STEM = (74, 104, 60, 255) +REED_LEAF = (92, 130, 72, 255) +REED_TIP = (146, 158, 96, 255) +STALL_AWNING = (176, 74, 58, 255) +STALL_AWNING_2 = (196, 176, 132, 255) +CARROT = (216, 126, 54, 255) +CABBAGE = (120, 168, 84, 255) +CLOTH = (208, 198, 172, 255) + + +def draw_bridge_center(c, ox): + """Arch bridge span: stone deck with a railing and an arched underside. + + The lower third is transparent so the water ripple still shows through the + arch; a warm railing sits on top of the deck. + """ + c.fill(ox, 0, 32, 32, CLEAR) + # Deck (single stone span, bright top edge / dark underside). + c.fill(ox, 10, 32, 9, BRIDGE_DECK) + c.fill(ox, 10, 32, 2, STONE_HI) + c.fill(ox, 17, 32, 2, STONE_DARK) + # Arch cut-outs under the deck (transparent so water shows through). + c.fill(ox + 6, 19, 8, 13, CLEAR) + c.fill(ox + 18, 19, 8, 13, CLEAR) + c.fill(ox + 10, 19, 12, 5, BRIDGE_DECK) + # Wood railing above the deck. + c.fill(ox, 4, 32, 2, WOOD_DARK) + c.fill(ox, 7, 32, 2, WOOD) + for px in (3, 14, 25): + c.fill(ox + px, 4, 2, 6, WOOD) + c.set(ox + px, 9, WOOD_DARK) + + +def draw_bridge_end(c, ox): + """Bridge end: stone abutment at the bank plus the last railing posts.""" + c.fill(ox, 0, 32, 32, CLEAR) + # Stepped abutment: two stone steps widening down to the bank. + c.fill(ox + 4, 12, 24, 6, STONE) + c.fill(ox + 4, 12, 24, 2, STONE_HI) + c.fill(ox, 18, 32, 14, STONE) + c.fill(ox, 18, 32, 2, STONE_HI) + c.fill(ox, 29, 32, 3, STONE_DARK) + # End posts of the railing. + c.fill(ox + 2, 3, 3, 11, WOOD_DARK) + c.fill(ox + 2, 3, 3, 2, WOOD) + c.fill(ox + 27, 3, 3, 11, WOOD_DARK) + c.fill(ox + 27, 3, 3, 2, WOOD) + c.fill(ox + 2, 6, 28, 2, WOOD) + + +def draw_boat(c, ox, right_half): + """Wu Peng boat half (2 tiles wide): dark hull + curved bamboo awning. + + Drawn as two halves so a 2-tile boat reads as one continuous silhouette. + The hull floats on the water: its top edge (gunwale) is 4px down, so open + water ripple stays visible above the boat on the tile. + """ + c.fill(ox, 0, 32, 32, CLEAR) + if not right_half: + # Left half: bow rising to the left, hull tapering. + for i in range(26): + half = 3 + i // 3 + c.fill(ox + 0, 10 + i, half, 1, BOAT_HULL) + c.fill(ox, 10, 3, 1, BOAT_HULL_DARK) + c.fill(ox + 12, 9, 20, 2, BOAT_HULL_DARK) + c.fill(ox + 12, 11, 20, 13, BOAT_HULL) + # Awning (wu peng) left edge, arch top. + c.fill(ox + 12, 1, 20, 8, BOAT_ROOF) + c.fill(ox + 12, 1, 20, 2, BOAT_ROOF_HI) + c.fill(ox + 12, 1, 2, 8, BOAT_ROOF_HI) + c.fill(ox + 12, 8, 20, 2, BOAT_HULL_DARK) + # Stern post at the extreme left of the visible half. + c.fill(ox, 6, 2, 6, BOAT_HULL_DARK) + else: + # Right half: stern rising to the right, mirrored continuous hull. + for i in range(26): + half = 3 + i // 3 + c.fill(ox + 32 - half, 10 + i, half, 1, BOAT_HULL) + c.fill(ox + 29, 10, 3, 1, BOAT_HULL_DARK) + c.fill(ox, 9, 20, 2, BOAT_HULL_DARK) + c.fill(ox, 11, 20, 13, BOAT_HULL) + c.fill(ox, 1, 20, 8, BOAT_ROOF) + c.fill(ox, 1, 20, 2, BOAT_ROOF_HI) + c.fill(ox + 18, 1, 2, 8, BOAT_ROOF_HI) + c.fill(ox, 8, 20, 2, BOAT_HULL_DARK) + c.fill(ox + 30, 6, 2, 6, BOAT_HULL_DARK) + + +def draw_stone_lantern(c, ox): + """Ishidoro-style stone lantern with a warm lit window (night glow anchor). + + The lit pane is a bright warm core (alpha opaque) sitting inside a stone + housing, so the renderer's radial glow layer adds a real halo on top. + """ + c.fill(ox, 0, 32, 32, CLEAR) + # Base plinth. + c.fill(ox + 8, 27, 16, 5, LANTERN_STONE) + c.fill(ox + 8, 27, 16, 1, STONE_HI) + c.fill(ox + 11, 24, 10, 4, LANTERN_STONE) + # Shaft. + c.fill(ox + 14, 18, 4, 7, LANTERN_STONE) + # Light housing with a bright warm pane. + c.fill(ox + 6, 9, 20, 10, LANTERN_STONE) + c.fill(ox + 6, 9, 20, 1, STONE_HI) + c.fill(ox + 9, 11, 14, 7, LANTERN_GLOW) + c.fill(ox + 11, 12, 4, 5, LANTERN_GLOW_HI) + # Dark frame over the pane. + c.vline(ox + 9, 11, 7, STONE_DARK) + c.vline(ox + 22, 11, 7, STONE_DARK) + c.vline(ox + 15, 11, 7, STONE_DARK) + # Cap + finial. + c.fill(ox + 4, 6, 24, 4, STONE_DARK) + c.fill(ox + 6, 5, 20, 2, LANTERN_STONE) + c.fill(ox + 15, 1, 3, 4, LANTERN_STONE) + + +def draw_stall(c, ox): + """Roadside market stall: striped awning + counter + produce baskets.""" + c.fill(ox, 0, 32, 32, CLEAR) + # Awning: red/cream stripes with a scalloped lower edge. + for x in range(0, 32, 4): + stripe = STALL_AWNING if (x // 4) % 2 == 0 else STALL_AWNING_2 + c.fill(ox + x, 6, 4, 8, stripe) + c.fill(ox, 13, 32, 2, WOOD_DARK) + for x in range(0, 32, 8): + c.fill(ox + x, 15, 8, 2, STALL_AWNING_2) + c.set(ox + x + 4, 17, WOOD_DARK) + # Support poles. + c.vline(ox + 2, 15, 13, WOOD_DARK) + c.vline(ox + 28, 15, 13, WOOD_DARK) + # Counter. + c.fill(ox + 2, 20, 28, 4, WOOD) + c.fill(ox + 2, 20, 28, 1, WOOD_DARK) + # Produce on the counter. + c.fill(ox + 6, 17, 4, 3, CARROT) + c.fill(ox + 7, 16, 2, 1, REED_LEAF) + c.fill(ox + 14, 16, 6, 4, CABBAGE) + c.fill(ox + 15, 15, 4, 1, REED_LEAF) + c.fill(ox + 24, 17, 4, 3, CLOTH) + # Base. + c.fill(ox + 2, 24, 28, 4, WOOD_DARK) + + +def draw_reeds(c, ox): + """Waterside reeds: a few stems with leaf blades and seed tips.""" + c.fill(ox, 0, 32, 32, CLEAR) + stems = [(6, 6, 24), (13, 10, 22), (21, 7, 23), (26, 13, 20)] + for (x, top, bottom) in stems: + c.fill(ox + x, top, 2, bottom - top, REED_STEM) + c.fill(ox + x, top, 2, 3, REED_TIP) + # Leaf blades angling off the stem. + c.fill(ox + x - 4, top + 5, 5, 1, REED_LEAF) + c.fill(ox + x - 3, top + 6, 4, 1, REED_LEAF) + c.fill(ox + x + 2, top + 9, 4, 1, REED_LEAF) + # Dense root cluster at the waterline (bottom of the tile). + c.fill(ox + 3, 28, 26, 4, REED_LEAF) + c.fill(ox + 8, 30, 16, 2, REED_STEM) + + + def build_cc0_tile_atlas(): - """Twelve 32x32 tiles: 4 Kenney ground + 8 self-drawn structure tiles. + """Nineteen 32x32 tiles: 4 Kenney ground + 15 self-drawn Jiangnan tiles. Ground source is the *packed* sheet (tilemap_packed.png) which has NO spacing between tiles: 192x176 = 12x11 tiles at a 16px pitch (the spaced tilemap.png @@ -208,8 +389,12 @@ def build_cc0_tile_atlas(): wall, railing or door tiles), layout fixed by the P34 atlas contract: 4 water A / 5 water B / 6 roof ridge / 7 roof eave 8 wall wood / 9 wall window / 10 railing / 11 door + P39 appends indices 12-18 (bridge / boat / lantern / stall / reeds) after the + P34 block so every older index stays unchanged and backwards compatible: + 12 bridge centre / 13 bridge end / 14 boat left / 15 boat right + 16 stone lantern / 17 market stall / 18 reeds """ - c = Canvas(384, 32) + c = Canvas(608, 32) packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA") def tile(grid_x, grid_y): @@ -227,6 +412,14 @@ def build_cc0_tile_atlas(): draw_wall(c, 9 * 32, window=True) draw_railing(c, 10 * 32) draw_door(c, 11 * 32) + # P39 appended block. + draw_bridge_center(c, 12 * 32) + draw_bridge_end(c, 13 * 32) + draw_boat(c, 14 * 32, right_half=False) + draw_boat(c, 15 * 32, right_half=True) + draw_stone_lantern(c, 16 * 32) + draw_stall(c, 17 * 32) + draw_reeds(c, 18 * 32) return c @@ -281,7 +474,7 @@ def build_cc0_character_atlas(): def main(): ASSETS.mkdir(parents=True, exist_ok=True) targets = [ - ("cc0-tiles.png", build_cc0_tile_atlas(), 384, 32), + ("cc0-tiles.png", build_cc0_tile_atlas(), 608, 32), ("cc0-characters.png", build_cc0_character_atlas(), 3840, 48), ] for name, canvas, w, h in targets: diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png index afa46a4..b8b808a 100644 Binary files a/src/LivingVillage.Desktop/Assets/cc0-tiles.png and b/src/LivingVillage.Desktop/Assets/cc0-tiles.png differ -- cgit v1.2.3