[GAMES101/CG] 作業1-透視投影 框架解析手記


Abstract

如果你對投影幾何沒什么概念,可以移步 這里

History:

此文章由於存在過一些重大邏輯錯誤,經過了兩次修改,現在仍在勘誤中


我們口頭模擬一下作業1的繪制過程:

main函數

int main(int argc, const char** argv){
    float angle = 0;
    bool command_line = false;
    std::string filename = "output.png";

    if (argc >= 3) {
        command_line = true;
        angle = std::stof(argv[2]); // -r by default
        if (argc == 4) {
            filename = std::string(argv[3]);
        }
        else
            return 0;
    }

    // 初始化光柵化器的實例r,且定義顯示大小為700*700
    rst::rasterizer r(700, 700);

    // 定義攝影機距離為z軸正向10個單位
    Eigen::Vector3f eye_pos = {0, 0, 10};

    // 硬編碼定義三角形三個頂點的坐標
    std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};

    // 三角形的三個頂點順序
    std::vector<Eigen::Vector3i> ind{{0, 1, 2}};

    // 向光柵化器傳入坐標vector
    auto pos_id = r.load_positions(pos);

    // 向光柵化器傳入頂點索引vector,寫過OpenGL的應該對這個有印象
    auto ind_id = r.load_indices(ind);

    int key = 0;
    int frame_count = 0;

    // 如果用戶在命令行參數選擇以圖片的形式進行輸出
    if (command_line) {
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);

        r.set_model(get_model_matrix(angle));
        r.set_view(get_view_matrix(eye_pos));
        r.set_projection(get_projection_matrix(45, 1, 0.1, 100));

        r.draw(pos_id, ind_id, rst::Primitive::Triangle);
        cv::Mat image(700, 700, CV_32FC3, r.frame_buffer().data());
        image.convertTo(image, CV_8UC3, 1.0f);

        cv::imwrite(filename, image);

        return 0;
    }

    // 在每次繪制循環中,若接收到的鍵盤輸入不為esc
    while (key != 27) {
        // 在本次繪制開頭,光柵化器對內部的幀緩存以及深度緩存進行初始化操作
        r.clear(rst::Buffers::Color | rst::Buffers::Depth);
        ...
    }

    return 0;
}

跳轉到rasterizer的clear函數看看是如何實現的:

rst::rasterizer::clear(rst::Buffers buff)

注意到clear的參數類型是rst::Buffers,先看看其位於rasterizer.hpp的聲明:

rst::Buffers
enum class Buffers
{
    Color = 1,
    Depth = 2
};

可看出,Buffers是一個強類型枚舉,它區分了程序欲刷新的緩沖為幀緩存(顏色緩存)還是深度緩存。

現在回到clear函數,可見內部采用了掩碼操作:

void rst::rasterizer::clear(rst::Buffers buff)
{
    // 若類型為rst::Buffers::Color,則將幀緩存的顏色刷新為RGB(0, 0, 0)
    if ((buff & rst::Buffers::Color) == rst::Buffers::Color)
    {
        std::fill(frame_buf.begin(), frame_buf.end(), Eigen::Vector3f{0, 0, 0});
    }
    // 若類型為rst::Buffers::Depth,則將深度緩存的所有深度值刷新為無限大(遠)
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

回到之前的while循環:

while (key != 27) {
    // 在本次幀繪制開頭,光柵化器對內部的幀緩存以及深度緩存進行初始化操作
    r.clear(rst::Buffers::Color | rst::Buffers::Depth);

    /* rasterizer內部有三個類型為Eigen::Matrix4f的變換矩陣,
       我們通過在main函數中定義的三個函數來構造並傳遞這三個矩陣*/
    r.set_model(get_model_matrix(angle));
    r.set_view(get_view_matrix(eye_pos));
    r.set_projection(get_projection_matrix(45, 1, 0.1, 50));
    ...
}

先跳出循環,看看main函數中的變換矩陣是如何構造的。

get_view_matrix

// 傳入攝影機坐標
Eigen::Matrix4f get_view_matrix(Eigen::Vector3f eye_pos)
{
    // 初始化一個單位矩陣view
    Eigen::Matrix4f view = Eigen::Matrix4f::Identity();

    /* 定義一個變換矩陣,其變換操作為,將攝影機移(Transition)至
       右手系坐標原點(0, 0, 0), 在最終渲染時,坐標空間內的所有物
       件均會同攝影機一起相對移動*/
    Eigen::Matrix4f translate;
    translate << 1, 0, 0, -eye_pos[0], 
                 0, 1, 0, -eye_pos[1], 
                 0, 0, 1, -eye_pos[2], 
                 0, 0, 0, 1;

    view = translate * view;

    return view;
}

get_model_matrix

// 傳入一個旋轉角度
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
    // 初始化一個單位矩陣model
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();

    // 定一個一個變換矩陣translate
    Eigen::Matrix4f translate;

    // math.h定義的三角函數均采用弧度制,這里先將旋轉角度轉換一下
    double radian = rotation_angle / 180.0 * MY_PI;

    translate << cos(radian), -sin(radian), 0, 0,
                 sin(radian),  cos(radian), 0, 0,
                 0, 0, 1, 0,
                 0, 0, 0, 1;

    model = translate * model;

    return model;
}

get_projection_matrix

/* eye_fov: 視場角(Field of View),aspect_ratio:縱橫比
   zNear:視錐Frustum的Near面與攝影機的單位距離,zNear:同理為Far面的單位距離*/
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar)
{
    // 初始化一個單位矩陣projection
    Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();

    /* 初始化一個單位矩陣persp2ortho,其作用為將透視點映射為適合
       進行正交投影的點*/
    Eigen::Matrix4f persp2ortho = Eigen::Matrix4f::Identity();
    persp2ortho << zNear, 0, 0, 0,
            0, zNear, 0, 0,
            0, 0, zNear + zFar, -zNear * zFar,
            0, 0, 1, 0;

    double halfEyeRadian = eye_fov * MY_PI / 2 / 180.0;
    double top = zNear * tan(halfEyeRadian);
    double bottom = -top;
    double right = top * aspect_ratio;
    double left = -right;

    Eigen::Matrix4f orthoScale = Eigen::Matrix4f::Identity();
    orthoScale << 2 / (right - left), 0, 0, 0,
            0, 2 / (top - bottom), 0, 0,
            0, 0, 2 / (zNear - zFar), 0,
            0, 0, 0, 1;

    Eigen::Matrix4f orthoTrans = Eigen::Matrix4f::Identity();
    orthoTrans << 1, 0, 0, -(right + left) / 2,
            0, 1, 0, -(top + bottom) / 2,
            0, 0, 1, -(zNear + zFar) / 2,
            0, 0, 0, 1;

    Eigen::Matrix4f matrixOrtho = orthoScale * orthoTrans;

    projection = matrixOrtho * persp2ortho;

    return projection;
}

現在來看看透視投影的操作過程。

GAMES101課程框架定義的世界坐標系和OpenGL相同。初始狀態下,作業1的攝像機位置以及三角形頂點表示為上圖

  • 首先進行Model Transform模型變換,代碼中規定\(angle = 0\),即不對三角形進行旋轉操作。

  • 接着進行View Transform(在虎書中叫做Camera Transform),這個變換會將攝像機移動到世界原點,同時會應用到世界中的所有對象(這里為三角形),效果即為三角形和攝像機之間保持相對靜止地進行了一次移動,在這里,我們的三角形三個頂點被變換為\((2, 0, -12), (0, 2, -12), (-2, 0, -12)\)

現在,規定一個空間:

  • 它的前后由Near面和Far面所限制,這個空間叫做View Space(虎書中叫做Camera Space),對於其形狀命名為Frustum視錐,見下圖灰色部分體積。

  • 超出View Space的對象會被剔除或裁剪。在作業1中,我們的三角形正確地坐落在View Space中。

  • 攝影機(原點)與far面上下兩條邊的中點分別連線,兩條連線之間的夾角即為視場角(FOV, Field of View)。

  • Near面是實際的物理投影面,也即,處於View Space中的對象經過透視變換會成為Near面上的二維投影。

  • 對象僅在處於View Space時,其頂點z值才具有實際的、表示“位置”的物理意義。投影變換完成后,其z值不再具有實際物理意義,而僅用於深度測試。

好了,到這里我們得到了View Space,而透視投影變換的目標是將View Space中的所有點變換到Canonical View Volume(CVV)中(虎書141頁),CVV是一個重心與原點重合,邊長為2的正方體:

為了得到這個目標,首先將View Space內所有點經由透視投影按正確的比例映射到near面上,圖形學通用做法是將此視錐內的所有點按照比例通過透視投影壓縮成一個正交立方體,這個步驟能將三角形正好壓縮成與Near面上的小三角形投影等大小:

現在,我們在View Space中任意取一點,它在經過變換后會在Near面上有一個投影點,我們通過計算它們的關系可以得出一個能求得正確變換比例的矩陣,如下圖:

考慮這兩個點的x值如何建立一個變換關系:

  • 設Near面與原點的距離為其縮寫n,設在View Space中取的任意點距原點z。

  • 可看出灰色區域有兩個相似三角形,則可以得出關系式 \(\frac{x^{'}}{x} = \frac{n}{z}\),也即 \(x^{'} = \frac{n}{z} * x\)

考慮兩點的y值的變換關系:

  • 可看出灰色區域有兩個相似三角形,則可以得出關系式 \(\frac{y^{'}}{y} = \frac{n}{z}\),也即 \(y^{'} = \frac{n}{z} * y\)

在數學上表示的話:

  • 即同理地對於View Space內任意一點 \(\left(\begin{array}{c}x\\ y\\ z\\ 1\end{array}\right)\),其通過比例運算后,會變為 \(\left(\begin{array}{c}nx/z\\ ny/z\\ unknown\\ 1\end{array}\right)\)(目前並不清楚z值的變換關系)。

  • 又由齊次坐標的性質(對每個坐標分量乘以一個相同的數,坐標與原坐標相同),我們對每個坐標分量同時乘以\(z\)\(\left(\begin{array}{c}nx\\ ny\\ unknown\\ z\end{array}\right)\)

而我們就是要構建一個矩陣M,使得對於視錐內的所有點(x,y,z),有 \(M^{(4 x 4)}_{persp->ortho}\left(\begin{array}{c}x\\ y\\ z\\ 1\end{array}\right)=\left(\begin{array}{c}nx\\ ny\\ unknown\\ z\end{array}\right)\)

由於矩陣M為一個4*4矩陣,且根據此等式以及矩陣和向量的乘法,容易推出M除了第三行之外的數字:

\(M^{(4 x 4)}_{persp->ortho}=\left(\begin{array}{cccc}n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ ? & ? & ? & ?\\ 0 & 0 & 1 & 0\end{array}\right)\)

只需要將第三行推出來,M就完美了。現在我們還有兩個條件可用:

1.在投影變換的過程中,位於near面上的任何點都不會改變

near面的z值為n,所以任意near上的一點可表示為\(\left(\begin{array}{c}x\\ y\\ n\\ 1\end{array}\right)\)。由於near上的點經過投影變換不會有任何改變,所以會有這樣的等式\(M ^{(4 x 4)}_{persp->ortho}\left(\begin{array}{c}x\\ y\\ n\\ 1\end{array}\right)=\left(\begin{array}{c}x\\ y\\ n\\ 1\end{array}\right)\),為了方便計算,將等式右邊的點乘以n,變為:\(M ^{(4 x 4)}_{persp->ortho}\left(\begin{array}{c}x\\ y\\ n\\ 1\end{array}\right)=\left(\begin{array}{c}nx\\ ny\\ n^{2}\\ n\end{array}\right)\)

所以根據此等式,容易推出M的第三行為(0, 0, A, B),也即 \(\left(\begin{array}{cccc}0 & 0 & A & B\end{array}\right)\left(\begin{array}{c}x\\ y\\ n\\ 1\end{array}\right)=n^{2}\),進而得到方程 \((1) An + B = n^{2}\)

2.在投影變換的過程中,位於far面上的任何點的z值均不會改變(因為far面本身不會移動)

假設far面的z值為f,由此條件,同理可得到方程 \((2) Af + B = f^{2}\)

由式子(1)(2)解出 \(A = n + f\) 以及 \(B = -nf\)

到此,我們成功得到矩陣M:\(M^{(4 x 4)}_{persp->ortho}=\left(\begin{array}{cccc}n & 0 & 0 & 0\\ 0 & n & 0 & 0\\ 0 & 0 & n + f & -nf\\ 0 & 0 & 1 & 0\end{array}\right)\)

之后的工作便是將這個正交立方體壓縮為一個CVV:

我們同樣根據比例將正交立方體進行壓縮、平移。所以分別構造縮放矩陣和平移矩陣:

\(M^{(4 x 4)}_{orthoScale}=\left(\begin{array}{cccc}\frac{2}{r - l} & 0 & 0 & 0\\ 0 & \frac{2}{t - b} & 0 & 0\\ 0 & 0 & \frac{2}{n - f} & 0\\ 0 & 0 & 0 & 1\end{array}\right)\) \(M^{(4 x 4)}_{orthoTrans}=\left(\begin{array}{cccc}1 & 0 & 0 & \frac{-(r + l)}{2}\\ 0 & 1 & 0 & \frac{-(t + b)}{2}\\ 0 & 0 & 0 & \frac{-(n + f)}{2}\\ 0 & 0 & 0 & 1\end{array}\right)\)

但由於實際的函數參數並未包含t、b、l、r,所以我們需要根據給定的幾個參數將它們算出來。

代碼中的aspect_ratio即為縱橫比(長寬比),所以參考上圖和代碼,可以發現其實僅做了一些簡單的幾何運算便算出了我們需要的參數。

這樣一來,我們便完成了透視投影變換矩陣的構造。現在回到循環:

while (key != 27) {
    r.clear(rst::Buffers::Color | rst::Buffers::Depth);

    r.set_model(get_model_matrix(angle));
    r.set_view(get_view_matrix(eye_pos));
    r.set_projection(get_projection_matrix(45, 1, 0.1, 50));

    r.draw(pos_id, ind_id, rst::Primitive::Triangle);
    ...
}

現在看看rasterizer的draw函數做了些什么。回顧下之前的代碼:

auto pos_id = r.load_positions(pos);
auto ind_id = r.load_indices(ind);

向rasterizer的這兩個函數分別傳入頂點坐標以及頂點索引后,會分別返回一個"id"。為了弄清楚"id"是做什么的,先看看"load"函數的內部:

rasterizer.cpp

rst::pos_buf_id & rst::ind_buf_id

注意到函數load_positions以及load_indices的返回值類型分別為rst::pos_buf_id以及rst::ind_buf_id,這兩個類型定義在rasterizer.hpp中:

struct pos_buf_id
{
    int pos_id = 0;
};

struct ind_buf_id
{
    int ind_id = 0;
};

可看到閆教授將兩種類型為int的"id"以struct的形式包裝起來了,目的應該是區分"id"的類型。

回到兩個函數:

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};
}
  • 函數內的get_next_id()維護了rasterizer的一個內部成員countcount初始化值為0,每次調用get_next_id(),它會將count值加1並返回。

  • 而函數內的pos_buf以及ind_buf均為rasterizer的內部變量成員,且類型均為std::map<int, std::vectorEigen::Vector3f>

  • 也就是說,在調用一個"load"函數時,函數會生成一個新的"id"值作為對應map的一個key,並將鍵值對{id, 頂點坐標vector / 頂點索引vector}插入對應map中。

最后,這兩個"load"函數分別返回生成的鍵值給調用者,供draw函數使用(根據閆教授的代碼注釋,這樣設計的原因是,可以保證調用者能拿到正確的key,也就是"id",來取得對應的頂點坐標vector / 頂點索引vector,避免調用者搞錯)。

注意,每次生成的buf_id以及ind_id應該是對應的:

知道兩個"load"函數的返回值意義后,回到draw函數:

draw

// 向draw函數傳入之前生成的“頂點坐標緩沖id”以及“頂點索引緩沖id”
void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{
    // 當前作業中,代碼支持的圖元(Primitive)類型僅為rst::Primitive::Triangle,即三角形
    if (type != rst::Primitive::Triangle)
    {
        throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");
    }

    // 容易得出,auto推導出的類型為std::vector<Eigen::Vector3f>
    // buf取得對應的圖元頂點坐標vector
    auto& buf = pos_buf[pos_buffer.pos_id];
    // ind取得對應的圖元頂點索引vector
    auto& ind = ind_buf[ind_buffer.ind_id];

    // 下面會解釋f1、f2的含義
    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
        Triangle t;
      
        // 構造一個元素為4行向量的數組v,向內插入mvp矩陣對頂點索引對應頂點坐標的變換點
        // 為了和mvp進行運算,將每個頂點坐標轉為一個Eigen::Vector4f,並規定w值為1
        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;
        }

        // 將變換好的頂點坐標傳入三角形實例t
        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);

        rasterize_wireframe(t);
    }
}

視口變換

可以想象z軸正向有一個正交攝影機,其拍攝的正交影像通過視口變換投影拉伸到屏幕空間上。需要注意的是,大多數情況下投影plane的寬高比是與CVV(正方體)的寬高比不同的,因此頂點變換到CVV中后圖像可能會發生變形壓縮,但只要最后映射的屏幕空間的寬高比與投影plane一致,視口變換必定能將這種錯誤矯正回來。

視口變換需要注意的點:

視口變換的情況比較特殊,我們考慮一個非特殊變換例子。矩形區域\(A\)的中心為\(O\),上下左右邊界分別為\(t、b、l、r\),我們欲將\(A\)中所有點投影到\(B\)區域:

比較符合直覺的方法便是,先將\(A\)區域的中點平移到\(B\)區域的中點,再將長寬拉伸直到與區域\(B\)重合:

平移算法非常簡單。首先算出\(A\)區域中點\(o = (\frac{l + r}{2}, \frac{t + b}{2})\)\(B\)區域中點\(o' = (\frac{l' + r'}{2}, \frac{t' + b'}{2})\)

可容易得出對原\(A\)區域所有點進行平移的矩陣:

\(M _{Translation}\left(\begin{array}{cccc}1 & 0 & 0 & \frac{l' + r'}{2} - \frac{l + r}{2}\\ 0 & 1 & 0 & \frac{t' + b'}{2} - \frac{t + b}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{array}\right)\)

之后我們對\(A\)區域的點采用比例拉伸的方法。\(A\)區域的寬\(width = r - l\),高\(height = t - b\)\(B\)區域的寬\(width' = r' - l'\),高\(height' = t' - b'\)

所以對於原區域內的點,對其\(x\)值進行拉伸的比例因子為\(\frac{width'}{width}\);對其\(y\)值進行拉伸的比例因子為\(\frac{height'}{height}\)

結合平移矩陣,最終得到我們需要的變換矩陣:\(M _{Transform}\left(\begin{array}{cccc}\frac{width'}{width} & 0 & 0 & \frac{l' + r'}{2} - \frac{l + r}{2}\\ 0 & \frac{height'}{height} & 0 & \frac{t' + b'}{2} - \frac{t + b}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{array}\right)\)


回到視口變換,它特殊在其原區域的中心正好是坐標軸原點\((0,0)\)

經過model、view、projection變換后的頂點的\(x、y、z\)值的范圍均為\([-1, 1]\)。先不考慮z值,我們關注如何將頂點的\(x、y值\)\([-1, -1]^{2}\)變換到\([0,width]*[0,height]\),也即:

根據我們上面推導的變換矩陣,現在可以直接將對應值代入:

\(M_{Viewport} = M _{Transform}\left(\begin{array}{cccc}\frac{width}{1-(-1)} & 0 & 0 & \frac{0 + width}{2} - \frac{-1 + 1}{2}\\ 0 & \frac{height}{1-(-1)} & 0 & \frac{height + 0}{2} - \frac{1 + (-1)}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{array}\right) = \left(\begin{array}{cccc}\frac{width}{2} & 0 & 0 & \frac{width}{2}\\ 0 & \frac{height}{2} & 0 & \frac{height}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{array}\right)\)

值得一提的是,框架代碼進行視口變換的操作並沒有使用矩陣運算,而是直接在坐標值上進行了計算,也即將變換矩陣

\(M _{Viewport}\left(\begin{array}{cccc}\frac{width}{2} & 0 & 0 & \frac{width}{2}\\ 0 & \frac{height}{2} & 0 & \frac{height}{2}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1\end{array}\right)\)

變為vert.x() = 0.5*width*(vert.x()+1.0);這樣的操作。

現在回來考慮原頂點的\(z\)值,由於\(z\)也經歷了模型、視圖、投影的變換過程,所以\(z\)值范圍也被縮小到了\([-1, 1]\)范圍內,此時變換過的\(z\)值很可能是錯誤的,因為投影變換是一個非線性變換!。由於\(z\)值會被用於深度測試,所以必須將其變換到原來的近、遠面[near, far]范圍內。

由main函數的代碼可知框架定義的near、far面值為0.1、100:r.set_projection(get_projection_matrix(45, 1, 0.1, 100));

所以我們的任務便是將\(z\)值從\([-1,1]\)重新變換到\([0.1,100]\)中,方法和上面的大同小異,將原范圍中心平移到目標范圍,再進行縮放:

首先算\([-1, 1]\)的長度:\(width = 1 - (-1) = 2\)\([0.1, 100]\)的長度:\(width' = 100 - 0.1\);再分別算兩個范圍的中點:\(o = \frac{1 + (-1)}{2}、o' = \frac{(100 + 0.1)}{2}\)

則可得出縮放因子為\(\frac{width'}{width} = \frac{100-0.1}{2}\),平移值為\(o' - o = \frac{(100 + 0.1)}{2} - \frac{1 + (-1)}{2} = \frac{(100 + 0.1)}{2}\)

所以draw函數中的f1和f2就是這么來的,f1代表縮放值,f2代表平移值,它們的分母均為2僅僅是一個巧合;

變換代碼也即為vert.z() = vert.z() * f1 + f2;


回到代碼中,框架將三個變換好的頂點傳入三角形實例,並為每一個頂點設置了一個RGB顏色值,最后調用rasterize_wireframe。從函數名字來看,就能知道它是用來畫線條的。

rasterize_wireframe
// 分別對三邊進行繪制
void rst::rasterizer::rasterize_wireframe(const Triangle& t)
{
    draw_line(t.c(), t.a());
    draw_line(t.c(), t.b());
    draw_line(t.b(), t.a());
}

框架對於draw_line的實現是采用了經典的Bresenhan兩點畫線算法。算法每計算出直線上的一個點,框架便調用set_pixel(point, line_color)來進行單個點的着色:

set_pixel
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
    //old index: auto ind = point.y() + point.x() * width;
    if (point.x() < 0 || point.x() >= width ||
        point.y() < 0 || point.y() >= height) return;
    auto ind = (height-point.y())*width + point.x();
    frame_buf[ind] = color;
}

函數首先對坐標值的合法性做一個判斷。然后算出當前點的存放索引應該為rasterizer成員frame_buf的哪個位置。為了解釋這個索引算法,先看看rasterizer的構造函數:

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

我們看到,光柵化器的構造函數傳入實際顯示的寬度以及長度,並將內部vector成員frame_buf以及depth_buf的尺寸設置為width * height。

如何解釋着色點的索引過程呢?

實際上我們將顯式區域的面積進行了離散化,而這個求着色點的過程其實是在對面積進行量化。在代碼中,我們將上圖的屏幕矩形的離散點從頭到尾打散並裝入一個一維的vector中,所以這種索引方式無論是在二維表示還是一維表示均是有效的。

需要注意的是,存儲幀緩存的Mat數組的尋址起點是左上角,而待光柵化的頂點的尋址起點是左下角。

至此作業1代碼的解析就告一段落了,本人水平十分有限,如果有任何疏漏歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM