games101 作业4及作业5 详解光线追踪框架

games101 作业4及作业5 详解光线追踪框架

作业4

代码分析

作业四的代码整体比较简单 主要流程就是 通过鼠标事件 获取四个控制点的坐标 然后绘制贝塞尔曲线的内容就由我们来完成

理论分析

贝塞尔曲线的理论就是给定一组控制点 然后不断的在控制点之间进行插值 再在得到的新的插值点之间进行插值 具体过程可以用树形结构来表示:

我们可以使用递归来推导插值得到贝塞尔曲线上的点的位置 也可以使用二项分布多项式来表示:

贝塞尔曲线的优良性质:

1.曲线的起点与终点一定是控制点的第一个点与最后一个点

2.曲线在起点和终点处分别会与第一个和最后一个控制点之间的线段保持切线一致

3.可以通过变换控制点来变换整条曲线,无需重新计算曲线的所有点

4.曲线完全包含在由控制点构成的凸包内部。凸包是指能够完全包围所有控制点的最小凸多边形。因此,贝塞尔曲线不会离开这个多边形的范围。这个特性帮助我们确保曲线的形状在控制点之间得到良好的控制

贝塞尔曲线的抗锯齿 锯齿的来源就是曲线和背景像素过渡的不自然 我们对像素周边的其它四个像素 根据距离 给背景像素一个平均的颜色即可

贝塞尔曲面 就是对一组贝塞尔曲线进行进一步的插值:

实际解决

这里我尽量不改变原来的代码结构 在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);
    }

}

结果展示:

作业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.反射计算:

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

虽然他整体推导有些复杂 但是思路还是对的 我也推了一下没啥问题 就偷个懒

这里作业加入了关于法线 与 入射光线是否在同侧的讨论

如果同侧 点积小于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);

结果展示:

这里下面那个棋盘的正方形 就是一开始初始化的三角形网格 可以看到图中有阴影 也能看出一些折射 反射的现象 后面那个球就是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;
}