关于3D模型转换工具的总结

精华03/17326 浏览开发心得
嗒喵喵是没办法识别GLB文件的模型的,所以需要一个转换工具来把它转换成嗒喵喵能够使用的格式,下面是嗒喵喵制作工具的一个总结,有需要的可以直接复制给嗒喵喵。
这是分割线,下面的全部复制给嗒喵喵就行了!
# GLB → UrhoX MDL 转换管线完整指南
> 文档版本:v1.0 | 2026-03-17
>
> 转换工具路径:`scripts/tools/glb2mdl.py`
---
## 目录
1. [概述](#1-概述)
2. [使用方法](#2-使用方法)
3. [转换流程](#3-转换流程)
4. [UMD2 二进制格式规范](#4-umd2-二进制格式规范)
5. [UANI 动画格式规范](#5-uani-动画格式规范)
6. [避坑指南](#6-避坑指南)
7. [开发历程与逆向工程记录](#7-开发历程与逆向工程记录)
---
## 1. 概述
`glb2mdl.py` 是一个将 glTF Binary (.glb) 文件转换为 UrhoX 引擎可用资源包的 Python 工具。
**输入**:`.glb` 文件(glTF 2.0 Binary 格式)
**输出**:
| 文件类型 | 格式 | 说明 |
|---------|------|------|
| `.mdl` | UMD2 | 3D 模型(顶点、索引、骨骼、包围盒) |
| `.ani` | UANI | 骨骼动画(每个动画一个文件) |
| `.png` | PNG | 从 GLB 中提取的纹理 |
| `.xml` | XML | PBR 材质定义 |
**依赖**:
```
pip install pygltflib numpy
```
---
## 2. 使用方法
### 命令行
```bash
# 基本用法(输出到 Fox_UrhoX/ 目录)
python scripts/tools/glb2mdl.py Fox.glb
# 指定输出目录
python scripts/tools/glb2mdl.py Fox.glb -o assets/Models
# 指定输出目录和模型文件名
python scripts/tools/glb2mdl.py Fox.glb -o assets/Models -n Fox
# 批量转换
python scripts/tools/glb2mdl.py *.glb
```
### 参数
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `input` | 输入 .glb 文件路径(支持多个) | 必填 |
| `-o, --output` | 输出目录 | `<文件名>_UrhoX/` |
| `-n, --name` | 模型文件名(不含后缀) | 输入文件名 |
### 输出目录结构
```
output_dir/
├── Fox.mdl              # 模型文件
├── Animations/
│   ├── Walk.ani         # 动画文件
│   ├── Run.ani
│   └── Survey.ani
├── Textures/
│   └── Fox_tex0.png     # 提取的纹理
└── Materials/
    └── Material_0.xml   # PBR 材质
```
---
## 3. 转换流程
### 流程概览
```
GLB 文件
  │
  ├──[1] GLBParser 解析
  │    ├── 解码 data URI buffer
  │    ├── 递归遍历场景节点树
  │    ├── 检测蒙皮(skin)
  │    └── 解析 accessor 数据
  │
  ├──[2] 骨骼解析(如有 skin)
  │    ├── 读取 inverseBindMatrices
  │    ├── 计算绑定姿势世界矩阵
  │    ├── 推导各骨骼局部 TRS
  │    └── 建立 node→bone 映射
  │
  ├──[3] 网格解析
  │    ├── 提取 POS/NORMAL/UV/TANGENT
  │    ├── 提取 JOINTS/WEIGHTS(蒙皮)
  │    ├── 非蒙皮顶点应用世界变换
  │    ├── 自动生成缺失的法线/切线
  │    └── 读取索引
  │
  ├──[4] 纹理提取
  │    └── 从 bufferView 读取图片 → PNG
  │
  ├──[5] 材质生成
  │    └── PBR 参数 → XML 材质文件
  │
  ├──[6] MDLGenerator 构建 UMD2
  │    ├── 确定顶点元素列表
  │    ├── 逐顶点打包二进制
  │    ├── 构建索引缓冲
  │    ├── 序列化骨骼
  │    └── 写入包围盒和几何中心
  │
  └──[7] ANIGenerator 构建 UANI
       ├── 收集各骨骼的动画通道
       ├── 统一时间线采样
       └── 序列化关键帧
```
### 各步骤详解
#### Step 1:GLB 文件解析
- 使用 `pygltflib` 加载 GLB
- 调用 `convert_buffers(BufferFormat.DATAURI)` 将二进制 buffer 转为 data URI
- 从 data URI 中 base64 解码得到原始字节数据
- 通过 `_read_accessor()` 按 glTF 规范读取顶点属性
#### Step 2:骨骼树解析
- 从 `skin.inverseBindMatrices` 读取逆绑定矩阵(列主序 → 行主序转置)
- 绑定姿势世界矩阵 = `inv(inverseBindMatrix)`
- 各骨骼局部 TRS = `inv(parent_world) * my_world`
- 建立 `node_index → bone_index` 双向映射
#### Step 3:网格 Primitive 解析
**顶点属性提取**:
| glTF 属性 | 说明 | 处理 |
|-----------|------|------|
| `POSITION` | 顶点位置 | 非蒙皮时应用世界变换 |
| `NORMAL` | 法线 | 非蒙皮时用逆转置矩阵变换 |
| `TEXCOORD_0` | UV 坐标 | 可选翻转 V |
| `TANGENT` | 切线(vec4) | w 分量为 handedness |
| `JOINTS_0` | 骨骼索引 | uint16,最多 4 骨骼影响 |
| `WEIGHTS_0` | 骨骼权重 | 归一化处理 |
**自动补全**:
- 无法线 → 自动从三角面生成平滑法线
- 无切线 → 自动生成 MikkTSpace 标准切线(法线贴图必需)
#### Step 4-5:纹理和材质
- 纹理从 GLB 内嵌的 bufferView 提取,写为 PNG
- 材质根据 glTF 的 PBR Metallic-Roughness 参数生成 XML
- 自动选择 Technique(PBRDiff / PBRDiffAlpha / PBRNoTexture)
#### Step 6-7:输出文件生成
详见下方格式规范章节。
---
## 4. UMD2 二进制格式规范
> 以下规范通过逆向分析 UrhoX 引擎加载的 Fox.mdl(130698 字节)得出。
### 总体结构
```
┌──────────────────────────────────┐
│  Magic "UMD2"              (4B) │
├──────────────────────────────────┤
│  Vertex Buffer Section          │
├──────────────────────────────────┤
│  Index Buffer Section           │
├──────────────────────────────────┤
│  Geometry Section               │
├──────────────────────────────────┤
│  Morph Count = 0           (4B) │
├──────────────────────────────────┤
│  Skeleton Section               │
├──────────────────────────────────┤
│  Bounding Box              (24B)│
├──────────────────────────────────┤
│  Geometry Centers               │
└──────────────────────────────────┘
```
### 4.1 Magic(4 字节)
```
偏移 0x00: "UMD2" (ASCII, 4 bytes)
```
**注意**:UrhoX 使用 `UMD2` 魔数,**不是** Urho3D 开源版的 `UMDL`。格式有重大差异。
### 4.2 Vertex Buffers
```
vb_count       : uint32         # 顶点缓冲区数量(通常为 1)
Per VB:
  vertex_count : uint32         # 顶点数
  elem_count   : uint32         # 元素描述符数量
  elem_descs[] : uint32 × N     # packed 元素描述符
  morphRangeStart : uint32      # Morph 范围起始(通常为 0)
  morphRangeCount : uint32      # Morph 范围数量(通常为 0)
  vertex_data  : raw bytes      # vertex_count × stride 字节
```
#### 元素描述符打包格式
每个元素描述符是一个 `uint32`,按以下方式打包:
```
descriptor = (type & 0xFF) | ((semantic & 0xFF) << 8) | ((index & 0xFF) << 16)
```
**元素类型(type)**:
| 值 | 名称 | 字节大小 | 说明 |
|----|------|---------|------|
| 0 | INT | 4 | 整数 |
| 1 | FLOAT | 4 | 单精度浮点 |
| 2 | VEC2 | 8 | 2D 向量 |
| 3 | VEC3 | 12 | 3D 向量 |
| 4 | VEC4 | 16 | 4D 向量 |
| 5 | UBYTE4 | 4 | 4 个无符号字节 |
| 6 | UBYTE4N | 4 | 4 个归一化无符号字节 |
**元素语义(semantic)**:
| 值 | 名称 | 典型类型 | 说明 |
|----|------|---------|------|
| 0 | POSITION | VEC3 | 顶点位置 |
| 1 | NORMAL | VEC3 | 法线 |
| 2 | BINORMAL | VEC3 | 副法线 |
| 3 | TANGENT | VEC4 | 切线(w=handedness) |
| 4 | TEXCOORD | VEC2 | UV 坐标 |
| 5 | COLOR | UBYTE4N | 顶点颜色 |
| 6 | BLENDWEIGHTS | VEC4 | 骨骼权重 |
| 7 | BLENDINDICES | UBYTE4 | 骨骼索引 |
#### Fox.mdl 元素顺序(蒙皮模型标准顺序)
| 序号 | 语义 | 类型 | 字节 | descriptor 值 |
|------|------|------|------|--------------|
| 0 | POSITION | VEC3 | 12 | `0x00000003` |
| 1 | NORMAL | VEC3 | 12 | `0x00000103` |
| 2 | COLOR | UBYTE4N | 4 | `0x00000506` |
| 3 | TEXCOORD | VEC2 | 8 | `0x00000402` |
| 4 | TANGENT | VEC4 | 16 | `0x00000304` |
| 5 | BLENDWEIGHTS | VEC4 | 16 | `0x00000604` |
| 6 | BLENDINDICES | UBYTE4 | 4 | `0x00000705` |
**总 stride = 72 字节/顶点**
### 4.3 Index Buffers
```
ib_count       : uint32         # 索引缓冲区数量(通常为 1)
Per IB:
  index_count  : uint32         # 索引数
  index_size   : uint32         # 每个索引的字节数(2 或 4)
  index_data   : raw bytes      # index_count × index_size 字节
```
- 顶点数 ≤ 65535 → `index_size = 2`(uint16)
- 顶点数 > 65535 → `index_size = 4`(uint32)
### 4.4 Geometries
```
geom_count     : uint32         # 几何体数量
Per Geometry:
  bone_mapping_count : uint32   # 骨骼映射数量
  bone_mapping[]     : uint32 × N  # 全局骨骼索引
  lod_count          : uint32   # LOD 数量(通常为 1)
  Per LOD:
    distance   : float32        # LOD 距离(第一级为 0.0)
    primType   : uint32         # 图元类型(0 = TRIANGLE_LIST)
    vbRef      : uint32         # 引用的 VB 索引
    ibRef      : uint32         # 引用的 IB 索引
    drawStart  : uint32         # 索引起始位置
    drawCount  : uint32         # 索引数量
```
### 4.5 Morphs
```
morph_count    : uint32         # Morph 数量(当前固定为 0)
```
### 4.6 Skeleton
```
bone_count     : uint32         # 骨骼数量
Per Bone:
  name         : null-terminated string  # 骨骼名称(C 字符串 + 0x00)
  parent       : int32          # 父骨骼索引
  position     : float32 × 3   # 局部位置 (x, y, z)
  rotation     : float32 × 4   # 局部旋转四元数 (w, x, y, z)
  scale        : float32 × 3   # 局部缩放 (x, y, z)
  offsetMatrix : float32 × 12  # 逆绑定矩阵(Matrix3x4)
  collision_mask : uint8        # 碰撞掩码(通常为 0)
```
### 4.7 Bounding Box
```
min : float32 × 3              # AABB 最小点 (x, y, z)
max : float32 × 3              # AABB 最大点 (x, y, z)
```
### 4.8 Geometry Centers
```
# 注意:没有数量头!紧跟在 BBox 之后,每个 Geometry 一个 Vec3
center[] : float32 × 3 × geom_count
```
---
## 5. UANI 动画格式规范
### 总体结构
```
┌──────────────────────────────────┐
│  Magic "UANI"              (4B) │
│  Version                   (4B) │  = 2
│  Length (seconds)          (4B) │
│  Track Count               (4B) │
├──────────────────────────────────┤
│  Track[]                        │
├──────────────────────────────────┤
│  Trigger Count = 0         (4B) │
│  Metadata Count = 0        (4B) │
└──────────────────────────────────┘
```
### 轨道格式
```
Per Track:
  name       : urhox_string    # uint32 长度 + UTF-8 字节(注意:与骨骼名的 C 字符串不同!)
  interp     : uint8           # 插值模式:0=STEP, 1=LINEAR
  kf_count   : uint32          # 关键帧数量
  Per Keyframe:
    time        : float32      # 时间(秒)
    translation : float32 × 3  # 位置
    rotation    : float32 × 4  # 旋转四元数 (w, x, y, z)
    scale       : float32 × 3  # 缩放
```
### 字符串编码差异
| 场景 | 编码方式 | 说明 |
|------|---------|------|
| MDL 骨骼名 | null-terminated C 字符串 | `"bone_name\0"` |
| ANI 轨道名 | uint32 长度前缀 + UTF-8 | `[len][bytes...]` |
---
## 6. 避坑指南
### 坑 #1:UMD2 vs UMDL — 魔数和格式完全不同
**问题**:UrhoX 引擎使用 `UMD2` 格式,开源 Urho3D 使用 `UMDL`。两者顶点元素定义方式完全不同。
| | UMDL(开源 Urho3D) | UMD2(UrhoX) |
|---|---|---|
| 魔数 | `UMDL` | `UMD2` |
| 顶点元素 | `elementMask` 位掩码(1 个 uint32) | 显式描述符数组(N 个 uint32) |
| VB 额外字段 | 无 | `morphRangeStart` + `morphRangeCount` |
**教训**:不能用 Urho3D 的 AssetImporter 生成的 MDL,必须按 UMD2 格式重新序列化。
### 坑 #2:骨骼名序列化 — null-terminated,不是长度前缀
**问题**:MDL 中的骨骼名使用 C 风格 null-terminated 字符串(`"name\0"`),**不是** UrhoX 通常的 `uint32 长度 + UTF-8` 格式。
**而动画文件(UANI)中的轨道名使用的是 `uint32 长度 + UTF-8`。**
搞混这两种编码会导致文件大小不匹配、解析失败。
### 坑 #3:Root 骨骼的 parent 字段 — 自引用而非 -1
**问题**:根骨骼的 `parent` 字段在 Fox.mdl 中是 `0`(自引用),不是通常直觉中的 `-1`。
```python
# ✅ 正确
if parent < 0:
    parent = bi  # 自引用(bone[0].parent = 0)
# ❌ 错误
parent = -1  # 引擎可能不识别
```
### 坑 #4:bone_mapping 必须包含全部骨骼
**问题**:蒙皮模型的每个 Geometry 的 `bone_mapping` 必须列出骨骼表中的**全部骨骼**,即使某些骨骼没有被该 Geometry 的任何顶点引用。
```python
# ✅ 正确:映射全部骨骼
bone_mapping = list(range(num_bones))  # [0, 1, 2, ..., N-1]
# ❌ 错误:只映射有权重的骨骼
bone_mapping = [bi for bi in range(num_bones) if has_weights[bi]]
```
**原因**:引擎的动画系统需要完整的骨骼映射来正确驱动骨骼变换。缺少映射会导致动画播放时部分骨骼不动。
### 坑 #5:Geometry Centers 没有数量头
**问题**:文件末尾的 Geometry Centers 区域**没有 `count` 字段**,直接连续写入 `geom_count` 个 `Vec3`。
```
BBox:             min(Vec3) + max(Vec3)    ← 24 字节
Geometry Centers: Vec3 × geom_count        ← 无头部,直接跟在 BBox 后面
```
多写或少写一个 `uint32` 计数头会导致文件大小偏差 4 字节。
### 坑 #6:glTF 四元数顺序 xyzw → UrhoX 顺序 wxyz
**问题**:glTF 标准的四元数顺序是 `(x, y, z, w)`,UrhoX 使用 `(w, x, y, z)`。
```python
# glTF rotation: [x, y, z, w]
x, y, z, w = node.rotation
# ✅ 正确:转为 wxyz
rotation = [w, x, y, z]
```
忘记转换会导致模型/动画旋转完全错误。
### 坑 #7:逆绑定矩阵的列主序 → 行主序
**问题**:glTF 的矩阵按列主序存储(和 OpenGL 一致),需要转置为行主序。
```python
# glTF: 16 个 float,列主序
raw = read_accessor(ibm_index)  # shape: (N, 16)
# ✅ 正确:reshape 后转置
ibm = raw.reshape(-1, 4, 4).transpose(0, 2, 1)
# ❌ 错误:直接 reshape(不转置)
ibm = raw.reshape(-1, 4, 4)  # 矩阵方向错误!
```
### 坑 #8:Offset Matrix 是 Matrix3x4(12 个 float),不是 4x4
**问题**:UMD2 的骨骼 `offsetMatrix` 字段是 `Matrix3x4`(3 行 4 列 = 12 个 float,48 字节),不是完整的 4x4 矩阵。
```python
# 逆绑定矩阵是 4x4,取前 3 行写入 MDL
mat34 = ibm[bi][:3, :]  # 3×4
# 按行序写入 12 个 float:
# m00, m01, m02, m03,  m10, m11, m12, m13,  m20, m21, m22, m23
```
### 坑 #9:collision_mask 是 uint8,不是 uint32
**问题**:骨骼末尾的 `collision_mask` 是 **1 个字节**(`uint8`),不是 4 字节。写错字节数会导致后续所有骨骼的数据偏移错乱。
### 坑 #10:morphRangeStart/morphRangeCount 不能省略
**问题**:即使没有 Morph 数据,每个 VB 的元素描述符之后也必须写入 `morphRangeStart(u32) + morphRangeCount(u32)` 两个零值。这是 UMD2 与 UMDL 格式的重要差异之一。
### 坑 #11:非蒙皮网格的法线变换
**问题**:非蒙皮顶点需要手动应用世界变换。**法线不能直接乘以世界矩阵**,必须用逆转置矩阵。
```python
# 位置变换
pos = (homo @ gmat)[:, :3]
# ✅ 正确:法线用逆转置矩阵
nm = np.linalg.inv(gmat[:3, :3]).T
nrm = nrm @ nm.T
# ❌ 错误:直接乘世界矩阵
nrm = nrm @ gmat[:3, :3].T
```
不过对于均匀缩放的变换,两种方式结果相同。非均匀缩放时差异明显。
---
## 7. 开发历程与逆向工程记录
### 7.1 为什么需要自研转换工具
UrhoX 引擎使用了自有的 `UMD2` 格式,与开源 Urho3D 的 `UMDL` 不兼容。现有的 Urho3D 工具(如 AssetImporter)生成的 MDL 文件无法被 UrhoX 正确加载。因此需要从零逆向 UMD2 格式并实现转换器。
### 7.2 逆向方法
1. **获取已知可用的 MDL 文件**:使用引擎自带的 Fox.mdl(130698 字节)作为参考
2. **二进制分析**:用 hex dump 逐字节分析文件结构
3. **格式推断**:通过已知的顶点数、骨骼数等信息反推各字段含义
4. **输出对比**:生成同一模型的 MDL 文件,与原始文件逐字节比较
5. **逐步修正**:通过文件大小差异定位格式错误
### 7.3 关键突破点
| 发现 | 影响 |
|------|------|
| 魔数是 `UMD2` 不是 `UMDL` | 整个格式不同于开源版 |
| 元素用显式描述符而非 bitmask | 重写整个顶点缓冲区序列化 |
| 骨骼名用 C 字符串 | 修正文件大小偏差 |
| Root parent 自引用 | 修正骨骼层级关系 |
| bone_mapping 需要全部骨骼 | 修正动画播放异常 |
| collision_mask 是 1 字节 | 修正每个骨骼 3 字节的累计偏差 |
### 7.4 验证结果
最终转换器输出的 Fox.mdl 与引擎原始文件对比:
- **文件大小完全一致**:130698 字节
- **结构完全一致**:所有区段对齐
- **数据差异**:仅存在浮点精度差异(<0.001),由 glTF → NumPy → struct.pack 的浮点运算链路引入
---
*文档完成于 2026-03-17*
猜你想搜
taptap 制造3d模型转换
16
10
5