用C++手写一个3D立方体投影:从模型变换到视口变换的完整流程
·
用C++手写一个3D立方体投影:从模型变换到视口变换的完整流程
在计算机图形学中,3D物体的渲染过程可以看作是一系列数学变换的串联。本文将带你从零开始,仅使用C++标准库和基础数学知识,实现一个完整的3D立方体投影流程。我们将重点探讨如何通过矩阵变换,将一个3D立方体模型逐步转换为2D屏幕上的投影图像。
1. 理解3D图形渲染管线
3D图形渲染管线是将3D模型转换为2D图像的一系列处理步骤。对于我们的立方体投影项目,主要涉及以下几个关键阶段:
- 模型变换 :将物体从模型空间转换到世界空间
- 视图变换 :将物体从世界空间转换到相机空间
- 投影变换 :将物体从3D相机空间投影到2D标准化设备坐标
- 视口变换 :将标准化设备坐标映射到屏幕像素坐标
每个阶段都对应一个4x4的变换矩阵,这些矩阵将按顺序相乘,最终形成一个完整的模型-视图-投影矩阵(MVP矩阵)。
2. 实现基础矩阵运算
在开始实现具体变换前,我们需要先构建矩阵运算的基础设施。这里我们定义一个简单的4x4矩阵类:
class Matrix {
public:
std::vector<std::vector<float>> m;
Matrix(int rows, int cols) : m(rows, std::vector<float>(cols, 0)) {}
static Matrix identity(int size) {
Matrix result(size, size);
for (int i = 0; i < size; ++i) {
result.m[i][i] = 1.0f;
}
return result;
}
Matrix operator*(const Matrix& other) const {
Matrix result(4, 4);
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
for (int k = 0; k < 4; ++k) {
result.m[i][j] += m[i][k] * other.m[k][j];
}
}
}
return result;
}
Vec3f operator*(const Vec3f& v) const {
Vec3f result;
result.x = m[0][0] * v.x + m[0][1] * v.y + m[0][2] * v.z + m[0][3];
result.y = m[1][0] * v.x + m[1][1] * v.y + m[1][2] * v.z + m[1][3];
result.z = m[2][0] * v.x + m[2][1] * v.y + m[2][2] * v.z + m[2][3];
return result;
}
};
3. 实现模型变换
模型变换用于将物体从模型空间转换到世界空间。常见的模型变换包括:
- 平移变换
- 缩放变换
- 旋转变换
3.1 平移变换
平移变换将物体沿指定方向移动一定距离:
Matrix translation(Vec3f v) {
Matrix Tr = Matrix::identity(4);
Tr.m[0][3] = v.x;
Tr.m[1][3] = v.y;
Tr.m[2][3] = v.z;
return Tr;
}
3.2 缩放变换
缩放变换可以改变物体的大小:
Matrix scale(float factorX, float factorY, float factorZ) {
Matrix Z = Matrix::identity(4);
Z.m[0][0] = factorX;
Z.m[1][1] = factorY;
Z.m[2][2] = factorZ;
return Z;
}
3.3 旋转变换
旋转变换可以让物体绕某个轴旋转:
Matrix rotation_x(float angle) {
angle = angle * PI / 180;
float sinangle = sin(angle);
float cosangle = cos(angle);
Matrix R = Matrix::identity(4);
R.m[1][1] = R.m[2][2] = cosangle;
R.m[1][2] = -sinangle;
R.m[2][1] = sinangle;
return R;
}
4. 实现视图变换
视图变换将物体从世界空间转换到相机空间。我们需要实现一个 lookat 函数,它接收相机位置、观察点和上向量三个参数:
Matrix lookat(Vec3f eye, Vec3f center, Vec3f up) {
Vec3f z = (eye - center).normalize();
Vec3f x = (up.cross(z)).normalize();
Vec3f y = (z.cross(x)).normalize();
Matrix res = Matrix::identity(4);
for (int i = 0; i < 3; i++) {
res.m[0][i] = x[i];
res.m[1][i] = y[i];
res.m[2][i] = z[i];
res.m[i][3] = -center[i];
}
return res;
}
5. 实现投影变换
投影变换将3D场景投影到2D平面上。我们将实现透视投影:
Matrix projection(Vec3f eye, Vec3f center) {
Matrix m = Matrix::identity(4);
m.m[3][2] = -1.f / (eye - center).norm();
return m;
}
透视投影的关键在于将z坐标的倒数作为透视因子,这会产生近大远小的效果。
6. 实现视口变换
视口变换将标准化设备坐标映射到屏幕像素坐标:
Matrix viewport(int x, int y, int w, int h, int depth) {
Matrix m = Matrix::identity(4);
m.m[0][3] = x + w / 2.f;
m.m[1][3] = y + h / 2.f;
m.m[2][3] = depth / 2.f;
m.m[0][0] = w / 2.f;
m.m[1][1] = h / 2.f;
m.m[2][2] = depth / 2.f;
return m;
}
7. 整合完整渲染流程
现在我们可以将所有变换整合起来,形成一个完整的渲染管线:
int main() {
// 初始化模型和图像
Model* model = new Model("cube.obj");
PNGImage image(width, height, PNGImage::RGBA);
// 定义变换矩阵
Matrix Model = translation(Vec3f(0, 0, 0)) * scale(0.5, 0.5, 0.5);
Matrix View = lookat(Vec3f(0, 0, 5), Vec3f(0, 0, 0), Vec3f(0, 1, 0));
Matrix Projection = projection(Vec3f(0, 0, 5), Vec3f(0, 0, 0));
Matrix ViewPort = viewport(width/4, height/4, width/2, height/2, 255);
// 组合MVP矩阵
Matrix MVP = ViewPort * Projection * View * Model;
// 渲染模型
for (int i = 0; i < model->nfaces(); i++) {
std::vector<int> face = model->face(i);
for (int j = 0; j < (int)face.size(); j++) {
Vec3f v0 = model->vert(face[j]);
Vec3f v1 = model->vert(face[(j + 1) % face.size()]);
// 应用变换
Vec3f p0 = MVP * v0;
Vec3f p1 = MVP * v1;
// 绘制线段
line(p0, p1, image, green);
}
}
// 保存图像
image.write_png_file("output.png");
delete model;
return 0;
}
8. 优化与扩展
虽然我们已经实现了基本功能,但还可以进行一些优化和扩展:
- 背面剔除 :通过计算面法线可以剔除背对相机的面
- 深度缓冲 :实现简单的Z缓冲算法来处理深度信息
- 光照模型 :添加简单的光照计算
- 纹理映射 :为立方体添加纹理
提示:在实际项目中,矩阵运算通常会使用SIMD指令或专门的数学库进行优化,以提高性能。
通过这个项目,我们不仅理解了3D图形渲染的基本原理,还亲手实现了一个简化版的渲染管线。这种底层实现虽然不如现代图形API高效,但对于理解计算机图形学的基本概念非常有帮助。
更多推荐


所有评论(0)