summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-28 08:33:20 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-28 08:33:20 +0800
commit0e9948b7067e07875a9fe81879be0ae46664a785 (patch)
tree4c0decedfb6826d7fb26248a4dea25e41a60ffcc
parentd92afbcb787eb91c4e6d001b1f0de04f0fc5813d (diff)
downloadliving-village-0e9948b7067e07875a9fe81879be0ae46664a785.tar.gz
feat(art): P33 接入 CC0 真实像素素材样板(地面+角色竖直切片)
- 修复 make-cc0-art.py 地块取样间距:packed sheet 无间距(16px pitch),旧 17px 误采错格 - 重选草地/石板格 (9,8)/(10,8)/(4,8)/(4,9),整数 2x NEAREST 重建 cc0-tiles.png - VillageArt 暴露 selectedGround/CharacterAtlasRelativePath 纯函数,保留 LV_ASSET_PACK=cc0 开关与程序绘制江南图集 fallback - 新增 AssetPackTests:图集加载尺寸、整数倍缩放、开关/回退断言 - docs/素材来源.md 记录来源/许可/sha256/构建/开关;同步 assets-and-licenses 与维护说明 P33 小节 - docs/evidence/p33-atlas-*.png 昼/夜/2x 特写/程序绘制对照(check-no-white-blocks 0 命中)
-rw-r--r--docs/assets-and-licenses.md6
-rw-r--r--docs/evidence/p33-atlas-baseline-day.pngbin0 -> 58740 bytes
-rw-r--r--docs/evidence/p33-atlas-closeup-2x.pngbin0 -> 23768 bytes
-rw-r--r--docs/evidence/p33-atlas-day.pngbin0 -> 45993 bytes
-rw-r--r--docs/evidence/p33-atlas-night.pngbin0 -> 59331 bytes
-rw-r--r--docs/素材来源.md53
-rw-r--r--docs/维护说明.md40
-rw-r--r--scripts/make-cc0-art.py11
-rw-r--r--src/LivingVillage.Desktop.Tests/AssetPackTests.fs73
-rw-r--r--src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj1
-rw-r--r--src/LivingVillage.Desktop/Assets/cc0-tiles.pngbin524 -> 388 bytes
-rw-r--r--src/LivingVillage.Desktop/VillageArt.fs36
12 files changed, 209 insertions, 11 deletions
diff --git a/docs/assets-and-licenses.md b/docs/assets-and-licenses.md
index 10bd69e..1d691a7 100644
--- a/docs/assets-and-licenses.md
+++ b/docs/assets-and-licenses.md
@@ -8,7 +8,11 @@
- 来源:`/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc`,字体族包含 Noto Sans CJK SC。
- 许可:SIL Open Font License 1.1;本地版权文件 SHA-256:`849f4ea9c214fa4ac3593b770c699f387534b11ce671264c1b10d85bdcb5997b`。
- 参考:<https://raw.githubusercontent.com/notofonts/noto-cjk/main/README.md>。
-- **其他外部美术资产**:当前仓库没有 JPG、GIF、SVG、其他字体或音频文件。设计文档提到的 Kenney CC 资产只是候选方向,未随仓库或 M6c 包分发。
+- **第三方 CC0 真实像素素材图集(P33 起)**:`src/LivingVillage.Desktop/Assets/cc0-tiles.png`(128x32,地块)与 `cc0-characters.png`(3840x48,角色)由 `scripts/make-cc0-art.py` 从两个 CC0 素材包确定性再生成(整数 2x NEAREST):
+ - **Kenney Tiny Farm 1.0**:来源 <https://kenney.nl/assets/tiny-farm>,许可 CC0 1.0(包内 `License.txt`),zip SHA-256 `a06f75f3…decc`。
+ - **Puny Characters(Shade)**:来源 <https://opengameart.org/content/puny-characters>,许可 CC0 1.0(发布页),zip SHA-256 `971d7b07…320e`。
+ - 原始 zip 记录于 `third_party/`;用途/构建/开关详见 `docs/素材来源.md`。运行时仅在 `LV_ASSET_PACK=cc0` 时启用,默认仍走程序绘制江南图集。
+- **其他外部美术资产**:当前仓库没有 JPG、GIF、SVG、其他字体或音频文件。
- **Task 3b 边界**:中文 UI 使用上述许可明确的派生字形图集;场景、角色和其他 UI 图形仍由仓库代码生成,不下载或声称拥有未包含的第三方素材。
## 第三方运行依赖
diff --git a/docs/evidence/p33-atlas-baseline-day.png b/docs/evidence/p33-atlas-baseline-day.png
new file mode 100644
index 0000000..d8364e6
--- /dev/null
+++ b/docs/evidence/p33-atlas-baseline-day.png
Binary files differ
diff --git a/docs/evidence/p33-atlas-closeup-2x.png b/docs/evidence/p33-atlas-closeup-2x.png
new file mode 100644
index 0000000..37e34ce
--- /dev/null
+++ b/docs/evidence/p33-atlas-closeup-2x.png
Binary files differ
diff --git a/docs/evidence/p33-atlas-day.png b/docs/evidence/p33-atlas-day.png
new file mode 100644
index 0000000..ed6419f
--- /dev/null
+++ b/docs/evidence/p33-atlas-day.png
Binary files differ
diff --git a/docs/evidence/p33-atlas-night.png b/docs/evidence/p33-atlas-night.png
new file mode 100644
index 0000000..68fa823
--- /dev/null
+++ b/docs/evidence/p33-atlas-night.png
Binary files differ
diff --git a/docs/素材来源.md b/docs/素材来源.md
new file mode 100644
index 0000000..dd193de
--- /dev/null
+++ b/docs/素材来源.md
@@ -0,0 +1,53 @@
+# 第三方像素素材来源与许可
+
+P33 开始把「程序几何绘制」的样例村庄逐步替换为**真实像素素材**(统一像素风格)。
+本单只做第一条竖直切片:样例村庄的一类地面(草地/石板)与一类角色(村民/主角)接入
+CC0 素材,其余仍走原程序绘制江南图集作为 fallback。
+
+- **开关**:`LV_ASSET_PACK=cc0` 时启用真实素材包;未设置或取其它值一律回退到程序绘制
+ 图集(`Assets/jiangnan-*.png`)。图集选择入口见 `src/LivingVillage.Desktop/VillageArt.fs`
+ 的 `useCc0Pack` / `selectedGroundAtlasRelativePath` / `selectedCharacterAtlasRelativePath`。
+- **构建命令**(确定性,二次运行 byte-identical):
+
+ ```bash
+ python3 scripts/make-cc0-art.py
+ # -> src/LivingVillage.Desktop/Assets/cc0-tiles.png 128x32 (4 个 32x32 地面格)
+ # -> src/LivingVillage.Desktop/Assets/cc0-characters.png 3840x48 (5 变体 x 4 向 x 6 帧, 32x48 格)
+ ```
+
+ 源素材 16x16 / 16x24,**整数 2x NEAREST** 放大到 32x32 / 32x48;缩放断言见
+ `src/LivingVillage.Desktop.Tests/AssetPackTests.fs`。
+
+## 1. Kenney Tiny Farm(地块)
+
+- 名称/版本:Tiny Farm(1.0),作者 Kenney。
+- 来源页:<https://kenney.nl/assets/tiny-farm>
+- 直链:<https://kenney.nl/media/pages/assets/tiny-farm/dfded1ae3e-1782913588/kenney_tiny-farm.zip>
+- 许可:**CC0 1.0**(包内 `License.txt` 已保留于 `third_party/kenney_tiny-farm/License.txt`)。
+- 本地原始包:`third_party/kenney_tiny-farm.zip`
+- SHA-256:`a06f75f312c27eff15a2288475612e6f6699411be7259d408323cd15a790decc`(191128 bytes)
+- 获取日期:2026-09-21
+- 用法:取 `Tilemap/tilemap_packed.png`(192x176,12x11 格,**无间距、16px pitch**;带间距的
+ `tilemap.png` 为 203x186/17px)。本单取 (9,8)/(10,8) 两档草 + (4,8)/(4,9) 石板,
+ 整数 2x 生成 `Assets/cc0-tiles.png`。
+
+## 2. Puny Characters(角色)
+
+- 名称:Puny Characters,作者 Shade。
+- 来源页:<https://opengameart.org/content/puny-characters>(附件直链 `puny-characters.zip`)
+- 许可:**CC0 1.0**(OpenGameArt 页面 License(s): CC0,itch.io 发布页同为 CC0;包内无独立
+ 许可文件,以发布页为准)。
+- 本地原始包:`third_party/puny-characters.zip`
+- SHA-256:`971d7b07eea18fb2cce475cd6554a176ebf783bb064dec5a637ba33c7829320e`(235985 bytes)
+- 获取日期:2026-09-21
+- 用法:每个角色图 768x256(8 行 x 24 列,约 32px pitch);取南向 16x24 取带,整数 2x 放到
+ 32x48 格。包只提供南向姿态,北向复用南向、东/西为镜像(限制沿用 `scripts/make-cc0-art.py`
+ 注释)。生成 `Assets/cc0-characters.png`。
+
+## 说明
+
+- 两者原始 zip 均随仓库记录于 `third_party/`(`PROVENANCE.md` 为总来源记录),仓库分发的
+ 只是上述再生成图集,不声称拥有未包含素材。
+- 版权/许可复核:见 `third_party/` 内原始文件与发布页;重跑 `make-cc0-art.py` 前后对
+ `third_party/*.zip` 重新 `sha256sum` 校验(本单校验一致)。
+- 「中式元素补绘」为后续切片;本单只接入基础地块与一类角色,保证管线跑通。
diff --git a/docs/维护说明.md b/docs/维护说明.md
index c8047c8..96dcecd 100644
--- a/docs/维护说明.md
+++ b/docs/维护说明.md
@@ -1033,3 +1033,43 @@ python3 scripts/check-no-white-blocks.py /tmp/opencode/lv-p32-evidence/night
说明:`DrawMapFitted` 生成器渲染路径不绘制炊烟(炊烟只在样例村庄 `VillageArt.drawWorld` 的
late pass),故「灯笼光晕 + 水面」以生成器夜帧为准,「炊烟」见样例村庄昼间特写。所有帧
`check-no-white-blocks.py` 0 命中(无 ≥64x64 不透明纯白块)。
+
+## P33 素材接入样板(真实像素素材竖直切片)
+
+把样例村庄的**一类地面 + 一类角色**从程序绘制换成 CC0 真实像素素材,跑通接入管线;其余仍走
+原程序绘制图集 fallback。来源 / 许可 / sha256 完整记录见 `docs/素材来源.md`。
+
+- 构建(确定性,二次运行 byte-identical):
+
+ ```bash
+ python3 scripts/make-cc0-art.py
+ # -> Assets/cc0-tiles.png 128x32 (4 个 32x32 地面格)
+ # -> Assets/cc0-characters.png 3840x48 (5 变体 x 4 向 x 6 帧, 32x48 格)
+ ```
+
+ 同时修复了地块取样间距:源用无间距的 `tilemap_packed.png`(16px pitch),旧代码误按带间距
+ 的 17px pitch 采样导致错格。
+- 开关:`LV_ASSET_PACK=cc0` 启用真实素材;未设置或其它值回退程序绘制江南图集。选择入口
+ `VillageArt.useCc0Pack` / `selectedGroundAtlasRelativePath` / `selectedCharacterAtlasRelativePath`。
+- 规格:源 16x16 / 16x24 整数 **2x NEAREST** → 32x32 / 32x48;加载尺寸与整数倍由
+ `src/LivingVillage.Desktop.Tests/AssetPackTests.fs` 断言(含开关与 fallback)。
+- 证据(`docs/evidence/`,被 .gitignore 命中故 `git add -f`;`check-no-white-blocks.py` 4 帧 0 命中):
+ - `p33-atlas-day.png` / `p33-atlas-night.png`:样例村庄真实素材昼 / 夜整帧 1280x720。
+ - `p33-atlas-closeup-2x.png`:村庄地面 + 角色 2x NEAREST 特写(Kenney 草色 (78,151,76) 与
+ (132,198,105) 合计约 7.4 万像素,证实真素材上图)。
+ - `p33-atlas-baseline-day.png`:同场景程序绘制 fallback 对照(草色 (66,112,70)),与真实素材
+ 帧明显不同(昼帧约 92% 像素差异)。
+- 复现:
+
+ ```bash
+ DLL=src/LivingVillage.Desktop/bin/Release/net8.0/LivingVillage.Desktop.dll
+ for h in 12.0 23.5; do
+ tag=day; [ "$h" = 23.5 ] && tag=night
+ timeout 20 xvfb-run -a -s "-screen 0 1280x720x24" env LV_AUTOPLAY_SAMPLE=1 \
+ LV_AUTOPLAY_RECORD=1 LV_AUTOPLAY_START_HOUR=$h LV_RECORD_EVERY=10 \
+ LV_RECORD_NAME=p33-$tag- LV_ASSET_PACK=cc0 \
+ LV_RECORD_DIR=/tmp/opencode/lv-p33-evidence/$tag dotnet "$DLL"
+ done
+ ```
+
+- 范围:本单只接入基础地块与一类角色;动画全套 / 全村 / 中式元素补绘留待后续切片。
diff --git a/scripts/make-cc0-art.py b/scripts/make-cc0-art.py
index f5c97ee..b26cba1 100644
--- a/scripts/make-cc0-art.py
+++ b/scripts/make-cc0-art.py
@@ -98,16 +98,19 @@ def verify_png(path, expected_w, expected_h):
def build_cc0_tile_atlas():
"""Four 32x32 ground tiles from Kenney Tiny Farm, upscaled 2x.
- Tile selection is fixed by grid coordinates chosen from the pack's
- tilemap_packed.png (grass: two greens; stone path: pale slab; dirt: apricot).
+ 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 is 203x186
+ at 17px pitch; mixing the two misaligns every non-zero cell). Tile selection is
+ fixed by grid coordinates: two full grass shades (9,8)/(10,8) for the checker
+ ground, then two grey stone slabs (4,8)/(4,9) for the paved path.
"""
c = Canvas(128, 32)
packed = Image.open(THIRD / "kenney_tiny-farm" / "Tilemap" / "tilemap_packed.png").convert("RGBA")
def tile(grid_x, grid_y):
- return packed.crop((grid_x * 17, grid_y * 17, grid_x * 17 + 16, grid_y * 17 + 16))
+ return packed.crop((grid_x * 16, grid_y * 16, grid_x * 16 + 16, grid_y * 16 + 16))
- choices = [(10, 9), (9, 7), (6, 7), (6, 9)]
+ choices = [(9, 8), (10, 8), (4, 8), (4, 9)]
for index, (gx, gy) in enumerate(choices):
t = tile(gx, gy)
c.blit(t, index * 32, 0, scale=2)
diff --git a/src/LivingVillage.Desktop.Tests/AssetPackTests.fs b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs
new file mode 100644
index 0000000..fc235d0
--- /dev/null
+++ b/src/LivingVillage.Desktop.Tests/AssetPackTests.fs
@@ -0,0 +1,73 @@
+namespace LivingVillage.Desktop.Tests
+
+open System
+open System.IO
+open Microsoft.VisualStudio.TestTools.UnitTesting
+open LivingVillage.Kernel
+open LivingVillage.Kernel.Sim
+open LivingVillage.Desktop.VillageArt
+
+/// P33 vertical slice: the provenanced CC0 asset pack (Kenney Tiny Farm ground +
+/// Puny Characters) must load at the documented 32x32 / 32x48 shapes, be an exact
+/// integer 2x of the 16px / 16x24 source sprites, and stay behind the
+/// `LV_ASSET_PACK=cc0` switch so the procedural Jiangnan atlas is the fallback.
+[<TestClass>]
+type AssetPackTests () =
+
+ let assetPath (relative: string) =
+ Path.Combine(AppContext.BaseDirectory, relative)
+
+ let pngDimensions (path: string) : int * int =
+ let bytes = File.ReadAllBytes path
+ let signature = [| 0x89uy; 0x50uy; 0x4Euy; 0x47uy |]
+ for i in 0 .. 3 do
+ Assert.IsTrue(signature.[i] = bytes.[i], sprintf "%s is not a PNG" path)
+ let be32 (offset: int) =
+ (int bytes.[offset] <<< 24)
+ ||| (int bytes.[offset + 1] <<< 16)
+ ||| (int bytes.[offset + 2] <<< 8)
+ ||| int bytes.[offset + 3]
+ be32 16, be32 20
+
+ [<TestMethod>]
+ member _.Cc0GroundAtlasIsIntegerMultipleOfTheTileGrid () =
+ let width, height = pngDimensions (assetPath cc0TileAtlasRelativePath)
+ Assert.AreEqual<int>(cc0TileAtlasWidth, width)
+ Assert.AreEqual<int>(cc0TileAtlasHeight, height)
+ Assert.AreEqual<int>(0, width % Sim.tilePixels)
+ Assert.AreEqual<int>(0, height % Sim.tilePixels)
+ Assert.AreEqual<int>(4, width / Sim.tilePixels)
+ Assert.AreEqual<int>(2, Sim.tilePixels / cc0TileSourcePixels)
+ Assert.AreEqual<int>(0, Sim.tilePixels % cc0TileSourcePixels)
+
+ [<TestMethod>]
+ member _.Cc0CharacterAtlasIsIntegerMultipleOfTheCellGrid () =
+ let width, height = pngDimensions (assetPath cc0CharacterAtlasRelativePath)
+ Assert.AreEqual<int>(cc0CharacterAtlasWidth, width)
+ Assert.AreEqual<int>(cc0CharacterCellHeight, height)
+ Assert.AreEqual<int>(0, width % cc0CharacterCellWidth)
+ Assert.AreEqual<int>(0, height % cc0CharacterCellHeight)
+ Assert.AreEqual<int>(
+ cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount,
+ width / cc0CharacterCellWidth)
+ Assert.AreEqual<int>(2, cc0CharacterCellWidth / cc0CharacterSourceWidth)
+ Assert.AreEqual<int>(2, cc0CharacterCellHeight / cc0CharacterSourceHeight)
+
+ [<TestMethod>]
+ member _.AssetPackSwitchSelectsRealPackAndFallsBackToProcedural () =
+ let previous = Environment.GetEnvironmentVariable "LV_ASSET_PACK"
+ try
+ Environment.SetEnvironmentVariable("LV_ASSET_PACK", null)
+ Assert.IsFalse(useCc0Pack (), "unset switch must keep the procedural fallback")
+ Assert.IsTrue((selectedGroundAtlasRelativePath ()).IsNone, "fallback ground is procedural")
+ Assert.AreEqual<string>(characterAtlasRelativePath, selectedCharacterAtlasRelativePath ())
+
+ Environment.SetEnvironmentVariable("LV_ASSET_PACK", "cc0")
+ Assert.IsTrue(useCc0Pack (), "cc0 switch enables the real pack")
+ Assert.IsTrue(selectedGroundAtlasRelativePath () = Some cc0TileAtlasRelativePath)
+ Assert.AreEqual<string>(cc0CharacterAtlasRelativePath, selectedCharacterAtlasRelativePath ())
+
+ Environment.SetEnvironmentVariable("LV_ASSET_PACK", "kenney")
+ Assert.IsFalse(useCc0Pack (), "unknown pack value falls back to procedural")
+ finally
+ Environment.SetEnvironmentVariable("LV_ASSET_PACK", previous)
diff --git a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
index d4fa1f5..d32a21f 100644
--- a/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
+++ b/src/LivingVillage.Desktop.Tests/LivingVillage.Desktop.Tests.fsproj
@@ -15,6 +15,7 @@
<Compile Include="MapGenTests.fs" />
<Compile Include="PerformanceSummaryTests.fs" />
<Compile Include="M3BatchOutputTests.fs" />
+ <Compile Include="AssetPackTests.fs" />
<Compile Include="SampleTests.fs" />
</ItemGroup>
diff --git a/src/LivingVillage.Desktop/Assets/cc0-tiles.png b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
index 1e43bc8..204eba2 100644
--- a/src/LivingVillage.Desktop/Assets/cc0-tiles.png
+++ b/src/LivingVillage.Desktop/Assets/cc0-tiles.png
Binary files differ
diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs
index 4faa886..9a034aa 100644
--- a/src/LivingVillage.Desktop/VillageArt.fs
+++ b/src/LivingVillage.Desktop/VillageArt.fs
@@ -544,22 +544,46 @@ module VillageArt =
else
texture
+ /// Fallback switch for the real-pixel asset pack (P33 vertical slice). When
+ /// `LV_ASSET_PACK=cc0`, the sample village swaps its ground + villager atlases for
+ /// the provenanced CC0 packs; any other value keeps the procedural Jiangnan atlases.
let useCc0Pack () : bool =
System.Environment.GetEnvironmentVariable("LV_ASSET_PACK") = "cc0"
let cc0TileAtlasRelativePath = "Assets/cc0-tiles.png"
+ // Source ground tiles are 16x16, upscaled exactly 2x to one Sim.tilePixels cell.
+ let cc0TileSourcePixels = 16
let cc0TileAtlasWidth = Sim.tilePixels * 4
+ let cc0TileAtlasHeight = Sim.tilePixels
let cc0CharacterAtlasRelativePath = "Assets/cc0-characters.png"
+ // Source Puny band is 16x24, upscaled exactly 2x to a 32x48 cell; atlas packs
+ // 5 variants x 4 directions x 6 frames.
+ let cc0CharacterSourceWidth = 16
+ let cc0CharacterSourceHeight = 24
+ let cc0CharacterCellWidth = 32
+ let cc0CharacterCellHeight = 48
+ let cc0CharacterVariantCount = 5
+ let cc0CharacterDirectionCount = 4
+ let cc0CharacterFrameCount = 6
+ let cc0CharacterAtlasWidth =
+ cc0CharacterCellWidth * cc0CharacterVariantCount * cc0CharacterDirectionCount * cc0CharacterFrameCount
+
+ /// Ground atlas selection: `Some cc0-tiles.png` when the pack is on, else `None`
+ /// so the renderer keeps its procedural Jiangnan ground as the fallback.
+ let selectedGroundAtlasRelativePath () : string option =
+ if useCc0Pack () then Some cc0TileAtlasRelativePath else None
+
+ /// Character atlas selection: real CC0 Puny sprites when the pack is on, else the
+ /// procedural Jiangnan character atlas (fallback).
+ let selectedCharacterAtlasRelativePath () : string =
+ if useCc0Pack () then cc0CharacterAtlasRelativePath else characterAtlasRelativePath
let loadTextures (device: GraphicsDevice) : ArtTextures =
let cc0 =
- if useCc0Pack () then
- Some(loadTexture device cc0TileAtlasRelativePath cc0TileAtlasWidth Sim.tilePixels)
- else
- None
- let characterPath = if useCc0Pack () then cc0CharacterAtlasRelativePath else characterAtlasRelativePath
+ selectedGroundAtlasRelativePath ()
+ |> Option.map (fun path -> loadTexture device path cc0TileAtlasWidth cc0TileAtlasHeight)
{ TileAtlas = loadTexture device worldAtlasRelativePath worldAtlasWidth worldAtlasHeight
- CharacterAtlas = loadTexture device characterPath characterAtlasWidth characterAtlasHeight
+ CharacterAtlas = loadTexture device (selectedCharacterAtlasRelativePath ()) cc0CharacterAtlasWidth cc0CharacterCellHeight
InteriorAtlas = loadTexture device interiorAtlasRelativePath interiorAtlasWidth interiorAtlasHeight
Cc0TileAtlas = cc0 }