games101 作業1及作業2分析及解決 詳解透視矩陣

dyccyber發表於2024-08-16

games101 作業1及作業2分析及解決

去年的時候把games101的課程以及作業完成,但是整個過程比較粗略,也藉助了不少外界的力量(doge),於是最近準備抽幾天集中再把作業(1-7)過一遍,常看常新嘛 環境配置直接用:https://github.com/roeas/GAMES101-Premake 之前是在虛擬機器上 這次用vs也方便一些
有時間也會研究一下大作業

作業一

程式碼分析

簡要分析一下整體的一個繪製流程
首先定義了繪製的視口 同時初始化了畫素緩衝區 與 深度緩衝區:

rst::rasterizer r(700, 700);
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h);
}

定義相機位置、三角形三個頂點在空間中的位置,三個頂點的索引順序,注意我這裡相機位置和頂點位置設定的都和原來不一樣,這裡後面再提:

Eigen::Vector3f eye_pos = {0, 0, 0};

std::vector<Eigen::Vector3f> pos{{2, 0, 12}, {0, 2, 12}, {-2, 0, 12}};

std::vector<Eigen::Vector3i> ind{{0, 1, 2}};

然後建立對應三角形的頂點緩衝區以及索引緩衝區:

auto pos_id = r.load_positions(pos);
auto ind_id = r.load_indices(ind);
rst::pos_buf_id rst::rasterizer::load_positions(const std::vector<Eigen::Vector3f> &positions)
{
    auto id = get_next_id();
    pos_buf.emplace(id, positions);

    return {id};
}

rst::ind_buf_id rst::rasterizer::load_indices(const std::vector<Eigen::Vector3i> &indices)
{
    auto id = get_next_id();
    ind_buf.emplace(id, indices);

    return {id};
}

然後就是設定模型、觀察以及透視矩陣,最後繪製
繪製部分:

void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{
    if (type != rst::Primitive::Triangle)
    {
        throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");
    }
    讀取對應的三角形的頂點以及索引資訊
    auto& buf = pos_buf[pos_buffer.pos_id];
    auto& ind = ind_buf[ind_buffer.ind_id];

    float f1 = (100 - 0.1) / 2.0;
    float f2 = (100 + 0.1) / 2.0;

    Eigen::Matrix4f mvp = projection * view * model;
    for (auto& i : ind)
    {
        Triangle t;
        轉換到螢幕空間
        Eigen::Vector4f v[] = {
                mvp * to_vec4(buf[i[0]], 1.0f),
                mvp * to_vec4(buf[i[1]], 1.0f),
                mvp * to_vec4(buf[i[2]], 1.0f)
        };
        透視除法
        for (auto& vec : v) {
            vec /= vec.w();
        }
        轉換到畫素空間
        for (auto & vert : v)
        {
            vert.x() = 0.5*width*(vert.x()+1.0);
            vert.y() = 0.5*height*(vert.y()+1.0);
            vert.z() = vert.z() * f1 + f2;
        }
        設定三角形的各個頂點
        for (int i = 0; i < 3; ++i)
        {
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
            t.setVertex(i, v[i].head<3>());
        }
        設定各個頂點的顏色
        t.setColor(0, 255.0,  0.0,  0.0);
        t.setColor(1, 0.0  ,255.0,  0.0);
        t.setColor(2, 0.0  ,  0.0,255.0);
        繪製 這裡是用線框形式繪製 使用的畫線演算法是Bresenham
        rasterize_wireframe(t);
    }
}

理論分析

img
貼一張大致的總結圖
重點分析透視矩陣的推導
這裡我介紹一下d3d12龍書的推導過程

把點投影到我們的投影平面上 利用相似我們可以得到的關係是(假設投影平面到我們攝像機的距離為1):

\[x^{'} = \frac{x}{z} \]

\[y^{'} = \frac{y}{z} \]

為了規範化歸一化 我們是要把投影平面\(x\in [-width/2,width/2]\)\(y\in [-height/2,height/2]\) 轉換到[-1,1]的這個平面上,要經歷變換:

\[x^{'} = \frac{x*2}{W} \]

\[y^{'} = \frac{y*2}{H} \]

如果我們使用fov 與 寬高比(r)來表示 則可以轉化為:

\[x^{'} = \frac{x}{(r* tan \frac{Fov}{2})} \]

\[y^{'} = \frac{y}{tan \frac{Fov}{2}} \]

可以看出我們其實是要對x,y進行兩步變換 我們可以第一步先進行歸一化變換
同時為了進行透視除法 我們需要儲存z座標,所以在第一步中我們要利用w分量來儲存z值,得到的變換過程如下:

\[\begin{bmatrix} \frac{1}{r\tan \frac{Fov}{2}} & 0 &0 &0 \\ 0& \frac{1}{\tan \frac{Fov}{2}}&0 &0 \\ 0& 0& A& B\\ 0& 0& 1&0 \end{bmatrix} \begin{bmatrix} x\\ y \\ z\\ 1 \end{bmatrix}= \begin{bmatrix} \frac{x}{(r* tan \frac{Fov}{2})}\\ \frac{y}{tan \frac{Fov}{2}} \\ Az+B\\ z \end{bmatrix}\]

之後第二步再進行透視除法:

\[\begin{bmatrix} \frac{x}{(rz* tan \frac{Fov}{2})}\\ \frac{y}{ztan \frac{Fov}{2}} \\ A+\frac{B}{z}\\ 1 \end{bmatrix}\]

最後我們還需要對z深度值進行歸一化操作 將z值轉換到0-1 在上述矩陣中我們可以直接利用 A與B來進行,令近平面上的點深度值為0,遠平面上的點深度值為1:
img

最終的透視矩陣:

\[\begin{bmatrix} \frac{1}{r\tan \frac{Fov}{2}} & 0 &0 &0 \\ 0& \frac{1}{\tan \frac{Fov}{2}}&0 &0 \\ 0& 0& \frac{f}{f-n} & \frac{-nf}{f-n} \\ 0& 0& 1& 0 \end{bmatrix}\]

實際解決

注意這裡我設定的相機以及頂點位置發生變化:

Eigen::Vector3f eye_pos = {0, 0, 0};

std::vector<Eigen::Vector3f> pos{{2, 0, 12}, {0, 2, 12}, {-2, 0, 12}};
r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

這樣設定就不會出現原來三角形倒置的問題了
因為按照原來的設定 z軸是朝外的 近平面原平面都設定為正 相當於相機朝向是z軸正方向 而三角形卻在z軸負半軸方向 這樣會產生問題

我覺得這樣改會比網上那個直接改透視矩陣要簡單一些

Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

    // TODO: Implement this function
    // Create the model matrix for rotating the triangle around the Z axis.
    // Then return it.
    float Cos = cos(rotation_angle / 180.f * MY_PI);
    float Sin = sin(rotation_angle / 180.f * MY_PI);
    model << Cos, -Sin, 0, 0,
        Sin, Cos, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1;
    return model;
}

Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar)
{
    // Students will implement this function

    Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
    float TanFov = tan((eye_fov / 2) / 180.f * MY_PI);

    projection << 1 / (aspect_ratio * TanFov), 0, 0, 0,
        0, 1 / TanFov, 0, 0,
        0, 0, zFar / zFar - zNear, -zFar * zNear / zFar - zNear,
        0, 0, 1, 0;

    return projection;
}

效果展示:
img

作業二

理論分析

整個程式碼框架和作業一變化不大
最大的差別就是將之前使用畫線演算法繪製線框 改為 實際填充畫素光柵化 即
draw函式的變化
整個繪製過程如下:
1.找到三角形圖元的boundingbox
2.判斷範圍內每個畫素塊是否在三角形內(使用叉積判斷)叉積得到的是一個三維向量 我們應該使用z座標來判斷(xy平面上做叉積得到的是一個垂直於xy平面的向量)如果三個叉積的結果同號 則說明點(畫素塊中心點)在三角形內
3.使用面積比例計算得到重心座標
4.使用重心座標插值得到三角形內畫素點的深度 這裡要進行透視校正插值 但是原始碼的方法是有錯誤的 應該使用三維空間中的正確深度值 而不是畫素空間被壓縮之後的深度值 詳細說明見:https://www.cnblogs.com/dyccyber/p/17873365.htmlhttps://zhuanlan.zhihu.com/p/509902950
5.進行深度測試

實際解決

覆蓋測試:
這裡我直接計算了z座標 沒有整體計算叉積

static bool insideTriangle(float x, float y, const Vector3f* _v)
{   
    // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2]
    Vector2f v0P(x - _v[0].x(), y - _v[0].y());
    Vector2f v1P(x - _v[1].x(), y - _v[1].y());
    Vector2f v2P(x - _v[2].x(), y - _v[2].y());
    Vector2f v0v1(_v[1].x() - _v[0].x(), _v[1].y() - _v[0].y());
    Vector2f v1v2(_v[2].x() - _v[1].x(), _v[2].y() - _v[1].y());
    Vector2f v2v0(_v[0].x() - _v[2].x(), _v[0].y() - _v[2].y());
    float Xp0 = v0v1.x() * v0P.y() - v0v1.y() * v0P.x();
    float Xp1 = v1v2.x() * v1P.y() - v1v2.y() * v1P.x();
    float Xp2 = v2v0.x() * v2P.y() - v2v0.y() * v2P.x();
    return (Xp0 < 0 && Xp1 < 0 && Xp2 < 0) || (Xp0 > 0 && Xp1 > 0 && Xp2 > 0);

}

螢幕空間光柵化:
這裡我使用了4xssaa進行抗鋸齒 要建立一個四倍的framebuffer與depthbuffer 依次對每個取樣點進行覆蓋與深度測試 然後求平均顏色

void rst::rasterizer::clear(rst::Buffers buff)
{
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
        std::fill(frame_sample.begin(), frame_sample.end(), Eigen::Vector3f{ 0, 0, 0 });
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
    frame_buf.resize(w * h);
    depth_buf.resize(w * h * 4);
    frame_sample.resize(w * h * 4);
    helper[0].x() = 0.25;
    helper[0].y() = 0.25;

    helper[1].x() = 0.75;
    helper[1].y() = 0.25;

    helper[2].x() = 0.25;
    helper[2].y() = 0.75;

    helper[3].x() = 0.75;
    helper[3].y() = 0.75;
 
}
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();
    int XMin = std::min(std::min(v[0].x(), v[1].x()), v[2].x());
    int XMax = std::max(std::max(v[0].x(), v[1].x()), v[2].x());
    int YMin = std::min(std::min(v[0].y(), v[1].y()), v[2].y());
    int YMax = std::max(std::max(v[0].y(), v[1].y()), v[2].y());
    for (int x = XMin; x < XMax; x++) {
        for (int y = YMin; y < YMax; y++) {
            int index = get_index(x, y) * 4;
            for (int i = 0; i < 4; i++) {
                if (insideTriangle(x + helper[i].x(), y + helper[i].y(), t.v)) {
                    auto [alpha, beta, gamma] = computeBarycentric2D(x + helper[i].x(), y + helper[i].y(), 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;
                    if (z_interpolated < depth_buf[index+i]) {
                        depth_buf[index+i] = z_interpolated;
                        frame_sample[index+i] = t.getColor();
                    }
                }
            }
            frame_buf[index / 4] = (frame_sample[index] + frame_sample[index + 1] + frame_sample[index + 2] + frame_sample[index + 3]) / 4;
            
        }
    }

}

結果展示:
img

相關文章