GAMES101 PA2

基础

Inside Triangle

直接利用计算几何中求凸包时所用到的 In-Trangle Test 以及 To-Left Test 即可,此处已知点的坐标,使用行列式计算三角形的有向面积。

代码
static bool toLeft(std::pair<float, float>& p, std::pair<float, float>& q, std::pair<float, float>& s) {
    // Area2 determinant
    return 
          p.first * q.second - p.second * q.first
        + q.first * s.second - q.second * s.first
        + s.first * p.second - s.second * p.first > 0.f;
}

static bool insideTriangle(int x, int y, const Vector3f* _v)
{   
    std::pair<float, float> p, q, r, s;
    p = {_v[0].x(), _v[0].y()}; q = { _v[1].x(), _v[1].y()}, r = { _v[2].x(), _v[2].y() }, s = {x, y};

    bool pq_left = toLeft(p, q, s);
    bool qr_left = toLeft(q, r, s);
    bool rp_left = toLeft(r, p, s);

    return (pq_left == qr_left) && (pq_left == rp_left);
}

Rasterize Triangle

整体的算法是:

  • 首先计算出三角形的包围盒(BB);
  • 然后枚举所有 BB 内的像素(下标取整):
    • 判断其中心(下标 +   0.5 +\ 0.5 + 0.5)是否在三角形内部:
      • 如果在,那就通过透视矫正插值计算出 z_interpolated,再与当前位置的 depth_buf 进行比较(注意此处框架的 z 全部是正值,而非负值,越加靠近摄像机就越小):
        • 如果离摄像机更近,则更新当前位置的 frame_bufdepth_buf
代码
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    int x_min = floor(std::min(v[0].x(), std::min(v[1].x(), v[2].x())));
    int x_max = ceil(std::max(v[0].x(), std::max(v[1].x(), v[2].x())));
    int y_min = floor(std::min(v[0].y(), std::min(v[1].y(), v[2].y())));
    int y_max = ceil(std::max(v[0].y(), std::max(v[1].y(), v[2].y())));

    for (int i = x_min; i < x_max; ++i) {
        for (int j = y_min; j < y_max; ++j) {
            if (insideTriangle(i + 0.5f, j + 0.5f, t.v)) {
                float alpha, beta, gamma;
                auto tu = computeBarycentric2D(i, j, t.v);
                std::tie(alpha, beta, gamma) = tu;
                
                // 一个透视矫正的深度插值,看看推导!
                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; 

                int ind = get_index(i, j);
                if (z_interpolated < depth_buf[ind]) {
                    Eigen::Vector3f point(i, j, 1.0);  // set point
                    set_pixel(point, t.getColor());   // set color
                    depth_buf[ind] = z_interpolated; // update depth buffer
                }
            }
        }
    }
}
效果

在这里插入图片描述

一点笔记

在计算 z_interpolated 时,使用了如下代码:

auto[alpha, beta, gamma] = computeBarycentric2D(i, j, t.v);
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; 

此处为啥这么写?

  • 因为要求在透视投影之前的各点的深度,来实现 Z-Buffer

  • 而这是一个透视矫正的深度插值

    三角形重心在做透视投影的前后都满足以下的插值公式:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-czRShsiH-1650941860346)(images/3.png)]

    但经过推导(可见 Reference 第一项),使用透视投影后的插值系数也能够计算出透视投影之前的重心插值

在这里插入图片描述

且被插值的属性 I I I 可以由下式计算得到:

在这里插入图片描述

此处插值的是深度,替换 I I I 为深度,而 Z A , Z B , Z C Z_A, Z_B, Z_C ZA,ZB,ZC 等均为透视投影前的深度,根据透视变换矩阵可知:
在这里插入图片描述

储存在 Vector4f 的第四维,所以在计算 Z Z Z 以及调用 Z A , Z B , Z C Z_A, Z_B, Z_C ZA,ZB,ZC 时使用的都是 w

  • 可是为什么这么写?既然都知道了 I A , I B , I C I_A, I_B,I_C IA,IB,IC,直接用 α , β , γ \alpha, \beta, \gamma α,β,γ 带入插值公式去算 I I I 不香嘛?

    事实上 α , β , γ \alpha, \beta, \gamma α,β,γ 并不好算,由计算公式:

在这里插入图片描述
可知,需要透视投影前三角形顶点的另外两维数据,即 X , Y X,Y X,Y,可事实上由于使用的是 Vector4f,在经过了 MVP变换 后只留下了透视投影之前的 Z A , Z B , Z C Z_A,Z_B,Z_C ZA,ZB,ZC,无法直接使用该公式计算 α , β , γ \alpha, \beta, \gamma α,β,γ,除非做一次逆变换;

所以只能换个方法,通过透视投影后的三角形的三个顶点的 X ′ , Y ′ X', Y' X,Y,计算出 α ′ , β ′ , γ ′ \alpha', \beta', \gamma' α,β,γ,再借以 Vector4f 中存储的透视投影前的 Z A , Z B , Z C Z_A, Z_B, Z_C ZA,ZB,ZC,计算出 Z Z Z,再得到 α , β , γ \alpha, \beta, \gamma α,β,γ

在这里插入图片描述

提高

可以看到在蓝色三角形的边缘存在一些走样( A l i a s i n g Aliasing Aliasing),可以通过一些反走样( A n t i − A l i a s i n g Anti-Aliasing AntiAliasing)的算法来解决;

  • 在频域上,做的就是滤波,滤掉高频,使得频率满足奈奎斯特采样定理;

  • 由数字信号处理可知,而频域上的乘法就是时域上的卷积,所以表现在图像上,就是对像素做一些平均(卷积操作),使得边缘更加模糊,看起来不那么锐利。在这里插入图片描述

4xSSAA

  • 对于三角形的包围盒内部的每一个像素,遍历他的四个 child pixel
    • 如果在三角形内,则更新 child pixel 对应的 frame_buf_4xSSAAdepth_buf_4xSSAA
  • 由于有多个三角形存在,不能立刻更新 frame_buf,只能在所有的三角形处理完毕后,再进行对每个像素的平均;
    • draw 函数的最后,遍历所有像素,每个像素的 frame_buf 等于四个 child pixel 所对应的 frame_buf_4xSSAA 的和,再求平均。
代码
    for (int i = x_min; i < x_max; ++i) {
        for (int j = y_min; j < y_max; ++j) {
			if (_4xSSAA) {
                float dx[] = { 0.25f, 0.75f, 0.75f, 0.25f }, dy[] = { 0.25f, 0.25f, 0.75f, 0.75f };
                for (int k = 0; k < 4; k++)
                {
                    float x = i + dx[k], y = j + dy[k];
                    if (insideTriangle(x, y, t.v)) {
                        auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);  // alpha', beta', gamma'
                        float w_reciprocal = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());  // 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;
                        
                        int ind = get_index(i, j);
                        if (z_interpolated < depth_buf_4xSSAA[ind][k]) {
                            // u cannot update the frame buffer here, because 2 triangles exist
                            frame_buf_4xSSAA[ind][k] = t.getColor();
                            depth_buf_4xSSAA[ind][k] = z_interpolated; // update depth buffer of 4xSSAA
                        }
                    }
                }
            }
            else {  /* trivial */ }
    }
}
效果

可以看到蓝色三角形的走样不再那么明显:

在这里插入图片描述

4xMSAA

  • 对于三角形的包围盒内部的每一个像素,遍历他的四个 child pixel
    • 如果在三角形内,则覆盖率 + 25 % +25\% +25%,该像素的颜色由三角形的颜色以及该像素的覆盖率所决定;
  • 事实上由于多个三角形存在,需要考虑混合颜色,或者插值。

Reference

[数学] 重心坐标插值与透视校正插值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值