1. 项目背景与目标
1.1 目标与范围
在图形学的实际动手实践中,目标是用 C++实现一个简易的软件光栅化渲染器,以便从理论到实现逐步观察渲染管线的各个环节。该文章聚焦如何在CPU端完成顶点处理、三角形光栅化以及像素着色的核心流程,帮助读者建立对渲染算法的直观理解。
本实战的核心范围包括顶点变换、三角形光栅化、深度测试以及基本的像素着色模型。通过逐步实现,可以从零到一地搭建一个可运行的渲染器雏形,并在此基础上扩展纹理与光照等特性。

// 一个极简的Vec3、Mat4与乘法示例
struct Vec3 { float x,y,z; };
struct Vec4 { float x,y,z,w; };struct Mat4 { float m[4][4]; };// 矩阵乘法:结果 r = M * v
Vec4 mul(const Mat4 &M, const Vec4 &v) {Vec4 r;r.x = M.m[0][0]*v.x + M.m[0][1]*v.y + M.m[0][2]*v.z + M.m[0][3]*v.w;r.y = M.m[1][0]*v.x + M.m[1][1]*v.y + M.m[1][2]*v.z + M.m[1][3]*v.w;r.z = M.m[2][0]*v.x + M.m[2][1]*v.y + M.m[2][2]*v.z + M.m[2][3]*v.w;r.w = M.m[3][0]*v.x + M.m[3][1]*v.y + M.m[3][2]*v.z + M.m[3][3]*v.w;return r;
}
2. 数学基础与坐标变换
2.1 坐标空间与MVP
要把3D顶点变换到屏幕像素,需要经历模型空间、世界空间、视图空间、裁剪空间、屏幕空间的一系列变换。MVP矩阵将这几步合并为一次乘法,便于在顶点阶段完成变换并尽量减少编程复杂度。
在渲染流水线中,顶点的每个分量都要经过齐次坐标变换,随后进行透视除法,得到归一化设备坐标(NDC),再映射到屏幕像素坐标。通过实现一个简化的管线,可以清晰观察每一步的数值影响与可调性。
// MVP示例:将模型顶点变换到屏幕坐标
Vec4 vertex = {dx, dy, dz, 1.0f};
Mat4 MVP = ...; // 组合矩阵:Proj * View * Model
Vec4 proj = mul(MVP, vertex);// 透视除法
float w = proj.w;
Vec3 ndc = {proj.x/w, proj.y/w, proj.z/w};// 屏幕坐标映射(假设分辨率 width x height)
int sx = int((ndc.x * 0.5f + 0.5f) * width);
int sy = int((1.0f - (ndc.y * 0.5f + 0.5f)) * height);
3. 核心光栅化算法实现
3.1 三角形光栅化的基本步骤
核心思想是通过对三角形的边界进行边函数判断来确定像素是否落在三角形内部。边函数方法的优点是能通过简单的线性代数实现快速测试,且易于与整数运算结合以提升性能。
实现的主要步骤通常包括:计算屏幕坐标下的顶点、构建边界盒子、遍历覆盖的像素、进行深度测试与着色。在每个像素处,结合重心坐标或深度插值可以实现更自然的着色结果。
// 边函数法的简化示例
struct Vec2{ float x,y; };
bool insideTriangle(int x, int y, const Vec2 &v0, const Vec2 &v1, const Vec2 &v2) {float w0 = (x - v1.x) * (v2.y - v1.y) - (y - v1.y) * (v2.x - v1.x);float w1 = (x - v2.x) * (v0.y - v2.y) - (y - v2.y) * (v0.x - v2.x);float w2 = (x - v0.x) * (v1.y - v0.y) - (y - v0.y) * (v1.x - v0.x);return (w0 >= 0 && w1 >= 0 && w2 >= 0);
}
// 简单的三角形光栅化核心循环
for (int y = minY; y <= maxY; ++y) {for (int x = minX; x <= maxX; ++x) {if (insideTriangle(x, y, v0, v1, v2)) {float w0, w1, w2; // 重心坐标(简化表示)// 计算深度float z = w0 * z0 + w1 * z1 + w2 * z2;if (z < zbuffer[x + y * width]) {zbuffer[x + y * width] = z;framebuffer[x + y * width] = shade; // 着色结果通过插值获得}}}
}
4. 渲染管线优化与实战要点
4.1 深度测试与像素着色
深度测试是软件光栅化中的关键步骤,用于正确处理遮挡关系。通过<Z缓冲区记录每个像素的最近深度,只有当新深度z小于缓冲区值时才写入帧缓冲区,确保可见性正确性。
像素着色阶段可以从<简单的常量颜色或纹理采样扩展到更真实的光照模型。为了保持学习的可控性,先实现一个简化的漫反射/镜面反射着色即可观察到光照对颜色的影响。
// 简易深度测试与着色伪代码
for each pixel:if (z < zbuffer[idx]) {zbuffer[idx] = z;framebuffer[idx] = shade; // 颜色来源可能是顶点颜色或纹理}
}
// 简单的着色函数(伪实现)
Color shade(const Vertex &v, const Vec3 &normal, const Vec3 &lightDir) {float ndotl = std::max(0.0f, dot(normal, lightDir));return Color(baseColor.r * ndotl, baseColor.g * ndotl, baseColor.b * ndotl);
}


