games101 作業4及作業5 詳解光線追蹤框架

dyccyber發表於2024-08-16

games101 作業4及作業5 詳解光線追蹤框架

作業4

程式碼分析

作業四的程式碼整體比較簡單 主要流程就是 透過滑鼠事件 獲取四個控制點的座標 然後繪製貝塞爾曲線的內容就由我們來完成

理論分析

貝塞爾曲線的理論就是給定一組控制點 然後不斷的在控制點之間進行插值 再在得到的新的插值點之間進行插值 具體過程可以用樹形結構來表示:
img

我們可以使用遞迴來推導插值得到貝塞爾曲線上的點的位置 也可以使用二項分佈多項式來表示:
img

貝塞爾曲線的優良性質:
img
1.曲線的起點與終點一定是控制點的第一個點與最後一個點
2.曲線在起點和終點處分別會與第一個和最後一個控制點之間的線段保持切線一致
3.可以透過變換控制點來變換整條曲線,無需重新計算曲線的所有點
4.曲線完全包含在由控制點構成的凸包內部。凸包是指能夠完全包圍所有控制點的最小凸多邊形。因此,貝塞爾曲線不會離開這個多邊形的範圍。這個特性幫助我們確保曲線的形狀在控制點之間得到良好的控制

貝塞爾曲線的抗鋸齒 鋸齒的來源就是曲線和背景畫素過渡的不自然 我們對畫素周邊的其它四個畫素 根據距離 給背景畫素一個平均的顏色即可

貝塞爾曲面 就是對一組貝塞爾曲線進行進一步的插值:
img

實際解決

這裡我儘量不改變原來的程式碼結構 在recursive_bezier中進行遞迴 相當於每次遞迴都算出樹形結構中插值得到的一層點 直至遞迴到插值得到的只有一個點返回

cv::Vec3b blendColors(const cv::Vec3b& color1, const cv::Vec3b& color2, float alpha)
{
    return color1 * (1.0 - alpha) + color2 * alpha;
}

void draw_anti_aliased_pixel(cv::Mat& window, cv::Point2f point, cv::Vec3b color)
{
    int x = static_cast<int>(std::floor(point.x));
    int y = static_cast<int>(std::floor(point.y));

    float alpha_x = point.x - x;
    float alpha_y = point.y - y;

    // Blend colors based on distance to pixel center
    window.at<cv::Vec3b>(y, x) = blendColors(window.at<cv::Vec3b>(y, x), color, (1 - alpha_x) * (1 - alpha_y));
    window.at<cv::Vec3b>(y, x + 1) = blendColors(window.at<cv::Vec3b>(y, x + 1), color, alpha_x * (1 - alpha_y));
    window.at<cv::Vec3b>(y + 1, x) = blendColors(window.at<cv::Vec3b>(y + 1, x), color, (1 - alpha_x) * alpha_y);
    window.at<cv::Vec3b>(y + 1, x + 1) = blendColors(window.at<cv::Vec3b>(y + 1, x + 1), color, alpha_x * alpha_y);
}

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

}
void bezier(const std::vector<cv::Point2f>& control_points, cv::Mat &window)
{
    cv::Vec3b color(0, 255, 0); // 綠色
    for (double t = 0.0; t <= 1.0; t += 0.001)
    {
        cv::Point2f point = recursive_bezier(control_points, t);
        //window.at<cv::Vec3b>(point.y, point.x)[1] = 255;
        draw_anti_aliased_pixel(window, point, color);
    }

}

結果展示:

img

img

作業5

程式碼分析

整體的程式碼框架大致如下:
初始化場景 場景中包含物體 燈光等等
場景中會包含整個rast space的大小 fov 相機的視角寬度 背景顏色
maxDepth用於控制光線發生反射折射的次數 我們不能讓光線無限的傳播下去
epsilon 用於防止浮點數精度問題 導致計算和物體交點時計算到了表面的下方 這樣再次反射會又一次和相同的表面相交

class Scene
{
public:
    // setting up options
    int width = 1280;
    int height = 960;
    double fov = 90;
    Vector3f backgroundColor = Vector3f(0.235294, 0.67451, 0.843137);
    int maxDepth = 5;
    float epsilon = 0.00001;

    Scene(int w, int h) : width(w), height(h)
    {}

    void Add(std::unique_ptr<Object> object) { objects.push_back(std::move(object)); }
    void Add(std::unique_ptr<Light> light) { lights.push_back(std::move(light)); }

    [[nodiscard]] const std::vector<std::unique_ptr<Object> >& get_objects() const { return objects; }
    [[nodiscard]] const std::vector<std::unique_ptr<Light> >&  get_lights() const { return lights; }

private:
    // creating the scene (adding objects and lights)
    std::vector<std::unique_ptr<Object> > objects;
    std::vector<std::unique_ptr<Light> > lights;
};

框架中用到了兩種物體 分別是兩個球體 和 三角形網格 這裡的三角形網格是兩個三角形 拼接成的正方形 兩個球體的材質一個是glossy_specular 一個是反射透射材質 三角形網格是glossy_specular材質:

auto sph1 = std::make_unique<Sphere>(Vector3f(-1, 0, -12), 2);
sph1->materialType = DIFFUSE_AND_GLOSSY;
sph1->diffuseColor = Vector3f(0.6, 0.7, 0.8);

auto sph2 = std::make_unique<Sphere>(Vector3f(0.5, -0.5, -8), 1.5);
sph2->ior = 1.5;
sph2->materialType = REFLECTION_AND_REFRACTION;

scene.Add(std::move(sph1));
scene.Add(std::move(sph2));

Vector3f verts[4] = {{-5,-3,-6}, {5,-3,-6}, {5,-3,-16}, {-5,-3,-16}};
uint32_t vertIndex[6] = {0, 1, 3, 1, 2, 3};
Vector2f st[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
auto mesh = std::make_unique<MeshTriangle>(verts, vertIndex, 2, st);
mesh->materialType = DIFFUSE_AND_GLOSSY;

scene.Add(std::move(mesh));
scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 0.5));
scene.Add(std::make_unique<Light>(Vector3f(30, 50, -12), 0.5));    

之後就是投射camera ray 計算每個畫素的顏色
trace用於計算和物體的交點 球體的話就用解析解 三角形網格需要遍歷每個三角形圖元求交點 使用Moller-Trumbore 演算法來計算交點 注意這裡求得交點之後還要判斷是不是最近的:

std::optional<hit_payload> trace(
        const Vector3f &orig, const Vector3f &dir,
        const std::vector<std::unique_ptr<Object> > &objects)
{
    float tNear = kInfinity;
    std::optional<hit_payload> payload;
    for (const auto & object : objects)
    {
        float tNearK = kInfinity;
        uint32_t indexK;
        Vector2f uvK;
        if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)
        {
            payload.emplace();
            payload->hit_obj = object.get();
            payload->tNear = tNearK;
            payload->index = indexK;
            payload->uv = uvK;
            tNear = tNearK;
        }
    }

    return payload;
}

castray中 反射折射材質如果光線彈射的次數的超過我們設定的五次 就會終止遞迴 或者打到了diffuse_glossy材質也會終止
diffuse_glossy材質的著色計算就採用bling-phong模型
這裡程式碼很多 就不貼了 講幾個細節
1.反射計算:
img

Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
    return I - 2 * dotProduct(I, N) * N;
}

簡單的向量運算
2.折射計算
這裡的推導見:https://www.cnblogs.com/night-ride-depart/p/7429618.html
雖然他整體推導有些複雜 但是思路還是對的 我也推了一下沒啥問題 就偷個懶
img
這裡作業加入了關於法線 與 入射光線是否在同側的討論
如果同側 點積小於0 說明是從物體的外部打來的光線
如果異側 點積大於0 說明是從物體的內部打來的光線 這是需要調整我們表面法線的方向,並且光密到光疏也要相應的調整:

Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    Vector3f n = N;
    //根據法線和入射光的位置 進行相應的調整
    if (cosi < 0) { cosi = -cosi; ???} else { std::swap(etai, etat); n= -N; }
    float eta = etai / etat;
    //全反射臨界計算 計算cos’
    float k = 1 - eta * eta * (1 - cosi * cosi);
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

這裡我覺得是不是框架的程式碼有點問題 其實點積算cos都是負的 為什麼在小於0的時候再取負 這樣下面eta * cosi - sqrtf(k)又要反過來 所以其實cosi = -cosi;這句是不需要的?

3.epsilon的使用
我們之前提到epsilon 用於控制hitpoint位置變化 防止再次打到相同表面 這裡也要根據同側還是異側進行一個調整
如果是 反射 同側就是加 異側應該是減 如果是折射 同側就是減 異側應該是加 所以這裡框架寫的是不是有點問題:

Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                           + ? hitPoint - N * scene.epsilon :
                            -? hitPoint + N * scene.epsilon;
Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                             hitPoint - N * scene.epsilon :
                             hitPoint + N * scene.epsilon;

4.shadow的生成
從hitpoint處向光源打一根光線 檢測是否打到物體 並且檢測物體是不是在光源與hitpoint之間(用距離判斷)同時滿足 則該點為陰影:

Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                           hitPoint + N * scene.epsilon :
                           hitPoint - N * scene.epsilon;
auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

結果展示:
img

這裡下面那個棋盤的正方形 就是一開始初始化的三角形網格 可以看到圖中有陰影 也能看出一些折射 反射的現象 後面那個球就是diffuse_glossy材質 上面也能看出一些高光
棋盤格紋理的生成:

 Vector3f evalDiffuseColor(const Vector2f& st) const override
 {
     float scale = 5;
     float pattern = (fmodf(st.x * scale, 1) > 0.5) ^ (fmodf(st.y * scale, 1) > 0.5);
     return lerp(Vector3f(0.815, 0.235, 0.031), Vector3f(0.937, 0.937, 0.231), pattern);
 }

理論分析

本次作業要完成的內容比較簡單
首先是生成camera ray 需要我們將rast space上的二維點轉換成三維點 需要經歷一系列變換 可參考我之前的一篇博文:https://www.cnblogs.com/dyccyber/p/17806284.html 最後乘以 * imageAspectRatio * scale 其實就是轉換到世界空間 對應著我們作業1透視矩陣對xy座標的縮放

然後是計算三角形與光線的相交 就是套用課上的公式 需要注意的是要加入邊界範圍的限制 一個是確定不是光線的反向相交 即tnear>0 一個是重心座標在0-1之間 防止交點在三角形外部

實際解決

for (int j = 0; j < scene.height; ++j)
{
    for (int 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;
        // 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 = normalize(Vector3f(x, y, -1)); // Don't forget to normalize this direction!
        framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
    }
    UpdateProgress(j / (float)scene.height);
}
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.

    Vector3f E1 = v1 - v0;
    Vector3f E2 = v2 - v0;
    Vector3f S = orig - v0;
    Vector3f S1 = crossProduct(dir, E2);
    Vector3f S2 = crossProduct(S, E1);
    float div = dotProduct(S1,E1);
    tnear = dotProduct(S2, E2) / div;
    u = dotProduct(S1, S) / div;
    v = dotProduct(S2, dir) / div;
    //兩個邊界 一個是光線的方向 一個是交點要在三角形內部
    if (tnear >= 0 && u >=0 && u<=1 && v>=0 && v<=1) {
        return true;
    }
    return false;
}

相關文章