No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-03 16:11:10 +08:00
input 添加工具 2026-09-03 16:11:10 +08:00
output 添加工具 2026-09-03 16:11:10 +08:00
src 添加工具 2026-09-03 16:11:10 +08:00
tests 添加工具 2026-09-03 16:11:10 +08:00
.gitignore 添加工具 2026-09-03 16:11:10 +08:00
.python-version 添加工具 2026-09-03 16:11:10 +08:00
PROJECT_OVERVIEW.md 添加工具 2026-09-03 16:11:10 +08:00
pyproject.toml 添加工具 2026-09-03 16:11:10 +08:00
README.md 添加工具 2026-09-03 16:11:10 +08:00
requirements.txt 添加工具 2026-09-03 16:11:10 +08:00

AlphaContourMeshGenerator

批量 PNG → Alpha 轮廓 → Mesh 资源生成器(纯 Python无第三方依赖Cocos Creator 3.8.8 友好)

核心特性

  • 零第三方依赖 —— 仅依赖 Python 标准库(structzlibdataclassesjson…)
  • Marching Squares 外轮廓提取 —— 16 个 case 的查找表,含亚像素插值,鞍点采样处理模糊 case5/6/9/10
  • RDP 轮廓简化 —— 独立可配置 tolerance(默认 0 = 不简化,保留最大精度)
  • Ear-cutting Polygon Triangulation —— 通用多边形三角化(含凸/凹顶点处理、孔洞拓扑)
  • Alpha 阈值 + 像素中心偏移 —— 完全模拟 Cocos UV 采样约定
  • 统一资源重命名 —— image_0001.png / image_0001.contours.json / image_0001.mesh.json
  • 多轮廓 + 内部洞 —— 排序按面积、可控的 hole 分类point-in-polygon + 反向缠绕判断)
  • Y 轴说明 —— 图片坐标 y-downCocos 坐标 y-up统一处理保证 Mesh 与纹理 1:1 对应
  • 完整异常处理 —— 完全透明 / 完全不透明 / 单像素噪点 / 退化多边形都安全

📁 项目结构

AlphaContourMeshGenerator/
├── README.md                        # ← 你正在看的文档
├── requirements.txt                 # 纯 Python无需第三方包
├── src/
│   ├── __init__.py                  # Python 包入口:暴露核心 API
│   ├── __main__.py                  # CLI`python -m src`
│   ├── config.py                    # PipelineConfig + PathsConfig
│   ├── data_types.py                # Contour / ContourSet / MeshData dataclass
│   ├── image_processor.py           # PNG RGBA 解码(含 5 种 filter
│   ├── alpha_contour_extractor.py   # 串联 marching squares + RDP + hole 分类
│   ├── marching_squares.py          # Marching Squares 核心算法 + 轮廓追踪
│   ├── rdp_simplifier.py            # Ramer-Douglas-Peucker 简化算法
│   ├── polygon_triangulator.py      # Ear-clipping 三角化
│   ├── mesh_generator.py            # Mesh 生成器含洞处理、UV 计算)
│   ├── file_manager.py              # 文件重命名 + JSON 序列化
│   ├── json_serializers.py          # JSON 输出格式化
│   └── pipeline.py                  # 业务编排(组合各模块)
├── tests/
│   ├── __init__.py
│   ├── generate_test_images.py      # 生成测试 PNG圆形、星形、甜甜圈…
│   └── test_core.py                 # 单元测试unittest
├── input/                           # 存放待处理的 PNG
└── output/
    ├── images/                      # 重命名后的 PNG
    ├── contours/                    # 轮廓 JSON
    └── meshes/                      # Mesh JSON

🚀 快速开始

1. 准备环境

仅需 Python 3.8+

python --version

2. 生成测试图片(可选)

python tests/generate_test_images.py

会向 tests/fixtures/ 写入 6 张测试图:

  • circle_001.png —— 圆形(基础外轮廓)
  • star_001.png —— 五角星(凹多边形)
  • donut_001.png —— 圆环(外轮廓 + 内部空洞 hole
  • twin_circles_001.png —— 两个独立不连通区域
  • alpha_zero_001.png —— 完全透明(验证异常处理)
  • alpha_full_001.png —— 完全不透明(验证异常处理)

把它们复制或软链到 input/,例如:

mkdir -p input
cp tests/fixtures/*.png input/

3. 执行批量处理

python -m src --input input --output output

4. 运行单元测试

python -m unittest tests.test_core

5. CLI 选项

usage: python -m src [-h] [-i INPUT] [-o OUTPUT]
                    [--threshold THRESHOLD]
                    [--rdp-tolerance RDP_TOLERANCE]
                    [--no-simplify]
                    [--no-hole-classify]
                    [--min-contour-points N]
                    [--prefix PREFIX]
                    [--digits N]
                    [--start-index N]

示例:

# 阈值 100启用 RDPtolerance=0.4 像素)
python -m src --threshold 100 --rdp-tolerance 0.4

# 仅最大外轮廓,跳过 hole 分类
python -m src --no-hole-classify

# 自定义命名
python -m src --prefix icon --digits 3 --start-index 100

📦 输出文件结构

output/
├── images/
│   ├── image_0001.png          # ← 与原始 PNG 字节一致
│   ├── image_0002.png
│   └── …
├── contours/
│   ├── image_0001.json         # ← 外轮廓 + 孔洞坐标
│   ├── image_0002.json
│   └── …
└── meshes/
    ├── image_0001.json         # ← 顶点 / UV / 三角形索引(扁平数组)
    ├── image_0002.json
    └── …

轮廓 JSONcontours

{
  "name": "image_0001",
  "width": 256,
  "height": 256,
  "threshold": 128,
  "outer_contours": [
    [
      { "x": 12.5, "y": 20.3 },
      { "x": 18.7, "y": 15.2 },
      { "x": 30.1, "y": 14.8 }
    ]
  ],
  "hole_contours": [],
  "contours": [
    [
      { "x": 12.5, "y": 20.3 },
      { "x": 18.7, "y": 15.2 },
      { "x": 30.1, "y": 14.8 }
    ]
  ]
}

Mesh JSONmeshes

{
  "name": "image_0001",
  "width": 256,
  "height": 256,
  "vertex_count": 12,
  "triangle_count": 4,
  "vertices": [12.5, 20.3, 18.7, 15.2, 30.1, 14.8, ...],
  "uvs":      [0.0488, 0.0793, 0.0729, 0.0594, 0.1176, 0.0578, ...],
  "indices":  [0, 1, 2, 1, 3, 2, ...]
}

扁平数组格式对 Cocos Creator 3.8.8 的 Mesh 资源直接友好,便于二次导入脚本一次性 new Float32Array(...) / new Uint16Array(...)


🧠 算法逐步解释

1. Marching Squares 与 Case Table

把 alpha 通道视为离散 2D 标量场 A(x, y)。我们将图像分成 2×2cell(滑动窗口):

        (x, y) ───── (x+1, y)
           │            │
           │   cell     │
           │            │
        (x, y+1) ─── (x+1, y+1)

每个 cell 有 4 个角点,用每个角点的 alpha 与 threshold 比较得到一个 4-bit 的 configuration

bit 含义
0 (1) TL 不透明
1 (2) TR 不透明
2 (4) BR 不透明
3 (8) BL 不透明

因此 0..15 共 16 种 case。src/marching_squares.py 中的 _CELL_EDGES 表就是这张查找表。每个 case 给出被切分的边(TL_TR 上边,BL_BR 下边,TL_BL 左边,TR_BR 右边)。

鞍点saddle情况case 5 / 6 / 9 / 10 中 对角 两个角点不透明,外观上 cell 中心可能不透明,也可能透明。我们通过采样 cell 中心的 alpha4 个角点的均值)来消歧:

  • 中心不透明 → 两段斜线属于同一形状 → 上下边相连
  • 中心透明 → 两段斜线分属两个形状 → 左右边分别相连

2. 亚像素插值

每条边的两个端点 alpha 一高一低时,跨越边界的位置并非格点,而是位于两端之间某处。线性插值:

t   = (threshold - a1) / (a2 - a1)
pos = p1 + t * (p2 - p1)

t ∈ [0, 1] 内自动 clamp避免 a1 = a2 = threshold 时的 NaN。结果是 不是 整数像素坐标,而是浮点精度。这是为什么我们能得到亚像素级贴合。

如需使输出与 PS / Cocos UV采样点位于像素中心一致pixel_center_offset=True,相当于 cell 锚点从 (x, y) 移动到 (x - 0.5, y - 0.5)

3. 轮廓连接Trace to Loops

marching squares 输出的 segments 是孤立线段。我们用一个 O(N) 的哈希链(by_start 哈希桶)从任一段开始,按端点方向追溯,直到回到起点,得到一个闭合环。

tolerance(默认 1e-4)用于在浮点噪声下把端点归并到同一桶。对于真正亚像素精度提取的轮廓,这个值足够小,不会破坏形状。

4. RDPRamer-Douglas-Peucker

用 RDP 简化多边形顶点。算法:递归地删除那些与 相邻两点连线 的垂直距离 ≤ tolerance 的中间点。

我们使用的是 closed polygon 的 RDP首尾隐式相连迭代版本避免 Python 递归栈溢出5000+ 顶点的米老鼠轮廓也不会爆栈)。

关键设计:你可以 分别 控制轮廓精度(送入 Cocos 渲染)和碰撞体精度(送入 Collider),具体做法:

  • 提取阶段 simplify_contours=False(默认)→ 输出高密度顶点
  • 三角化前先用 rdp_simplifier.simplify(..., tolerance=0.5) 得到 mesh
  • 给 collider 再用 rdp_simplifier.simplify_open(..., tolerance=1.5) 简化
  • 两套数据分别保存到 output/meshes/ 和自定义的 output/colliders/

5. Polygon Triangulation

我们用 Ear-clippingsrc/polygon_triangulator.py

  1. 计算 polygon 缠绕方向CCW / CW
  2. 每步寻找一个 ear:连续的三个顶点 (P-1, P, P+1) 形成一个三角形:
    • 不含 polygon 内其他顶点
    • 朝外(凸顶点)
  3. 把这个三角形加入输出,从 polygon 移除 P。
  4. 重复直至剩余 < 3 顶点。

回退策略:若 strict 策略死循环(自交或退化输入),按"最平坦 ear"启发式继续,确保一定终止。

6. 整体协同

整条流水线(src/pipeline.py

PNG (RGBA)
   │
   ▼
ImageProcessor.read_alpha_grid
   │   ↳ 解 zlib 流,按 5 种 PNG filter 还原扫描线,提取 alpha
   ▼
M α  (H × W 整数矩阵)
   │
   ▼
ContourExtractor.extract
   │   ↳ Marching Squares → 16-case table → segments
   │   ↳ Trace segments → loops轮廓拼接
   │   ↳ RDP simplify (可选)
   │   ↳ signed_area 排序、按 point-in-polygon + 反向 winding 分类 hole
   ▼
ContourSet { 外轮廓…, 洞… }
   │
   ▼
MeshGenerator.generate
   │   ↳ 耳切三角化每个 outer含 hole 时把 hole 顶点反向追加)
   │   ↳ 写出扁平 vertices / uvs / indices
   ▼
MeshData → output/meshes/image_xxxx.json

7. 坐标轴说明 ⚠️

坐标系 原点 X 方向 Y 方向
图片PNG 左上角 → 右 ↓ 下
Cocos Creator UV 左下角 → 右 ↑ 上
Cocos Creator 顶点坐标(与纹理矩形组合) 由 Quad 决定 → 右 ↑ 上

我们当前提供:

  • vertices 的 y 保持为图片坐标y-down与 contours JSON 一致 —— 方便你做编辑/调试/编辑器内可视化
  • uvs 通过 v = y / height 计算;当 y_axis="y_down" 时 v 与图片一致,使用纹理采样器的 V 轴翻转(与 Cocos 默认 Quad + SpriteFrame 一致)。如果要发到任何 y-up 渲染管线,可设置 y_axis="y_up",生成器会自动 v = 1 - v

无论选择哪种,纹理与 mesh 是 1:1 对应 的(不会出现拉伸/翻转)。


⚠️ 异常处理

输入 行为
完全透明图 提取出空 ContourSet,仍写入空 contours JSON无 holes、无 outermesh.json 不写出,控制台提示 [skip] ...。统计计入 skipped
完全不透明图 同样无边界 → 同上
PNG 不是 8-bit NotImplementedError 提示
真正的 RGBA PALETTE 索引图 走 fallback 路径,把整张视为不透明(可自行扩展 image_processor.py
多连通区域(多个外轮廓) 排序后全部写到 outer_contours[]
存在内部空洞 hole hole_contours[],并被 mesh_generator 加入 mesh 形成岛中岛结构
简化后顶点数 < 3 丢弃该轮廓(min_contour_points 可调)
异常图 异常被 Pipeline.process_one 捕获,记入 stats.errors,不中断其它图

🧩 在 Cocos Creator 3.8.8 中使用

导入 mesh 数据

// resources/AlphaMeshes/image_0001.json
const json = JsonReader.read<MeshJson>(Resources.load(...));

// 顶点缓冲
const positions = new Float32Array(json.vertices);
const uvs = new Float32Array(json.uvs);
const indices = new Uint16Array(json.indices);

// 在 Cocos 里组建 Mesh
const mesh = new Mesh();
mesh.setVertices(gfx.VertexFormat.POSITION_UV, positions, uvs);
mesh.setIndices(indices, gfx.DrawMode.TRIANGLES);
mesh.setBounds(new geometry.AABB(...));

导入轮廓数据(用于 Collider / 编辑器绘制)

const contours = JsonReader.read<ContourJson>(Resources.load(...));
for (const ring of contours.outer_contours) {
    // ring : Array<{x, y}> —— 与 mesh `vertices` 完全一致
    // 把每个 ring 上传到 PolygonCollider2D
}

image_0001.png 一定要放在 image/image_0001.png 同级目录导入,这样 JsonReader 顺序能与 mesh image_0001.json 配对。


🔌 编程式 API

from src import (
    PipelineConfig,
    PathsConfig,
    Pipeline,
    ContourExtractor,
    MeshGenerator,
    ImageProcessor,
)

# 1. 最简用法
pipeline = Pipeline(PipelineConfig(), PathsConfig())
pipeline.run()

# 2. 自定义每张图:直接调子模块
ip = ImageProcessor()
w, h, grid = ip.read_alpha_grid("input/foo.png")

cs = ContourExtractor(PipelineConfig(alpha_threshold=128)).extract("foo", grid, w, h)
mesh = MeshGenerator(PipelineConfig()).generate(cs)

# 3. 手动序列化
from src.json_serializers import contour_set_to_dict, mesh_data_to_dict
print(contour_set_to_dict(cs))
print(mesh_data_to_dict(mesh))

📚 参考文献

  • Marching Squares —— Lorensen, Cline (1987), Marching Cubes: A High Resolution 3D Surface Construction Algorithm, SIGGRAPH
  • RDP —— Ramer (1972), Douglas, Peucker (1973)
  • Ear-clipping —— O'Rourke (1994), Computational Geometry in C
  • PNG (RFC 2083) —— W3C / ISO

📝 License

MIT