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

1,429 阅读11分钟

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

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

目录:

1.实现效果

三角形遮挡(z-buffering)
三角形遮挡(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
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.结束语

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

下一篇讲光线追踪,是图形渲染里比较难的一部分。

欢迎关注公众号: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