第一次接触GAMES101作业时,我和大多数同学一样被各种矩阵变换绕得头晕。直到把作业0到作业2完整实现后,才发现图形学的理论公式在代码里竟然如此优雅。下面就以三个典型作业为例,带你拆解那些课本上晦涩的概念如何变成可运行的代码。
齐次坐标这个看似抽象的概念,在Eigen库的Vector3f里就是简单的(x,y,1)。记得作业0里要实现点旋转后平移,我最初傻傻地分开计算,直到发现可以用矩阵连乘一次性完成所有变换。这里有个坑要注意:Eigen库的矩阵初始化顺序是行优先,和数学课本的列优先写法相反,第一次实现时我就栽在这个细节上。
绕Z轴旋转的矩阵看着简单,但实际编码时容易忽略角度转换。有次我直接用了角度值,结果三角形转得比陀螺还快——原来Eigen的三角函数需要弧度制。建议封装个角度转弧度的工具函数,后面投影矩阵还会用到。
cpp复制Eigen::Matrix4f get_model_matrix(float angle) {
float rad = angle * MY_PI / 180; // 这个MY_PI要自己定义
Eigen::Matrix4f rotate;
rotate << cos(rad), -sin(rad), 0, 0,
sin(rad), cos(rad), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
return rotate;
}
投影矩阵是作业1的难点,关键要理解从视锥体到立方体的两步转换。我花了三小时才搞明白:先用M_persp->ortho把棱台压成长方体,再用正交投影映射到NDC空间。特别注意near/far的符号处理,当时我写反了导致深度测试全乱。
cpp复制Eigen::Matrix4f get_projection_matrix(float fov, float aspect, float zNear, float zFar) {
float tan_half_fov = tan(fov * MY_PI / 360); // 注意是半角度
float top = zNear * tan_half_fov;
float right = top * aspect;
Eigen::Matrix4f persp_to_ortho = Eigen::Matrix4f::Zero();
persp_to_ortho(0,0) = zNear;
persp_to_ortho(1,1) = zNear; // 这里容易写错下标
persp_to_ortho(2,2) = zNear + zFar;
persp_to_ortho(3,2) = 1;
...
}
实现bounding box时,我最初遍历了整个屏幕像素,结果性能惨不忍睹。后来改用三角形顶点坐标的min/max确定范围,速度直接提升20倍。这里有个边界情况要注意:当三角形是斜着贴近屏幕边缘时,需要做clamp处理避免数组越界。
cpp复制void rasterize_triangle(const Triangle& t) {
// 计算包围盒边界
int x_min = floor(std::min({t.v[0].x(), t.v[1].x(), t.v[2].x()}));
int x_max = ceil(std::max({t.v[0].x(), t.v[1].x(), t.v[2].x()}));
// y轴同理...
for (int x = x_min; x <= x_max; x++) {
for (int y = y_min; y <= y_max; y++) {
// 重心坐标计算...
}
}
}
深度测试看着简单,但两个陷阱让我debug到凌晨:一是忘记初始化depth buffer为最大值,二是比较时用了错误的不等式方向。建议在代码里加个断言检查深度范围,我就遇到过因为透视矩阵算错导致深度值异常的情况。
作业框架里给的MSAA示例是4x采样,但实际测试发现2x采样就能获得80%的改善效果。我的实现方案是:先创建4倍的采样buffer,在光栅化时对每个子像素做单独判断,最后对四个采样点取平均。注意采样点位置会影响效果,我试过随机分布比网格分布抗锯齿更自然。
cpp复制// 2x2的MSAA实现示例
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
float sub_x = x + (i + 0.5) * 0.5;
float sub_y = y + (j + 0.5) * 0.5;
if (insideTriangle(sub_x, sub_y)) {
sample_count++;
// 计算子像素深度...
}
}
}
color = base_color * (sample_count / 4.0f);
当三角形较小时,传统MSAA会浪费大量计算在空白区域。后来我改用了自适应采样:先检测边缘像素,只对这些像素开启MSAA。在测试场景中,这种方案能让帧率提升3倍以上。不过要注意边缘检测的阈值设置,太敏感会导致画面闪烁。