OBJ模型加载器

项目目标

实现一个简单的 OBJ 3D 模型加载器,支持解析 Wavefront OBJ 格式文件,提取顶点和面信息,并使用线框渲染显示模型。

实现过程

核心功能

  1. OBJ格式解析:支持顶点(v)和面(f)的解析
  2. 多种面格式支持f v1 v2 v3f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
  3. 自动缩放和居中:根据模型边界自动调整显示
  4. 线框渲染:使用 Bresenham 算法绘制三角形边缘

迭代历史

  • Iteration 1: 初始实现,一次性成功
    • 完整的OBJ解析器(支持多种格式)
    • 自动边界计算和缩放
    • 线框渲染输出
  • 验证: 量化验证通过
    • 图片尺寸:800x600 ✅
    • 立方体线框:1199像素 ✅
    • 位置居中:(400, 300) ✅

核心代码

1. OBJ格式解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int parseVertexIndex(const std::string& token) {
// 解析格式:v 或 v/vt 或 v/vt/vn 或 v//vn
size_t pos = token.find('/');
std::string indexStr = (pos == std::string::npos) ? token : token.substr(0, pos);
int index = std::stoi(indexStr);
// OBJ索引从1开始,转换为从0开始
return index - 1;
}

// 加载OBJ文件
bool load(const std::string& filename) {
std::ifstream file(filename);
std::string line;

while (std::getline(file, line)) {
std::istringstream iss(line);
std::string prefix;
iss >> prefix;

if (prefix == "v") {
// 顶点坐标
float x, y, z;
iss >> x >> y >> z;
vertices.push_back(Vec3(x, y, z));
}
else if (prefix == "f") {
// 三角形面
std::string v1, v2, v3;
iss >> v1 >> v2 >> v3;

int idx1 = parseVertexIndex(v1);
int idx2 = parseVertexIndex(v2);
int idx3 = parseVertexIndex(v3);

faces.push_back(Triangle(idx1, idx2, idx3));
}
}

return true;
}

2. 自动缩放和居中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 计算模型边界盒
Vec3 minBound(1e10, 1e10, 1e10);
Vec3 maxBound(-1e10, -1e10, -1e10);
for (const auto& v : model.vertices) {
minBound.x = std::min(minBound.x, v.x);
minBound.y = std::min(minBound.y, v.y);
minBound.z = std::min(minBound.z, v.z);
maxBound.x = std::max(maxBound.x, v.x);
maxBound.y = std::max(maxBound.y, v.y);
maxBound.z = std::max(maxBound.z, v.z);
}

// 计算缩放和平移
Vec3 center = (minBound + maxBound) * 0.5f;
Vec3 size = maxBound - minBound;
float scale = std::min(width, height) * 0.4f / std::max(std::max(size.x, size.y), size.z);

3. 正交投影

1
2
3
4
5
6
// 简单的正交投影(忽略Z轴)
auto project = [&](const Vec3& v) -> std::pair<int, int> {
float x = (v.x - center.x) * scale + width / 2;
float y = (v.y - center.y) * scale + height / 2;
return {(int)x, (int)y};
};

4. 线框渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 渲染所有三角形边缘
for (const auto& tri : model.faces) {
Vec3 v0 = model.vertices[tri.v0];
Vec3 v1 = model.vertices[tri.v1];
Vec3 v2 = model.vertices[tri.v2];

auto p0 = project(v0);
auto p1 = project(v1);
auto p2 = project(v2);

// 绘制三条边(使用 Bresenham 算法)
drawLine(p0.first, p0.second, p1.first, p1.second);
drawLine(p1.first, p1.second, p2.first, p2.second);
drawLine(p2.first, p2.second, p0.first, p0.second);
}

运行结果

OBJ模型加载器输出

验证结果

  • ✅ 图片尺寸:800x600
  • ✅ 白色背景比例:99.75%
  • ✅ 立方体线框像素数:1199
  • ✅ 立方体中心位置:(400, 300)
  • ✅ 立方体尺寸:240x240 像素

技术总结

学到的技术点

  1. OBJ格式理解

    • 顶点:v x y z
    • 面:f v1 v2 v3f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
    • 索引从1开始(需要转换)
  2. 边界盒计算

    • 遍历所有顶点求 min/max
    • 计算中心点和尺寸
    • 根据屏幕尺寸自动缩放
  3. 正交投影

    • 简单的2D投影(忽略Z轴)
    • 适用于线框渲染
    • 后续可升级为透视投影
  4. 线框渲染

    • 遍历所有三角形
    • 使用 Bresenham 算法绘制边
    • 轻量级的3D模型可视化方法

未来改进方向

  1. 法线和纹理坐标:支持 vnvt 解析
  2. 实体渲染:实现三角形填充(光栅化)
  3. 光照模型:添加 Phong/Blinn-Phong 光照
  4. 透视投影:实现真实的3D透视效果
  5. 复杂模型:支持加载外部OBJ文件(如Stanford Bunny)
  6. 旋转动画:添加交互式模型旋转

代码仓库

GitHub: daily-coding-practice/2026/02/02-25-OBJ-Model-Loader


完成时间: 2026-02-25 05:32
迭代次数: 1 次(一次成功)
编译器: g++ 12.3.1
总用时: ~2 分钟