- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| input | ||
| output | ||
| src | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| PROJECT_OVERVIEW.md | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
AlphaContourMeshGenerator
批量 PNG → Alpha 轮廓 → Mesh 资源生成器(纯 Python,无第三方依赖,Cocos Creator 3.8.8 友好)
✨ 核心特性
- ✅ 零第三方依赖 —— 仅依赖 Python 标准库(
struct、zlib、dataclasses、json…) - ✅ Marching Squares 外轮廓提取 —— 16 个 case 的查找表,含亚像素插值,鞍点采样处理模糊 case(5/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-down,Cocos 坐标 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,启用 RDP(tolerance=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
└── …
轮廓 JSON(contours)
{
"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 JSON(meshes)
{
"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×2 的 cell(滑动窗口):
(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 中心的 alpha(4 个角点的均值)来消歧:
- 中心不透明 → 两段斜线属于同一形状 → 上下边相连
- 中心透明 → 两段斜线分属两个形状 → 左右边分别相连
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. RDP(Ramer-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-clipping(src/polygon_triangulator.py):
- 计算 polygon 缠绕方向(CCW / CW)。
- 每步寻找一个 ear:连续的三个顶点 (P-1, P, P+1) 形成一个三角形:
- 不含 polygon 内其他顶点
- 朝外(凸顶点)
- 把这个三角形加入输出,从 polygon 移除 P。
- 重复直至剩余 < 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、无 outer),mesh.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顺序能与 meshimage_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