Games101現代計算機圖形學入門 - 作業1~8 集合含提高項總結

Github 地址 :Games101 作業總結
Games101 官網:Games101 - 現代圖形學入門
記錄 Games101 Homework 系列 1 ~ 8 及相關作業提高項

環境安裝

  • 開發環境:Win 10
  • IDE:Visual Studio 2022
    由於懶得搞 VM,所以直接在 win 平臺下開發,主要的依賴如下:
  • Eigen
  • OpenCV
    需要先安裝 CMake,下載 Eigen 源碼和 OpenCV 源碼,利用 Cmake 進行源碼編譯後,將 include 和 dll 配置到 Visual studio 中即可。

作業 0 - 框架搭建

要求

給定一個點 P=(2,1), 將該點繞原點先逆時針旋轉 45 ◦ ,再平移 (1,2), 計算出變換後點的座標(要求用齊次座標進行計算)。

思路

思路比較簡單,定義好仿射變換的矩陣,然後運用到向量上即可。由於是二維平面,運用齊次座標後得到 3 x 3 的矩陣

\[\begin{bmatrix} cosθ & -sinθ & 1 \\ sinθ & cosθ & 2 \\ 0 & 0 & 1 \\ \end{bmatrix} \]

利用 Eigen 定義好矩陣後左乘點 p \((2,1,0)\) 即可。

作業 1 - MVP 變換

要求

填寫一個旋轉矩陣和一個透視投影矩陣。給定三維下三個點 v 0 (2.0,0.0,−2.0), v1 (0.0,2.0,−2.0), v2 (−2.0,0.0,−2.0), 你需要將這三個點的座標變換爲屏幕座標並在屏幕上繪製出對應的線框三角形 (在代碼框架中已經提供了 draw_triangle 函數,所以只需要去構建變換矩陣即可)。簡而言之,需要進行模型、視圖、投影、視口等變換來將三角形顯示在屏幕上。在提供的代碼框架中,需要完成模型變換和投影變換的部分。

思路

只需要實現如下接口:

Eigen::Matrix4f get_model_matrix(float rotation_angle);
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
                                      float zNear, float zFar);

分別對應 MVP 變換中的 Model Transform 和 Project Transform。只需要分別實現對應的矩陣即可。

[!note]
View Transform 在框架中已經實現好了,因此不需要關注相機的位置。

Model Transform

這裏需要實現 Model Transform 的原因是三角形會根據鍵盤輸入旋轉,因此需要構建對應三角形每個頂點的變換矩陣。也就是旋轉矩陣 。繞指定軸的旋轉矩陣在課程中已經推導過了,更詳細的可以看 Lecture 3 ~ 4 的 [[Lecture 3 ~ 4 多維變換與MVP#繞 XYZ 旋轉|推導過程]] 。
矩陣的構建比較簡單,只是需要注意:c++ 的標準接口 sin 和 cos 接收的參數是弧度,因此需要做一次角度 -> 弧度的轉換。

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 angle = rotation_angle / 180 * MY_PI;
    model << std::cos(angle), -std::sin(angle), 0, 0,
        std::sin(angle), std::cos(angle), 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1;

    return model;
}

Projection Transform

接下來要做的是投影變換,需要先做一次透視投影,然後再做正交投影將物體縮放到合適的比例並移動到合適的位置。

  • 透視投影的矩陣最終結果和[[Lecture 3 ~ 4 多維變換與MVP#透視投影(Perspective projection)|推導過程]]一致。這裏對各個參數進行簡單介紹
    • eye_fov:視角寬度,這裏理解爲上下視角或者左右視角的寬度都可以,因爲框架中 width/height 的比例爲 1
    • aspect_ratio:寬高比
    • zNear:近平面 Z 值
    • zFar:原平面 Z 值
  • 正交投影的矩陣如下,由於 XY 沒有再平移(透視投影已經做了 XY 的修正,所以最後一列的列向量的 XY 值爲 0,只需要考慮 Z 值)

image.png

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();
    Eigen::Matrix4f Mperspective;
    Mperspective << zNear, 0, 0, 0,
        0, zNear, 0, 0,
        0, 0, zNear + zFar, -zNear * zFar,
        0, 0, 1, 0;
     
    /* 假定 eye_fov 是上下的角度 */
    /* zNear 需要取反,因爲推導的矩陣是建立在 zNear ~ zFar 爲負值的情況 */
    float half_height = std::tan(eye_fov / 2) * -zNear;
    float half_width = half_height * aspect_ratio;

    // 先平移後縮放,正交投影
    Eigen::Matrix4f Morth;
    Morth << 1 / half_width, 0, 0, 0,
        0, 1 / half_height, 0, 0,
        0, 0, 2 / (zNear - zFar), (zFar - zNear) / (zNear - zFar),
        0, 0, 0, 1;

    projection =   Morth * Mperspective;
    return projection;
}

提高項 - 繞任意軸旋轉

提高項的內容難在理解推導矩陣公式,推導過程出來之後只需要直接寫矩陣公式即可。

image.png

這裏主要需要注意:羅德里格斯公式中的矩陣都是 3x3 的,而這裏代碼中運用了其次座標,因此返回的格式是 4x4 的矩陣,所以最後還需要轉換一下

/* 繞任意軸旋轉矩陣 */
Eigen::Matrix4f get_axis_model_matrix(float rotation_angle, Eigen::Vector3f axis)
{
    float angle = rotation_angle / 180 * MY_PI;
    Eigen::Matrix3f N = Eigen::Matrix3f::Identity();
    N << 0, -axis.z(), axis.y(),
        axis.z(), 0, -axis.x(),
        -axis.y(), axis.x(), 0;
    Eigen::Matrix3f rod = std::cos(angle) * Eigen::Matrix3f::Identity() + (1 - std::cos(angle)) * axis * axis.transpose() + std::sin(angle) * N;
    Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
    
    model << rod(0, 0), rod(0, 1), rod(0, 2), 0,
        rod(1, 0), rod(1, 1), rod(1, 2), 0,
        rod(2, 0), rod(2, 1), rod(2, 2), 0,
        0, 0, 0, 1;

    return model;
}

作業 2 - 光柵化

要求

在屏幕上畫出一個實心三角形,換言之,柵格化一個三角形。

思路

流程其實說的比較清楚了:

  1. 創建三角形的 2 維 bounding box。
  2. 遍歷此 bounding box 內的所有像素(使用其整數索引)。然後,使用像素中心的屏幕空間座標來檢查中心點是否在三角形內。
  3. 如果在內部,則將其位置處的插值深度值 (interpolated depth value) 與深度緩衝區 (depth buffer) 中的相應值進行比較。
  4. 如果當前點更靠近相機,請設置像素顏色並更新深度緩衝區 (depth buffer)。
    這裏比較麻煩的點在於只知道三角形頂點的深度,需要計算任意點的深度值,需要利用到重心座標,不過框架中直接提供了這一塊的計算公式。因此我們只需要直接利用計算出來的值來比較深度插值即可,該公式具體的推導可以參考

判斷點是否位於三角形內

這裏對判斷是否在三角形內部的接口返回值進行了修改,從 bool 調整爲 float,主要是爲了適配提高項中的 MSAA 超採樣。判斷點是否位於三角形內部利用了叉乘的計算方式,給定一個 (x,y) 位置的像素,這裏根據超採樣的次數 sampleTimes 來決定對一個像素採樣多少次。提高項中要求的是 2x2,因此實際採樣 4 次。

static bool _insideTriangle(Vector3f P, const Vector3f* _v)
{
    Vector3f AB = _v[1] - _v[0];
    Vector3f AP = P - _v[0];
    auto cross1 = AB.cross(AP);

    Vector3f BC = _v[2] - _v[1];
    Vector3f BP = P - _v[1];
    auto cross2 = BC.cross(BP);

    Vector3f CA = _v[0] - _v[2];
    Vector3f CP = P - _v[2];
    auto cross3 = CA.cross(AP);

    /* 相同符號 */
    if ((cross1.z() > 0 && cross2.z() > 0 && cross3.z() > 0) ||
        (cross1.z() < 0 && cross2.z() < 0 && cross3.z() < 0)) {
        return true;
    }

    return false;
}

/* 採樣次數 */
static const int sampleTimes = 1;
static const int totalSamplePoint = sampleTimes * sampleTimes;
static float insideTriangle(int x, int 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]
    Vector3f P = Eigen::Vector3f(x, y, 1.0f);
    float step = 1.0f / sampleTimes * 0.5f;
    float lefttopx = P.x() - 0.5;
    float lefttopy = P.y() + 0.5;

    float insideTrianglePoints = 0;
    for (int i = 1; i <= sampleTimes; i++) {
        for (int j = 1; j <= sampleTimes; j++) {
            Vector3f SamplePoint = Eigen::Vector3f(lefttopx + i * step, lefttopy - j * step, 1.0f);
            if (_insideTriangle(SamplePoint, _v)) {
                insideTrianglePoints += 1;
            }
        }
    }

    return insideTrianglePoints == 0 ? 0 : insideTrianglePoints / totalSamplePoint;
}

光柵化三角形

光柵化三角形的流程比較簡單:

  • 創建 Bounding Box
  • 遍歷 Pixel,校驗是否在三角形內部
  • 計算深度,如果深度更小,說明在更前方,更新深度值並設置顏色
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();

    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle
    float minx = FLT_MAX, maxx = FLT_MIN, miny = FLT_MAX, maxy = FLT_MIN;
    for (const auto& p : v) {
        minx = p.x() < minx ? p.x() : minx;
        maxx = p.x() > maxx ? p.x() : maxx;
        miny = p.y() < miny ? p.y() : miny;
        maxy = p.y() > maxy ? p.y() : maxy;
    }

    std::cout << "minx=" << minx << " miny=" << miny << " maxx=" << maxx << " maxy=" << maxy << std::endl;
    // If so, use the following code to get the interpolated z value.
    for (int y = floor(miny); y < ceil(maxy); y++) {
        for (int x = floor(minx); x < ceil(maxx); x++) {
            /* 檢查是否在三角形內 */
            float samplePercent = insideTriangle(x, y, t.v);
            if (samplePercent == 0.f) {
                continue;
            }
            /* 重心座標插值 t.v 是三角形頂點座標數組*/
            auto Barycentric2D = computeBarycentric2D(x, y, t.v);
            float alpha = std::get<0>(Barycentric2D), beta = std::get<1>(Barycentric2D), gamma = std::get<2>(Barycentric2D);
            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;

            // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
            auto ind = get_index(x, y);
            if (depth_buf[ind] > z_interpolated) {
                depth_buf[ind] = z_interpolated;
                Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
                set_pixel(point, t.getColor() * samplePercent);
            }
        }
    }
}

提高項 - MSAA

這裏主要對三角形進行抗鋸齒,最終效果如下(感覺不是特別明顯,需要放大看)

6d2d5b90b8ad585f289be9c5f68ff68.png

抗鋸齒的流程其實在前面實現光柵化的流程中就已經講述了,但是前面實現的時候少寫了一個點,這裏每個像素還是採用一個深度值,在兩個三角形疊加的邊界部分,可能出現一個像素分別被兩個三角形佔據,而一個深度值會導致最終只顯示一個三角形的顏色。因此需要根據對單個三角形 Sample 的次數,增加一個 Sample Depth List(保存每個樣本的深度值) 和 Sample Frame List(每個樣本的顏色)。最終上色的時候使用樣本疊加的顏色。
修改後的光柵化流程如下:

增加 Sample 的 Depth Buffer 和 Frame Buffer

初始化 Buffer

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

清理 Buffer

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});
    }
    if ((buff & rst::Buffers::Depth) == rst::Buffers::Depth)
    {
        std::fill(depth_buf.begin(), depth_buf.end(), std::numeric_limits<float>::infinity());
    }
    if ((buff & rst::Buffers::SampleColor) == rst::Buffers::SampleColor)
    {
        std::fill(sample_frame_buf.begin(), sample_frame_buf.end(), Eigen::Vector3f{ 0, 0, 0 });
    }
    if ((buff & rst::Buffers::SampleDepth) == rst::Buffers::SampleDepth)
    {
        std::fill(sample_depth_buf.begin(), sample_depth_buf.end(), std::numeric_limits<float>::infinity());
    }
}

然後在 main 中調用 Clear 的地方加上清理的枚舉,需要注意的是這裏 Buffers 的枚舉是按位或運算,因此不能直接遞增,要用 2 的 N 次方

重新調整光柵化

光柵化的調整流程主要是從原來的計算重心座標只計算像素點,調整爲計算採樣點。然後更新對應的 Sample Frame Buffer 和 Sample Depth Buffer。最後設置顏色的時候,根據像素的 index,找到對應映射的採樣點 Range,求 Color 的 RGB Avg 值即可。


//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
    auto v = t.toVector4();

    // TODO : Find out the bounding box of current triangle.
    // iterate through the pixel and find if the current pixel is inside the triangle
    float minx = FLT_MAX, maxx = FLT_MIN, miny = FLT_MAX, maxy = FLT_MIN;
    for (const auto& p : v) {
        minx = p.x() < minx ? p.x() : minx;
        maxx = p.x() > maxx ? p.x() : maxx;
        miny = p.y() < miny ? p.y() : miny;
        maxy = p.y() > maxy ? p.y() : maxy;
    }

    std::cout << "minx=" << minx << " miny=" << miny << " maxx=" << maxx << " maxy=" << maxy << std::endl;
    // If so, use the following code to get the interpolated z value.
    for (int x = floor(minx); x < ceil(maxx); x++) {
        for (int y = floor(miny); y < ceil(maxy); y++) {
            /* 檢查是否在三角形內 */
            float grid_width = 1.0f / sampleTimes;
            float grid_p_offset = grid_width * 0.5f;
            // 左上角開始算
            float lefttopx = x - 0.5f;
            float lefttopy = y - 0.5f;
            bool should_set_color = false;
            for (int i = 0; i < sampleTimes; i++) {
                for (int j = 0; j < sampleTimes; j++) {
                    float samplex = lefttopx + i * grid_width + grid_p_offset;
                    float sampley = lefttopy + j * grid_width + grid_p_offset;
                    if (insideTriangle(samplex, sampley, t.v)) {
                        // 計算重心座標
                        auto Barycentric2D = computeBarycentric2D(samplex, sampley, t.v);
                        float alpha = std::get<0>(Barycentric2D), beta = std::get<1>(Barycentric2D), gamma = std::get<2>(Barycentric2D);
                        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 sample_ind = get_index(x, y) + i * sampleTimes + j;
                        float sample_z = sample_depth_buf[sample_ind];
                        if (sample_z > z_interpolated) {
                            should_set_color = true;
                            sample_depth_buf[sample_ind] = z_interpolated;
                            sample_frame_buf[sample_ind] = t.getColor() / totalSamplePoint;
                        }
                    }
                }
            }
            int ind = get_index(x, y);
            Vector3f final_color = { 0, 0, 0 };
            for (int i = ind; i < ind + totalSamplePoint; i++) {
                final_color += sample_frame_buf[i];
            }
            Eigen::Vector3f point = Eigen::Vector3f(x, y, 1.0f);
            set_pixel(point, final_color);
        }
    }
}

作業 3 - Shading

要求

  1. 修改函數 rasterize_triangle(const Triangle& t) in rasterizer.cpp: 在此處實現與作業 2 類似的插值算法,實現法向量、顏色、紋理顏色的插值。
  2. 修改函數 get_projection_matrix() in main.cpp: 將你自己在之前的實驗中實現的投影矩陣填到此處,此時你可以運行 ./Rasterizer output.png normal 來觀察法向量實現結果。
  3. 修改函數 phong_fragment_shader() in main.cpp: 實現 Blinn-Phong 模型計算 Fragment Color.
  4. 修改函數 texture_fragment_shader() in main.cpp: 在實現 Blinn-Phong 的基礎上,將紋理顏色視爲公式中的 kd,實現 Texture Shading Fragment Shader.
  5. 修改函數 bump_fragment_shader() in main.cpp: 在實現 Blinn-Phong 的基礎上,仔細閱讀該函數中的註釋,實現 Bump mapping.
  6. 修改函數 displacement_fragment_shader() in main.cpp: 在實現 Bumpmapping 的基礎上,實現 displacement mapping

作業的一些問題

摘自論壇作業三公告
(1) bump mapping 部分的 h(u,v)=texture_color(u,v).norm, 其中 u,v 是 tex_coords, w,h 是 texture 的寬度與高度
(2) rasterizer.cpp 中 v = t.toVector4()
(3) get_projection_matrix 中的 eye_fov 應該被轉化爲弧度制
(4) bump 與 displacement 中修改後的 normal 仍需要 normalize
(5) 可能用到的 eigen 方法:norm(), normalized(), cwiseProduct()
(6) 實現 h(u+1/w,v) 的時候要寫成 h(u+1.0/w,v)
(7) 正規的凹凸紋理應該是隻有一維參量的灰度圖,而本課程爲了框架使用的簡便性而使用了一張 RGB 圖作爲凹凸紋理的貼圖,因此需要指定一種規則將彩色投影到灰度,而我只是「恰好」選擇了 norm 而已。爲了確保你們的結果與我一致,我纔要求你們都使用 norm 作爲計算方法。
(8) bump mapping & displacement mapping 的計算的推導日後將會在光線追蹤部分詳細介紹,目前請按照註釋實現。

思路

總體的思路是先做好光柵化,對三角形的各個屬性在面上的點做插值,其次就是做 shader,應用不同的光照模型 or 貼圖。

光柵化

第一步的首先需要做光柵化,這裏主要應用到重心座標的概念。對屬性進行插值的方式和作業二類似。這裏需要注意的是有個 interpolated_shadingcoords ,這裏其實是從 eye pos 出發打到三角形的各個頂點插值後的座標。也就是三角形的被觀測點。

//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t, const std::array<Eigen::Vector3f, 3>& view_pos) 
{
    const auto& v = t.toVector4();
    const auto& color = t.color;
    const auto& normal = t.normal;
    const auto& texcoord = t.tex_coords;

    float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN;
    for (const auto& p : v) {
        xmin = p.x() < xmin ? p.x() : xmin;
        xmax = p.x() > xmax ? p.x() : xmax;
        ymin = p.y() < ymin ? p.y() : ymin;
        ymax = p.y() > ymax ? p.y() : ymax;
    }

    // If so, use the following code to get the interpolated z value.
    for (int y = floor(ymin); y < ceil(ymax); y++) {
        for (int x = floor(xmin); x < ceil(xmax); x++) {
            /* 檢查是否在三角形內 */
            if (!insideTriangle(x, y, t.v)) {
                continue;
            }
            /* 重心座標插值 t.v 是三角形頂點座標數組*/
            auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
            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;

            // TODO : set the current pixel (use the set_pixel function) to the color of the triangle (use getColor function) if it should be painted.
            auto ind = get_index(x, y);
            if (depth_buf[ind] > z_interpolated) {
                depth_buf[ind] = z_interpolated;

                // * 顏色插值 *
                Vector3f interpolated_color = alpha * color[0]  + beta * color[1]  + gamma * color[2] ;
                interpolated_color *= w_reciprocal;
                // * 法線插值 *
                Vector3f interpolated_normal = alpha * normal[0]  + beta * normal[1]  + gamma * normal[2] ;
                interpolated_normal *= w_reciprocal;
                // * 紋理插值 *
                Vector2f interpolated_texcoords = alpha * texcoord[0]  + beta * texcoord[1]  + gamma * texcoord[2] ;
                interpolated_texcoords *= w_reciprocal;
                // * 觀測點插值 *
                Vector3f interpolated_shadingcoords = alpha * view_pos[0]  + beta * view_pos[1]  + gamma * view_pos[2];
                interpolated_shadingcoords *= w_reciprocal;
               
                fragment_shader_payload payload( interpolated_color, interpolated_normal.normalized(), interpolated_texcoords, texture ? &*texture : nullptr);
                payload.view_pos = interpolated_shadingcoords;
                // Use: Instead of passing the triangle's color directly to the frame buffer, pass the color to the shaders first to get the final color;
                auto pixel_color = fragment_shader(payload);
                Eigen::Vector2i point = Eigen::Vector2i(x, y);
                set_pixel(point, pixel_color);
            }
        }
    }
}

normal_fragment_shader

框架默認實現了該 Shader,處理完默認的光柵化後即可看到效果

phong_fragment_shader

接下來需要實現光照的 Phong 模型,簡單套公式即可

image.png

其中有幾點需要注意:

  • \(I_a\) 表示 光照的 Intensity
  • Vector3f 相乘需要使用 cwiseProduct
  • 計算半程向量,法線等向量時需要進行歸一化
Eigen::Vector3f phong_fragment_shader(const fragment_shader_payload& payload)
{
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    Eigen::Vector3f result_color = {0, 0, 0};
    for (auto& light : lights)
    {
        // TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 
        // components are. Then, accumulate that result on the *result_color* object.
        Vector3f l = (light.position - point);
        Vector3f v = eye_pos - point;
        float r_square = l.dot(l);
        // 計算 ambient 環境光
        Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
        // 計算 diffuse 漫反射光 
        float ndotl = normal.normalized().dot(l.normalized());
        Vector3f diffuse = kd.cwiseProduct(light.intensity / r_square * std::max(0.f, ndotl));
        // 計算 specular 高光 
        Vector3f h = l.normalized() + v.normalized();
        float ndoth = normal.normalized().dot(h.normalized());
        Vector3f specular = ks.cwiseProduct(light.intensity / r_square) * pow(std::max(0.f, ndoth), p);

        result_color += (ambient + diffuse + specular);
    }
    return result_color * 255.f;
}

texture_fragment_shader

這裏主要將物體表面的顏色替換成 Texture 的 getColor 接口,需要注意的是:(u,v) 座標在實際應用中可能存在負數或者 > 1(正常取值範圍在 0 ~ 1),所以還需要做有效性檢測


Eigen::Vector3f texture_fragment_shader(const fragment_shader_payload& payload)
{
    Eigen::Vector3f return_color = {0, 0, 0};
    Eigen::Vector3f texture_color;
    if (payload.texture)
    {
        // TODO: Get the texture value at the texture coordinates of the current fragment
        /* 獲取紋理座標 */
        float x = payload.tex_coords.x();
        float y = payload.tex_coords.y();
        if (x < 0 || x > 1 || y < 0 || y > 1)
        {
            std::cout << "error tex coords x=" << x << " y=" << y << std::endl;
            texture_color << return_color.x(), return_color.y(), return_color.z();
        }
        else 
        {
            texture_color = payload.texture->getColor(x, y);
        }
    }
    //texture_color << return_color.x(), return_color.y(), return_color.z();

    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = texture_color / 255.f;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = texture_color;
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

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

    for (auto& light : lights)
    {
        // TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 
        // components are. Then, accumulate that result on the *result_color* object.
        Vector3f l = (light.position - point);
        Vector3f v = eye_pos - point;
        float r_square = l.dot(l);
        // 計算 ambient 環境光
        Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
        // 計算 diffuse 漫反射光 
        float ndotl = normal.normalized().dot(l.normalized());
        Vector3f diffuse = kd.cwiseProduct(light.intensity / r_square * std::max(0.f, ndotl));
        // 計算 specular 高光 
        Vector3f h = l.normalized() + v.normalized();
        float ndoth = normal.normalized().dot(h.normalized());
        Vector3f specular = ks.cwiseProduct(light.intensity / r_square) * pow(std::max(0.f, ndoth), p);

        result_color += (ambient + diffuse + specular);
    }

    return result_color * 255.f;
}

bump_fragment_shader

這一節主要實現凹凸貼圖。這裏有個嚴重的問題是註釋中的 h(u,v) 計算完全沒有提及,後續作業三的公告中才做了解釋:bump mapping 部分的 h(u,v)=texture_color(u,v).norm, 其中 u,v 是 tex_coords, w,h 是 texture 的寬度與高度
這裏的核心思路是擾動法線,利用貼圖的法線信息和原本物體表面的法線來計算出擾動後的法線,用到了 TBN 矩陣和切線空間,這兩個知識點需要後續補齊。

Eigen::Vector3f bump_fragment_shader(const fragment_shader_payload& payload)
{
    
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color; 
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;


    float kh = 0.2, kn = 0.1;

    // TODO: Implement bump mapping here
    // Let n = normal = (x, y, z)
    // Vector t = (x*y/sqrt(x*x+z*z),sqrt(x*x+z*z),z*y/sqrt(x*x+z*z))
    // Vector b = n cross product t
    // Matrix TBN = [t b n]
    // dU = kh * kn * (h(u+1/w,v)-h(u,v))
    // dV = kh * kn * (h(u,v+1/h)-h(u,v))
    // Vector ln = (-dU, -dV, 1)
    // Normal n = normalize(TBN * ln)

    // TODO: Implement displacement mapping here
    float x = normal.x(), y = normal.y(), z = normal.z();
    Vector3f t = Eigen::Vector3f(x * y / sqrt(x * x + z * z), sqrt(x * x + z * z), z * y / sqrt(x * x + z * z));
    Vector3f b = normal.cross(t);
    float h = payload.texture->height;
    float w = payload.texture->width;
    float u = payload.tex_coords.x();
    float v = payload.tex_coords.y();
    Eigen::Matrix3f TBN;
    TBN << t.x(), b.x(), normal.x(),
        t.y(), b.y(), normal.y(),
        t.z(), b.z(), normal.z();

    float dU = 0, dV = 0;
    if (payload.texture && is_valid_uv(u, v)) {
        dU = kh * kn * (payload.texture->getColor(u + 1.0f / w, v).norm() - payload.texture->getColor(u, v).norm());
        dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());
    }

    Vector3f ln = Eigen::Vector3f(-dU, -dV, 1);
    normal = (TBN * ln);
    Eigen::Vector3f result_color = { 0, 0, 0 };
    result_color = normal;

    return result_color * 255.f;
}

displacement_fragment_shader

這裏的流程和 Bump Shader 基本一致,主要的區別在於,Bump Fragment Shader 只做法線擾動,而 Displacement Fragment Shader 會修改三角面上的頂點值。

Eigen::Vector3f displacement_fragment_shader(const fragment_shader_payload& payload)
{
    
    Eigen::Vector3f ka = Eigen::Vector3f(0.005, 0.005, 0.005);
    Eigen::Vector3f kd = payload.color;
    Eigen::Vector3f ks = Eigen::Vector3f(0.7937, 0.7937, 0.7937);

    auto l1 = light{{20, 20, 20}, {500, 500, 500}};
    auto l2 = light{{-20, 20, 0}, {500, 500, 500}};

    std::vector<light> lights = {l1, l2};
    Eigen::Vector3f amb_light_intensity{10, 10, 10};
    Eigen::Vector3f eye_pos{0, 0, 10};

    float p = 150;

    Eigen::Vector3f color = payload.color; 
    Eigen::Vector3f point = payload.view_pos;
    Eigen::Vector3f normal = payload.normal;

    float kh = 0.2, kn = 0.1;
    
    // TODO: Implement displacement mapping here
    float x = normal.x(), y = normal.y(), z = normal.z();
    Vector3f t = Eigen::Vector3f(x * y / sqrt(x * x + z * z), sqrt(x * x + z * z), z * y / sqrt(x * x + z * z));
    Vector3f b = normal.cross(t);
    float h = payload.texture->height;
    float w = payload.texture->width;
    float u = payload.tex_coords.x();
    float v = payload.tex_coords.y();
    Eigen::Matrix3f TBN;
    TBN << t.x(), b.x(), normal.x(),
        t.y(), b.y(), normal.y(),
        t.z(), b.z(), normal.z();

    float dU = 0, dV = 0;
    if (payload.texture && is_valid_uv(u, v)) {
        dU = kh * kn * (payload.texture->getColor(u + 1.0f / w, v).norm() - payload.texture->getColor(u, v).norm());
        dV = kh * kn * (payload.texture->getColor(u, v + 1.0f / h).norm() - payload.texture->getColor(u, v).norm());
        point += kn * normal * (payload.texture->getColor(u, v).norm());
    }

    Vector3f ln = Eigen::Vector3f(-dU, -dV, 1);
    normal = (TBN * ln);
    Eigen::Vector3f result_color = {0, 0, 0};

    for (auto& light : lights)
    {
        // TODO: For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular* 
        // components are. Then, accumulate that result on the *result_color* object.
        Vector3f l = (light.position - point);
        Vector3f v = eye_pos - point;
        float r_square = l.dot(l);
        // 計算 ambient 環境光
        Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
        // 計算 diffuse 漫反射光 
        float ndotl = normal.normalized().dot(l.normalized());
        Vector3f diffuse = kd.cwiseProduct(light.intensity / r_square * std::max(0.f, ndotl));
        // 計算 specular 高光 
        Vector3f h = l.normalized() + v.normalized();
        float ndoth = normal.normalized().dot(h.normalized());
        Vector3f specular = ks.cwiseProduct(light.intensity / r_square) * pow(std::max(0.f, ndoth), p);

        result_color += (ambient + diffuse + specular);
    }

    return result_color * 255.f;
}

提高項 - 雙線性紋理採樣

這裏主要是針對紋理過大,多個像素使用同個紋素的問題,雙線性紋理採樣後可以讓被放大的紋理更加平滑,做法如下:

image.png|525

簡單做插值即可。

    Eigen::Vector3f getColorBilinear(float u, float v)
    {
        auto u_img = u * width;
        auto v_img = (1 - v) * height;
        float u11 = ceil(u_img);
        float v11 = ceil(v_img);
        float u01 = floor(u_img);
        float v01 = floor(v_img);
        auto color1 = image_data.at<cv::Vec3b>(v01, u11);
        auto color2 = image_data.at<cv::Vec3b>(v01, u01);
        auto color3 = image_data.at<cv::Vec3b>(v11, u11);
        auto color4 = image_data.at<cv::Vec3b>(v11, u01);
        float s = (u_img - u01) / (u11 - u01);
        float t = (v_img - v01) / (v11 - v01);
        auto color5 = color4 + s * (color3 - color4);
        auto color6 = color2 + s * (color1 - color2);
        auto final_color = color6 + t * (color5 - color6);
        return Eigen::Vector3f(final_color[0], final_color[1], final_color[2]);
    }

作業 4 - 貝塞爾曲線

要求

Bézier 曲線是一種用於計算機圖形學的參數曲線。在本次作業中,你需要實現 de Casteljau 算法來繪製由 4 個控制點表示的 Bézier 曲線 (當你正確實現該算法時,你可以支持繪製由更多點來控制的 Bézier 曲線)。
你需要修改的函數在提供的 main.cpp 文件中。
• bezier:該函數實現繪製 Bézier 曲線的功能。它使用一個控制點序列和一個 OpenCV::Mat 對象作爲輸入,沒有返回值。它會使 t 在 0 到 1 的範圍內進行迭代,並在每次迭代中使 t 增加一個微小值。對於每個需要計算的 t,將調用另一個函數 recursive_bezier,然後該函數將返回在 Bézier 曲線上 t 處的點。最後,將返回的點繪製 OpenCV ::Mat 對象上。
• recursive_bezier:該函數使用一個控制點序列和一個浮點數 t 作爲輸入,實現 de Casteljau 算法來返回 Bézier 曲線上對應點的座標

思路

總體比較簡單,利用課程上講述的遞歸算法實現即可

image.png|525

利用給定的控制點,遞歸計算可以得到下一組控制點,直到控制點的數量爲 1 ,即可返回。

cv::Point2f recursive_bezier(const std::vector<cv::Point2f>& control_points, float t)
{
    // TODO: Implement de Casteljau's algorithm
    if (control_points.size() == 1) {
        return control_points[0];
    }

    std::vector<cv::Point2f> new_control_points;
    for (int i = 0; i < control_points.size() - 1; i++) {
        new_control_points.push_back(control_points[i] * t + control_points[i + 1] * (1 - t));
    }
    return recursive_bezier(new_control_points, t);
}

void bezier(const std::vector<cv::Point2f>& control_points, cv::Mat& window)
{
    // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's 
    // recursive Bezier algorithm.
    for (double t = 0.0; t <= 1.0; t += 0.001)
    {
        auto point = recursive_bezier(control_points, t);
		window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
    }
}

提高項

要求對曲線做抗鋸齒,原先的曲線長這樣

image.png

要求中提示可以直接根據距離做着色,一開始選擇用返回的控制點周圍的 2x2 像素,發現效果不是很好,最終選擇 3x3,控制點的 x,y 座標進行 round 操作取到九宮格中心點,控制點離像素點的最遠距離爲 1.5 * \(\sqrt2\) ,因爲最遠的情況可能爲左上角的像素中心點到控制點像素的右下角,一個像素的對角線長度爲 \(\sqrt2\) ,所以是 \(1.5 * \sqrt2\) 。所以要做的就是遍歷 9 個像素,取到每個像素的中心點與控制點的距離,然後投影到 255 ~ 0 的 RGB 區間。

cv::Point2f recursive_bezier(const std::vector<cv::Point2f>& control_points, float t)
{
    // TODO: Implement de Casteljau's algorithm
    if (control_points.size() == 1) {
        return control_points[0];
    }

    std::vector<cv::Point2f> new_control_points;
    for (int i = 0; i < control_points.size() - 1; i++) {
        new_control_points.push_back(control_points[i] * t + control_points[i + 1] * (1 - t));
    }
    return recursive_bezier(new_control_points, t);
}

void bezier(const std::vector<cv::Point2f>& control_points, cv::Mat& window)
{
    // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau's 
    // recursive Bezier algorithm.
    float max_dist = 1.5f * sqrt(2);
    for (double t = 0.0; t <= 1.0; t += 0.001)
    {
        auto point = recursive_bezier(control_points, t);
        for (int i = round(point.x) - 1; i < round(point.x) + 2; i++) {
            for (int j = round(point.y) - 1; j < round(point.y) + 2; j++) {
                auto sample_point = cv::Point2f(i, j);
                float d = cv::norm(sample_point - point);
                std::cout << d << std::endl;
                float rgb = (1.0f - (d / max_dist)) * 255.0f;
                float old_rgb = window.at<cv::Vec3b>(sample_point.y, sample_point.x)[1];
                window.at<cv::Vec3b>(sample_point.y, sample_point.x)[1] = old_rgb < rgb ? rgb : old_rgb;
            }
        }
    }
}

修改後的效果如下;

image.png

作業 5 - 光線與三角形相交

要求

在這部分的課程中,我們將專注於使用光線追蹤來渲染圖像。在光線追蹤中最重要的操作之一就是找到光線與物體的交點。一旦找到光線與物體的交點,就可以執行着色並返回像素顏色。在這次作業中,我們需要實現兩個部分:光線的生成和光線與三角的相交。本次代碼框架的工作流程爲:

  1. 從 main 函數開始。我們定義場景的參數,添加物體(球體或三角形)到場景中,並設置其材質,然後將光源添加到場景中。
  2. 調用 Render (scene) 函數。在遍歷所有像素的循環裏,生成對應的光線並將返回的顏色保存在幀緩衝區(framebuffer)中。在渲染過程結束後,幀緩衝區中的信息將被保存爲圖像。
  3. 在生成像素對應的光線後,我們調用 CastRay 函數,該函數調用 trace 來查詢光線與場景中最近的對象的交點。
  4. 然後,我們在此交點執行着色。我們設置了三種不同的着色情況,並且已經爲你提供了代碼。
    你需要修改的函數是:
    • Renderer. Cpp 中的 Render ():這裏你需要爲每個像素生成一條對應的光線,然後調用函數 castRay () 來得到顏色,最後將顏色存儲在幀緩衝區的相應像素中。
    • Triangle. Hpp 中的 rayTriangleIntersect (): v 0, v 1, v 2 是三角形的三個頂點,orig 是光線的起點,dir 是光線單位化的方向向量。Tnear, u, v 是你需要使用我們課上推導的 Moller-Trumbore 算法來更新的參數。

思路

實現主要分兩步:

  • 對每個像素打射線
  • 檢查射線和三角形是否相交

對每個像素生成射線

課程中沒有講述具體的過程,這裏主要參考如下:

void Renderer::Render(const Scene& scene)
{
	/*省略代碼*/
    std::vector<Vector3f> framebuffer(scene.width * scene.height);
    float scale = std::tan(deg2rad(scene.fov * 0.5f));
    float imageAspectRatio = scene.width / (float)scene.height;

    // Use this variable as the eye position to start your rays.
    float pixel_width = 1.0f;
    float offset = pixel_width * 0.5f;
    for (int j = 0; j < scene.height; ++j)
    {
        for (int i = 0; i < scene.width; ++i)
        {
            // 先歸一化到 (0, 1)
            auto norm_x = ((i * pixel_width) + offset) / scene.width;
            auto norm_y = ((j * pixel_width) + offset) / scene.height;
            // 轉換到 (-1 , 1)
            norm_x = norm_x * 2 - 1;
            norm_y = norm_y * 2 - 1;
            // scale 表示 near 平面拖遠之後,成像平面應該縮放的倍數
            // imageAspectRatio 表示寬高比,因爲不一定是 1:1 ,而前面歸一化成爲了正方形
            float x = norm_x * scale * imageAspectRatio;
            // 最終需要 * -1,因爲光柵化空間座標是從左上角爲 (0,0),正常是左下角爲 (0,0),需要翻轉下 y 軸
            float y = norm_y * scale * -1;
            // TODO: Find the x and y positions of the current pixel to get the direction
            // vector that passes through it.
            // Also, don't forget to multiply both of them with the variable *scale*, and
            // x (horizontal) variable with the *imageAspectRatio*            

            Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
            framebuffer[m++] = castRay(eye_pos, normalize(dir), scene, 0);
        }
        UpdateProgress(j / (float)scene.height);
    }
	/*省略代碼*/
}

判斷三角形與射線是否相交

這裏直接代入公式即可:

image.png

\(b_1\)\(b_2\) 分別表示 u 和 v,也就是三角形的重心座標。

bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
                          const Vector3f& dir, float& tnear, float& u, float& v)
{
    // TODO: Implement this function that tests whether the triangle
        // that's specified bt v0, v1 and v2 intersects with the ray (whose
        // origin is *orig* and direction is *dir*)
        // Also don't forget to update tnear, u and v.
    auto e1 = v1 - v0, e2 = v2 - v0, s = orig - v0;
    auto s1 = crossProduct(dir, e2), s2 = crossProduct(s, e1);

    float t = dotProduct(s2, e2) / dotProduct(s1, e1);
    float b1 = dotProduct(s1, s) / dotProduct(s1, e1);
    float b2 = dotProduct(s2, dir) / dotProduct(s1, e1);

    if (t > 0.0 && b1 >= 0.0 && b2 >= 0.0 && (1 - b1 - b2) >= 0.0)
    {
        tnear = t;
        u = b1;
        v = b2;
        return true;
    }
    return false;
}

作業 6 - BVH 加速

要求

首先,你需要從上一次編程練習中引用以下函數:
Render() in Renderer.cpp: 將你的光線生成過程粘貼到此處,並且按照新框架更新相應調用的格式。
Triangle::getIntersection in Triangle.hpp: 將你的光線-三角形相交函數粘貼到此處,並且按照新框架更新相應相交信息的格式。在本次編程練習中,你需要實現以下函數:
IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 這個函數的作用是判斷包圍盒 BoundingBox 與光線是否相交,你需要按照課程介紹的算法實現求交過程。
getIntersection(BVHBuildNode* node, const Ray ray) in BVH.cpp: 建立 BVH 之後,我們可以用它加速求交過程。該過程遞歸進行,你將在其中調用你實現的 Bounds3::IntersectP

思路

前兩點對 Render 和 Triangle 的修改比較簡單,不多贅述(感覺沒有必要多此一舉讓我們再補充這些代碼),重點在於後面兩點:

光線與 AABB 判斷相交

AABB 包圍盒由 6 個平面構成,將其延申,分爲 3 組平面,分別對應 x,y,z 三根座標軸,然後依次求出每個平面打到光線時的 t 值。

image.png

我們計算 \(o+td\) 中,t 的值表示光線經過多久會打到 AABB,以 x 軸爲例,進入 x 軸平面的計算爲 \(o.x+td.x = x_0\) ,其中只有 t 未知,解的 t 之後得到進入 x 軸平面的時間,然後依次計算得到其他軸的 t,最後比較得到最晚進入的時間。離開包圍盒的時間也是同理,最後計算出最早離開包圍盒的時間。思考下圖:

image.png

代碼如下,其中 invDir 的用意是可以直接拿來做乘法,效率更高,而 dirIsNeg 表示光線的方向,如果在往負方向則比較值的方向也需要取反。

inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir,
                                const std::array<int, 3>& dirIsNeg) const
{
    // invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
    // dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
    // TODO test if ray bound intersects
    float t_x_min = (pMin.x - ray.origin.x) * invDir.x;
    float t_y_min = (pMin.y - ray.origin.y) * invDir.y;
    float t_z_min = (pMin.z - ray.origin.z) * invDir.z;
    float t_x_max = (pMax.x - ray.origin.x) * invDir.x;
    float t_y_max = (pMax.y - ray.origin.y) * invDir.y;
    float t_z_max = (pMax.z - ray.origin.z) * invDir.z;
    if (!dirIsNeg[0]) {
        std::swap(t_x_min, t_x_max);
    }
    if (!dirIsNeg[1]) {
        std::swap(t_y_min, t_y_max);
    }
    if (!dirIsNeg[2]) {
        std::swap(t_z_min, t_z_max);
    }
    float t_enter = std::max(std::max(t_x_min, t_y_min), t_z_min);
    float t_exit = std::min(std::min(t_x_max, t_y_max), t_z_max);
    if (t_enter < t_exit && t_exit >= 0)
        return true;
    else
        return false;
}

BVH 查詢

這個比較簡單,按照課程上的算法來即可:

image.png

Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
    // TODO Traverse the BVH to find intersection
    const std::array<int, 3> dirIsNeg = { int(ray.direction.x > 0), int(ray.direction.y > 0), int(ray.direction.z > 0) };
    if (!node->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg))
    {
        return {};
    }

    if (node->object) {
        return node->object->getIntersection(ray);
    }

    /* 深度優先 */
    auto leftIntersection = getIntersection(node->left, ray);
    auto rightIntersction = getIntersection(node->right, ray);
    /* 
    取最近打到的物體 
    可能出現:
    1. 1 個打到 1 個沒打到
    2. 兩個全命中了物體,檢查命中較近的物體
    */
    return leftIntersection.distance >= rightIntersction.distance ? rightIntersction : leftIntersection;
}

提高項 - SAH

SAH 的介紹 ,原本 BVH 是直接取空間包圍盒最長的軸,對物體按距離排序做二分,而 SAH 主要是基於面積公式和概率論的方式來計算包圍盒。這裏使用分桶的方法:

image.png

代碼段會對每個軸 (x, y, z) 進行操作。

  • 對於每個軸,首先初始化一個桶(bucket):創建一個大小爲B的桶數組。B通常比較小,例如小於32。
  • 然後計算每一個物體p的質心(centroid),看看這個物體落在哪個桶裏。
    • 將物體p的包圍盒與桶b的包圍盒做並集操作,也就是擴展桶b的包圍盒,使其能夠包含物體p。
    • 增加桶b中的物體計數。
  • 對於每個可能的劃分平面(總共有B-1個),使用表面積啓發式(SAH)公式評估其成本。
  • 執行成本最低的劃分(如果找不到有效的劃分,就將當前節點設爲葉子節點)。
    原本需要對所有物體的每一種可能劃分進行評估,現在只需要對B-1個劃分進行評估。因此,分桶方法可以在構建BVH時,有效地降低計算複雜度,提高算法的效率。

image.png

劃分策略:SAH的目標是找到一種空間劃分方式,使得整體花費最小。
假設有 A 個物體被劃分到 x 子節點,B 個物體被劃分到 y 子節點,且假設穿過子節點的概率 p 與該節點的包圍盒大小成正比。那麼,空間劃分的總花費 C 可以近似爲上圖中的公式。
其中, \(S_A\) 、 \(S_B\) 分別表示x、y子節點的表面積, \(S_N\) 表示整個節點的表面積, \(N_A\) 、 \(N_B\) 分別表示 x、y 子節點中的物體數量, \(C_{isect}\)  表示射線與物體相交的計算成本。

BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects)
{
    BVHBuildNode* node = new BVHBuildNode();

    // Compute bounds of all primitives in BVH node
    Bounds3 bounds;
    for (int i = 0; i < objects.size(); ++i)
        bounds = Union(bounds, objects[i]->getBounds());
    if (objects.size() == 1) {
        // Create leaf _BVHBuildNode_
        node->bounds = objects[0]->getBounds();
        node->object = objects[0];
        node->left = nullptr;
        node->right = nullptr;
        return node;
    }
    else if (objects.size() == 2) {
        node->left = recursiveBuild(std::vector{objects[0]});
        node->right = recursiveBuild(std::vector{objects[1]});

        node->bounds = Union(node->left->bounds, node->right->bounds);
        return node;
    }
    else {
        Bounds3 centroidBounds;
        for (int i = 0; i < objects.size(); ++i)
            centroidBounds =
                Union(centroidBounds, objects[i]->getBounds().Centroid());
        int dim = centroidBounds.maxExtent();
        switch (dim) {
        case 0:
            std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
                return f1->getBounds().Centroid().x <
                       f2->getBounds().Centroid().x;
            });
            break;
        case 1:
            std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
                return f1->getBounds().Centroid().y <
                       f2->getBounds().Centroid().y;
            });
            break;
        case 2:
            std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
                return f1->getBounds().Centroid().z <
                       f2->getBounds().Centroid().z;
            });
            break;
        }

        switch (SplitMethod::SAH) {
            case SplitMethod::NAIVE:
            {
                auto beginning = objects.begin();
                auto middling = objects.begin() + (objects.size() / 2);
                auto ending = objects.end();

                auto leftshapes = std::vector<Object*>(beginning, middling);
                auto rightshapes = std::vector<Object*>(middling, ending);

                assert(objects.size() == (leftshapes.size() + rightshapes.size()));

                node->left = recursiveBuild(leftshapes);
                node->right = recursiveBuild(rightshapes);

                node->bounds = Union(node->left->bounds, node->right->bounds);
                break;
            }
            case SplitMethod::SAH:
            {
                // 定義 10 個桶
                float min_cost = std::numeric_limits<float>::infinity();
                const int buckets = 10;
                int suitable_bucket_index = 1;
                for (int i = 1; i <= buckets; i++) {
                    auto beginning = objects.begin();
                    auto middling = objects.begin() + (objects.size() * i / buckets);
                    auto ending = objects.end();

                    auto leftshapes = std::vector<Object*>(beginning, middling);
                    auto rightshapes = std::vector<Object*>(middling, ending);
                    
                    Bounds3 leftbounds, rightbounds;
                    for (auto object : leftshapes) {
                        leftbounds = Union(leftbounds, object->getBounds().Centroid());
                    }

                    for (auto object : rightshapes) {
                        rightbounds = Union(rightbounds, object->getBounds().Centroid());
                    }

                    float SA = leftbounds.SurfaceArea();
                    float SB = rightbounds.SurfaceArea();
                    float cost = 0.125 + (SA * leftshapes.size() + SB * rightshapes.size()) / centroidBounds.SurfaceArea();
                    if (cost < min_cost) {
                        suitable_bucket_index = i; 
                        min_cost = cost;
                    }
                }

                auto beginning = objects.begin();
                auto middling = objects.begin() + (objects.size() * suitable_bucket_index / buckets);
                auto ending = objects.end();

                auto leftshapes = std::vector<Object*>(beginning, middling);
                auto rightshapes = std::vector<Object*>(middling, ending);
                assert(objects.size() == (leftshapes.size() + rightshapes.size()));

                node->left = recursiveBuild(leftshapes);
                node->right = recursiveBuild(rightshapes);

                node->bounds = Union(node->left->bounds, node->right->bounds);
                break;
            }
        }
    }

    return node;
}

作業 7 - Path Tracing

要求

實現路徑追蹤,主要根據如下僞代碼流程:

image.png

注意點:

  • 計算 L_dir 的時候,第二個點乘 ws 需要 * -1,因爲 ws 是 Shadingpoint 到光源的向量,參考下圖

image.png

  • 在 hitPoint 上加上 EPSLON * N,如果不這樣,那麼之後判斷光線是否被遮擋會有問題(會認爲被自己遮擋),然後出現下圖的問題

image.png

  • 計算射線到光源中間有無遮擋的時候不能直接判斷 intersection.happened ,因爲命中光源時也會爲 true,需要判斷命中點是否滿足 hasEmission
    總體流程如下:
  1. 從像素打出射線,檢查射線是否命中,命中則繼續下一步,反之結束
  2. 對光源表面進行採樣,得到一個採樣的交點 Intersection 和光源的 pdf
  3. 檢查光源採樣點和像素射線交點,兩點之間是否有其他物體遮擋,沒有遮擋則可計算直接光
  4. 計算俄羅斯輪盤賭概率,如果成功進行下一步
  5. 按照像素射線交點材質的性質,給定像素射線入射方向與交點法向量,用某種分佈採樣一個出射方向,這裏是漫反射
  6. 有了出射方向和交點,得到新的射線,計算是否有命中
  7. 如果命中了非光源,計算新射線命中交點給原來像素射線交點帶來的間接光
  8. 最後將直接光和間接光結合,得到最初命中的位置的顏色
// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Vector3f l_indir{ 0.f };
    Vector3f l_dir{0.f};
    Intersection ray_inter = intersect(ray);
    if (!ray_inter.happened) {
        return l_dir + l_indir;
    }

    float pdf_light;
    Intersection light_inter;
    sampleLight(light_inter, pdf_light);

    Vector3f N = ray_inter.normal;
    Vector3f x = light_inter.coords;
    Vector3f wo = ray.direction;
    Vector3f p = ray_inter.coords;
    Vector3f ws = (x - p).normalized();
    // shoot a ray from p to x
    Ray ray_light_to_p = Ray(p + EPSILON * N, ws);
    auto rpx_inter = intersect(ray_light_to_p);
    Vector3f NN = rpx_inter.normal;
    Material* m = ray_inter.m;
    // if the ray is not blocked in the middle
    if(rpx_inter.happened && rpx_inter.m->hasEmission()){
        l_dir = rpx_inter.m->getEmission() * m->eval(wo, ws, N) * dotProduct(ws, N) \
            * dotProduct(-ws, NN) / (rpx_inter.distance) / pdf_light;
    }

    // Test Russian Roulette with probability RussianRoulette
    if (get_random_float() <= RussianRoulette) {
        Vector3f wi = (m->sample(wo, N)).normalized();
        Ray rpwi(p, wi);
        auto rpwi_inter = intersect(rpwi);
        if (rpwi_inter.happened && !rpwi_inter.m->hasEmission()) {
            l_indir = castRay(rpwi, depth + 1) * m->eval(wo, wi, N) * dotProduct(wi, N) \
                / m->pdf(wo, wi, N) / RussianRoulette;
        }
    }
    return m->getEmission() + l_dir + l_indir;
}

提高項

效率優化

  1. 獲取隨機數的接口中的局部變量頻繁創建耗時較高,可以改爲 static
inline float get_random_float()
{
    static std::random_device dev;
    static std::mt19937 rng(dev());
    static std::uniform_real_distribution<float> dist(0.f, 1.f); // distribution in range [1, 6]

    return dist(rng);
}
  1. 增加多線程,每個像素計算着色具有良好局部性,沒有臨界區,所以將屏幕像素分批分多個線程同時進行渲染即可
// change the spp value to change sample ammount
    int spp = 512;
    int worker_num = 16;
    std::mutex lock;
    std::vector<std::thread> threads(worker_num);
    std::cout << "SPP: " << spp << "\n";
    int height_gap = scene.height / worker_num; // ÐèÒª¿ÉÒÔ±» 8 Õû³ý

    auto worker = [&](int start_height, int length) {
        for (uint32_t j = start_height; j < start_height + length; ++j) {
            for (uint32_t i = 0; i < scene.width; ++i) {
                // generate primary ray direction
                float x = (2 * (i + 0.5) / (float)scene.width - 1) *
                    imageAspectRatio * scale;
                float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

                Vector3f dir = normalize(Vector3f(-x, y, 1));
                int frame_index = j * scene.width + i;
                float dspp = 1.0f / spp;
                for (int k = 0; k < spp; k++) {
                    framebuffer[frame_index] += scene.castRay(Ray(eye_pos, dir), 0) * dspp;
                }
            }
        }
    };
    for (int i = 0; i < worker_num; i++) {
        threads[i] = std::thread(worker, i * height_gap, height_gap);
    }
    for (int i = 0; i < threads.size(); i++) {
        threads[i].join();
    }

這裏原本有個輸出進度的,由於多線程統計進度比較麻煩,直接去掉了
3. 微表面材質的計算,這裏暫時對我來說超綱了,後續再補充...Orz

參考資料

作業 8 - 質點彈簧系統

由於本次作業用到了一些 unix 的接口,所以換到了 ubuntu 環境下開發

要求

構造兩條 Rope,Rope 分別用 顯式/半隱式歐拉法 和 顯式 Verlet ,比較兩者的區別,主要實現內容如下:
• rope.cpp 中的 Rope::rope(...)
• rope.cpp 中的 void Rope::simulateEuler(...)
• rope.cpp 中的 void Rope::simulateVerlet(...)

思路

構造 Rope

首先構造 Rope,Rope 的構造函數如下:

Rope::Rope(Vector2D start, Vector2D end, int num_nodes, float node_mass, float k, vector<int> pinned_nodes)

主要就是繩子的兩端座標,質點數量,質點的質量,還有胡克定律的係數 k,pinned_nodes 表示對應的質點是不動的,不然就會出現繩子直接掉下去的情況,需要有一個固定點,默認框架中 pinned_nodes 指定了第一個質點爲固定點。
因此思路比較簡單,就是遍歷 num_nodes 次,每次構造一個質點,每兩個質點 Mass 構造一根彈簧 Spring

    Rope::Rope(Vector2D start, Vector2D end, int num_nodes, float node_mass, float k, vector<int> pinned_nodes)
    {
        // TODO (Part 1): Create a rope starting at `start`, ending at `end`, and containing `num_nodes` nodes.
        for(int i=0; i<num_nodes; ++i) {
            Vector2D pos = start + (end - start) * ((double)i / ((double)num_nodes - 1.0));          
            masses.push_back(new Mass(pos, node_mass, false));
        }

        for(int i=0; i<num_nodes-1; ++i) {
            springs.push_back(new Spring(masses[i], masses[i+1], k));
        }

		// Comment-in this part when you implement the constructor
       for (auto &i : pinned_nodes) {
           masses[i]->pinned = true;
       }
    }

顯式/半隱式歐拉法

首先根據 \(F=ma\) ,只要計算出質點總受力 \(F\),除以質點的質量 \(m\) 就可以得到質點的速度 \(a\) 。質點的受力分爲兩部分 : 重力 和 胡克定律計算得出的彈簧力。胡克定律如下:

\[F_{b \rightarrow\ a} = -k\frac {b-a}{||b-a||} (||b-a||-l) \]

a b 表示質點座標,l 表示彈簧長度,k 爲胡克定律常數係數。計算出 總受力 再施加給質點即可。

  1. 顯式歐拉法 : \(x(t+1) = x(t) + v(t) * dt\)
    物體下一時刻位置 = 當前時刻位置 + 當前時刻速度 * 時間
  2. 半隱式歐拉法: \(x(t+1) = x(t) + v(t+1) * dt\)
    物體下一時刻位置 = 當前時刻位置 + 下一時刻速度 * 時間,然後利用當前時刻的位置 \(x(t)\) 加上下一時刻的速度 \(v(t+1) * dt\) , \(dt\) 表示時間,就可以得到下一時刻的位置
    最後計算如下:
void Rope::simulateVerlet(float delta_t, Vector2D gravity)
    {
        for (auto &s : springs)
        {
            // TODO (Part 3): Simulate one timestep of the rope using explicit Verlet (solving constraints)
            auto mod_ab = (s->m1->position - s->m2->position).norm();
            s->m1->forces += -s->k * (s->m1->position - s->m2->position) / mod_ab * (mod_ab - s->rest_length);
            s->m2->forces += -s->k * (s->m2->position - s->m1->position) / mod_ab * (mod_ab - s->rest_length); 
        }

        float damping_factor = 0.00005;
        for (auto &m : masses)
        {
            if (!m->pinned)
            {
                Vector2D temp_position = m->position;
                // TODO (Part 3.1): Set the new position of the rope mass
                auto a = m->forces / m->mass + gravity;
                // TODO (Part 4): Add global Verlet damping
                m->position = temp_position + (1 - damping_factor) * (temp_position - m->last_position) + a * delta_t * delta_t;
                m->last_position = temp_position;
            }

            // Reset all forces on each mass
            m->forces = Vector2D(0, 0);
        }
    }

顯式 Verlet

Verlet 是另一種精確求解所有約束的方法。這種方法的優點是隻處理仿真中頂點的位置並且保證四階精度。和歐拉法不同,Verlet 積分按如下的方式來更新下一步位置:

\[x(t+1) = x(t) + [x(t) - x(t-1)] + a(t) * dt * dt \]

對比上一步主要替換質點更新位置的流程

    void Rope::simulateVerlet(float delta_t, Vector2D gravity)
    {
        for (auto &s : springs)
        {
            // TODO (Part 3): Simulate one timestep of the rope using explicit Verlet (solving constraints)
            auto mod_ab = (s->m1->position - s->m2->position).norm();
            s->m1->forces += -s->k * (s->m1->position - s->m2->position) / mod_ab * (mod_ab - s->rest_length);
            s->m2->forces += -s->k * (s->m2->position - s->m1->position) / mod_ab * (mod_ab - s->rest_length); 
        }

        for (auto &m : masses)
        {
            if (!m->pinned)
            {
                Vector2D temp_position = m->position;
                // TODO (Part 3.1): Set the new position of the rope mass
                auto a = m->forces / m->mass + gravity;
                // TODO (Part 4): Add global Verlet damping
                m->position = temp_position + (temp_position - m->last_position) + a * delta_t * delta_t;
                m->last_position = temp_position;
            }

            // Reset all forces on each mass
            m->forces = Vector2D(0, 0);
        }
    }
}

阻尼

增加阻尼係數 float damping_factor = 0.00005f,因爲動能會因摩擦而減小,不可能出現無限跳動的彈簧

\[x(t+1) = x(t) + (1-damping\_factor)*[x(t) - x(t-1)] + a(t) * dt * dt \]

    void Rope::simulateVerlet(float delta_t, Vector2D gravity)
    {
        for (auto &s : springs)
        {
            // TODO (Part 3): Simulate one timestep of the rope using explicit Verlet (solving constraints)
            auto mod_ab = (s->m1->position - s->m2->position).norm();
            s->m1->forces += -s->k * (s->m1->position - s->m2->position) / mod_ab * (mod_ab - s->rest_length);
            s->m2->forces += -s->k * (s->m2->position - s->m1->position) / mod_ab * (mod_ab - s->rest_length); 
        }

        float damping_factor = 0.00005;
        for (auto &m : masses)
        {
            if (!m->pinned)
            {
                Vector2D temp_position = m->position;
                // TODO (Part 3.1): Set the new position of the rope mass
                auto a = m->forces / m->mass + gravity;
                // TODO (Part 4): Add global Verlet damping
                m->position = temp_position + (1 - damping_factor) * (temp_position - m->last_position) + a * delta_t * delta_t;
                m->last_position = temp_position;
            }

            // Reset all forces on each mass
            m->forces = Vector2D(0, 0);
        }
    }
}

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章