欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵
第一篇主要讲图形渲染中三种坐标系的变换,以及线条插值绘制,这是光栅化最基础的概念,第二篇继续深入,讲三角形内部插值和前后遮挡如何实现,即Z-buffering。另外补充一个重要的概念,MSAA(Multi-Sampling AA)(多重采样抗锯齿)。
目录:
1.实现效果
看起来很简单的事情,要完全理解对初学者而言得下点功夫。
2.核心概念
先说Z-buffering,锯齿稍难点放后面讲
2.1 Z-buffering原理
多个物体同时出现在场景中,最终显示在屏幕上只有一个平面,一定是前面的挡住后面的,跟画画一样,先画后面的草地和山,再画前面的小树。
渲染过程有点区别的是,真正把一帧数据绘制到屏幕之前,这帧数据已经融合好了,已经剔除掉了后面的数据,最后一次性绘制到屏幕上。不是像画画一样一帧帧的往屏幕上叠加。
实现“剔除”,只保留最前面的场景的数据,就叫Z-buffering处理。假如屏幕分辨率为1280 * 720,那么用同等大小内存来记录1280*720个点的Z坐标,不断更新。
在这里Z坐标正方向指向屏幕外,Z坐标越大,离屏幕越近。实际上不同的图形渲染API中,坐标系不一定相同。
Android中,surfaceflinger将多个view最后合成一张图,就是做了类似Z-buffering的事。
2.2三角形插值
三角形有很多特性,非常适合作为渲染的最小单位,如:
- 各顶点/各边在同一平面上
- 内部的点很好定义
- 三角形内的顶点之间插值容易实现(质心插值)
当然,其他几何形状也可以作为最小单位,三角形比较常见。
一个连续的形状如何绘制到离散的像素上呢?
基于线性插值,根据顶点的属性(如颜色、z坐标),可以对三角形内部的任一点做线性插值
这里不推导线性/双线性插值的算法实现,原理不复杂,推导略啰嗦。
需要注意的是:经过投影变换后对z坐标做插值,并不准确,因为投影变换是非线性的,求出来的三个系数不能直接用来做插值,需要校正。后面的代码实现是近似处理,以简化复杂度
参考透视矫正插值[1]
2.3MSAA抗锯齿
因为屏幕像素的离散化,斜边的几何线条很容易形成锯齿状,分辨率越低越明显.
如何处理锯齿呢? 两种思路:
- 一是图片采样的足够密,分辨率足够高
- 超采样 用大分辨率的方式处理图片,会占用更多的内存,消耗更多的处理资源,并不是经济的做法。超采样是一种很好的妥协。
超采样原理:假设每个像素是正方形,里面均匀的分布四个小点(实际上只有一个点),各占1/4权重,假如一个像素中有三个小点在图形中,那么这个红色的点只显示3/4的强度。
用颜色强度很好的处理了边界,模棱两可的像素点看起来颜色淡一点,模拟了边界的效果。
3.核心代码说明
工程的大部分代码和第一篇代码差不多,关注两段代码,判断点是否在三角形内、插值与超采样.
3.1判断像素点是否在三角形内
用叉乘的方式判断一个点是否在三角形内。线性代数的基础知识,原理很简单,不做赘述.
代码有很多版本,有的是把叉乘用代码实现,略显的啰嗦,下面的代码直接用向量库里叉乘的方法。
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
Vector3f P(x+0.5f,y+0.5f,1.0f);
const Vector3f& A = _v[0];
const Vector3f& B = _v[1];
const Vector3f& C = _v[2];
Vector3f AB = B - A;
Vector3f BC = C - B;
Vector3f CA = A - C;
Vector3f AP = P - A;
Vector3f BP = P - B;
Vector3f CP = P - C;
float z1 = AB.cross(AP).z();
float z2 = BC.cross(BP).z();
float z3 = CA.cross(CP).z();
return (z1 > 0 && z2 >0 && z3 > 0) || (z1 < 0 && z2 <0 && z3 < 0);
}
draw方法处理投影变换,第一篇已经详细讲过了,处理完点的坐标变换后,调用rasterize_triangle()方法进行三角形光栅化
void rst::rasterizer::draw(pos_buf_id pos_buffer, ind_buf_id ind_buffer, col_buf_id col_buffer, Primitive type)
{
....
...
rasterize_triangle(t);
}
3.2 三角形内部插值 & MSAA实现
三角形光栅化,最核心的代码片段,重要的地方都有注释
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
// 三角形三个点转成4维向量,增加了w维度
auto v = t.toVector4();
// 求包围三角形的最小四边形,这样仅处理四边形即可
// v是一个二维数组,有三个向量,每个向量的四分量为x y z w
float min_x = std::min(v[0][0], std::min(v[1][0], v[2][0]));
float max_x = std::max(v[0][0], std::max(v[1][0], v[2][0]));
float min_y = std::min(v[0][1], std::min(v[1][1], v[2][1]));
float max_y = std::max(v[0][1], std::max(v[1][1], v[2][1]));
min_x = (int)std::floor(min_x);
max_x = (int)std::ceil(max_x);
min_y = (int)std::floor(min_y);
max_y = (int)std::ceil(max_y);
// 控制是否打开MSAA抗锯齿
bool MSAA = false;
//MSAA 4X
if (MSAA) {
// 格子里的细分四个小点坐标
std::vector<Eigen::Vector2f> pos
{
{0.25,0.25},
{0.75,0.25},
{0.25,0.75},
{0.75,0.75},
};
for (int x = min_x; x <= max_x; x++) {
for (int y = min_y; y <= max_y; y++) {
// 记录最小深度
float minDepth = FLT_MAX;
// 四个小点中落入三角形中的点的个数
int count = 0;
// 对四个小点坐标进行判断
for (int i = 0; i < 4; i++) {
// 小点是否在三角形内
if (insideTriangle((float)x + pos[i][0], (float)y + pos[i][1], t.v)) {
// 如果在,对深度z进行插值
auto tup = computeBarycentric2D((float)x + pos[i][0], (float)y + pos[i][1], t.v);
float alpha;
float beta;
float gamma;
// std::tie表示打散tup到 alpha beta gamma三个分量
std::tie(alpha, beta, gamma) = tup;
// reciprocal 倒数
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
// 按照三角形三个点的权重,对当前点插值,求出z值,注意,这里的reciprocal用的有点莫名其妙,先不用管
// 而且alpha beta gamma用起来是需要矫正的
// 此处留个疑问:为什么不能在投影变换时,求出每个点的z坐标映射值呢?
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// 求出当前点中四个小点对应的深度,以代表当前点的z值,用来和其他点的z做对比
minDepth = std::min(minDepth, z_interpolated);
// 包含一个点count +1
count++;
}
}
if (count != 0) {
if (depth_buf[get_index(x, y)] > minDepth) {
// 简单的对color/4也可以,处理的比较粗糙。
// 注意getColor其实就只用了一个值,三角形三个点的颜色相同
// 这里还考虑了当前缓冲里面存贮的颜色值
Vector3f color = t.getColor()*count/4 + (4-count)*frame_buf[get_index(x,y)]/4;
Vector3f point(3);
point << (float)x, (float)y, minDepth;
// 替换深度
depth_buf[get_index(x, y)] = minDepth;
// 修改颜色
set_pixel(point, color);
}
}
}
}
}
else {
// 不考虑MSAA抗锯齿,就比较简单了,不做赘述
for (int x = min_x; x <= max_x; x++) {
for (int y = min_y; y <= max_y; y++) {
if (insideTriangle((float)x + 0.5, (float)y + 0.5, t.v)) {
auto tup = computeBarycentric2D((float)x + 0.5, (float)y + 0.5, t.v);
float alpha;
float beta;
float gamma;
std::tie(alpha, beta, gamma) = tup;
float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
if (depth_buf[get_index(x, y)] > z_interpolated) {
Vector3f color = t.getColor();
Vector3f point(3);
point << (float)x, (float)y, z_interpolated;
depth_buf[get_index(x, y)] = z_interpolated;
set_pixel(point, color);
}
}
}
}
}
}
3.3编译运行
cd build
make -j4
./Rasterizer image01.png
// filename = std::string(argv[1]);可以读取到命令行的参数,记录要保存的图片名
//argv[i]第一个参数为./Rasterizer
// 第二个参数为 image01.png,保存图片,不带参数渲染到屏幕上
4.结束语
文中的大部分图片和公式引用自“闫令琪-现代图形学”
下一篇讲光线追踪,是图形渲染里比较难的一部分。
欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵
[2] [3] [4] [5] [6]
参考资料
[1]透视矫正插值和图形渲染管线总结: https://zhuanlan.zhihu.com/p/144331875
[2]笔记-光栅化与着色: https://zhuanlan.zhihu.com/p/140779399
[3]讨论区 › 作业3 关于深度值问题自己踩的坑和一些想法: http://games-cn.org/forums/topic/zuoye3-guanyushenduzhiwentizijicaidekengheyixiexiangfa/
[4]games101-2 比较好的实现: https://blog.csdn.net/qq_36242312/article/details/105758619
[5]代码实现—1: https://github.com/kingiluob/Games101/blob/master/Assignment2/rasterizer.cpp
[6]代码实现—2: https://github.com/Quanwei1992/GAMES101/blob/master/02/rasterizer.cpp