不用opengl/vulkan直接写光栅器是为了掌控顶点变换、裁剪、透视除法、光栅化、片元着色全过程,理解viewport变换、w参与插值及z-buffer需1/w线性插值的原理。

为什么不用 OpenGL / Vulkan 直接写 rasterizer
因为你要控制每一步:顶点变换、裁剪、透视除法、光栅化、片元着色——GPU 驱动把这些全黑盒了。glDrawArrays 一调,中间发生了什么根本看不到。软渲染器不是为了替代 GPU,而是为了理解 viewport transform 怎么把 [-1,1]^3 映射到像素坐标,为什么 w 要参与插值,以及 Z-buffer 为什么必须用 1/w 线性插值。
最简光栅化循环里必须实现的三件事
一个三角形画出来,绕不开这三个环节,漏掉任意一个都会出现“有顶点没画面”或“颜色错乱”:
-
clip:在齐次裁剪空间(clip space)做[-w,w]判断,剔除完全在视锥外的三角形;对跨边界的三角形做 Sutherland-Hodgman 裁剪,输出 3–7 个顶点 -
perspective divide:对每个裁剪后顶点执行vec3{ x/w, y/w, z/w },得到 NDC 坐标;注意此时z是非线性的,不能直接用于深度测试 -
scanline rasterization:用整数步进的扫描线算法(别用浮点逐像素判断!),计算每行像素的左右边界,再对每个像素做重心坐标插值——这里必须传入原始w值,才能正确插值1/w和color/w
容易崩在 z-buffer 插值上
新手常把 z 当成线性量直接插值,结果远处物体闪烁、近处穿模。根本原因是透视投影下,z_screen = z_ndc = (z_eye * f + n) / (z_eye * (f - n)),它和 z_eye 不是线性关系。正确做法是:
- 顶点着色阶段输出
1.0f / v.w(即1.0 / z_eye),不是v.z - 光栅化时对
1/w做重心插值,得到每个像素的1/w_p - 深度测试用
w_p = 1.0f / (interpolated_1_w),再映射到[0,1]区间存入z_buffer
不这么做,z-fighting 在 1 米外就肉眼可见。
从 main() 开始跑通第一帧的关键路径
别一上来就写矩阵类或材质系统。先硬编码一个三角形,走通最小闭环:
- 定义三个
vec4顶点(含w=1.0),手动乘projection * view * model得到 clip 坐标 - 写死一个
std::vector<uint32_t> frame_buffer(W*H)</uint32_t>,初始化为黑色 - 实现
clip_triangle()→perspective_divide()→rasterize_triangle(),后者内部只做:计算 bounding box、遍历像素、用barycentric插值color和1/w、更新z_buffer - 用
stb_image_write.h把frame_buffer存成out.png,用看图软件打开验证
这个闭环跑通前,加任何“高级功能”都是干扰项。很多卡在第 2 步——clip 后顶点顺序错乱,导致叉积法向反了,背面剔除把唯一三角形干掉了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











