Press "Enter" to skip to content

图形渲染(2)Triangles and Z-buffering

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵

 

第一篇主要讲图形渲染中三种坐标系的变换,以及线条插值绘制,这是光栅化最基础的概念,第二篇继续深入,讲三角形内部插值和前后遮挡如何实现,即Z-buffering。另外补充一个重要的概念,MSAA(Multi- AA)(多重采样抗锯齿)。

 

目录:

1.实现效果

2.核心概念

2.1 Z-buffering原理

2.2三角形插值

2.3MSAA抗锯齿

3.核心代码说明

3.1判断像素点是否在三角形内

3.2 三角形内部插值 & MSAA实现

3.3编译运行

4.结束语

1.实现效果

三角形遮挡(z-buffering)

抗锯齿

看起来很简单的事情,要完全理解对初学者而言得下点功夫。

 

2.核心概念

 

先说Z-buffering,锯齿稍难点放后面讲

 

2.1 Z-buffering原理

多个物体同时出现在场景中,最终显示在屏幕上只有一个平面,一定是前面的挡住后面的,跟画画一样,先画后面的草地和山,再画前面的小树。

 

渲染过程有点区别的是,真正把一帧数据绘制到屏幕之前,这帧数据已经融合好了,已经剔除掉了后面的数据,最后一次性绘制到屏幕上。不是像画画一样一帧帧的往屏幕上叠加。

 

实现“剔除”,只保留最前面的场景的数据,就叫Z-buffering处理。假如屏幕分辨率为1280 * 720,那幺用同等大小内存来记录1280*720个点的Z坐标,不断更新。

 

在这里Z坐标正方向指向屏幕外,Z坐标越大,离屏幕越近。实际上不同的图形渲染API中,坐标系不一定相同。

遮挡案例

Android中,surfaceflinger将多个view最后合成一张图,就是做了类似Z-buffering的事。

 

2.2三角形插值

 

三角形有很多特性,非常适合作为渲染的最小单位,如:

各顶点/各边在同一平面上
内部的点很好定义
三角形内的顶点之间插值容易实现(质心插值)

当然,其他几何形状也可以作为最小单位,三角形比较常见。

Triangle Meshes

一个连续的形状如何绘制到离散的像素上呢?

 

基于线性插值,根据顶点的属性(如颜色、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.结束语

 

文中的大部分图片和公式引用自“闫令琪-现代图形学”

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注