写在前面

来不及思考下周的复变函数和经济学原理考试了,现在向我走来的是计算机图形学的经典问题:Path tracing了。

作业要求

在这部分的课程中,我们将专注于使用光线追踪来渲染图像。在光线追踪中 最重要的操作之一就是找到光线与物体的交点。一旦找到光线与物体的交点,就 可以执行着色并返回像素颜色。在这次作业中,我们需要实现两个部分:光线的 生成和光线与三角的相交。本次代码框架的工作流程为:

  1. 从 main 函数开始。我们定义场景的参数,添加物体(球体或三角形)到场景 中,并设置其材质,然后将光源添加到场景中。
  2. 调用 Render(scene) 函数。在遍历所有像素的循环里,生成对应的光线并将 返回的颜色保存在帧缓冲区(framebuffer)中。在渲染过程结束后,帧缓冲 区中的信息将被保存为图像。
  3. 在生成像素对应的光线后,我们调用 CastRay 函数,该函数调用 trace 来 查询光线与场景中最近的对象的交点。
  4. 然后,我们在此交点执行着色。我们设置了三种不同的着色情况,并且已经 为你提供了代码。 你需要修改的函数是:
  • Renderer.cpp 中的 Render():这里你需要为每个像素生成一条对应的光 线,然后调用函数 castRay()来得到颜色,最后将颜色存储在帧缓冲区的相 应像素中。
  • Triangle.hpp 中的 rayTriangleIntersect(): v0, v1, v2 是三角形的三个 顶点,orig 是光线的起点,dir 是光线单位化的方向向量。tnear, u, v 是你需要使用我们课上推导的Moller-Trumbore 算法来更新的参数。

知识点回顾: Moller-Trumbore 算法实现

  • 传送门:[Moller-Trumbore 算法实现]((6 封私信 / 80 条消息) [数学] Möller–Trumbore 算法 - 知乎) 回到课程当中,我们来重新复习一下光线相交的相应算法: 这种算法的主要功能是计算三维中射线和三角形求交的快速方法,无需预先计算包含三角形的平面方程;这对于渲染中的光线追踪有着显著的效果。因为我们一般假定光线为射线,而模型的片元在计算阶段多以三角形作为单位进行光线求交以及对应的颜色的计算和处理。 核心思想是:平面->点+法向量;光线射线=点+射线向量

通过这种方式判断光线和三角形对应平面的交点,然后将对应交点和三角形对应三条边同时针方向做叉乘就可以判断光线是否与对应面片相交了。 推导时需要关注克拉默法则以及向量叉乘知识,公式推导方面仅作了解即可。

实现

框架理解

可以发现,此次的工程框架中有不少Comment部分,介绍了一些函数方法的用途,在正式补完方法之前,我们先对这些着重解释的部分进行理解。

  • PS:这期的框架中出现了make_unique等C++11语法,可以看出框架的语言特性变多,更加贴近现代软件工程思想。
  • 流程:
  1. 从 main 函数开始。我们定义场景的参数,添加物体(球体或三角形)到场景 中,并设置其材质,然后将光源添加到场景中。
  2. 调用 Render(scene) 函数。在遍历所有像素的循环里,生成对应的光线并将 返回的颜色保存在帧缓冲区(framebuffer)中。在渲染过程结束后,帧缓冲 区中的信息将被保存为图像。
  3. 在生成像素对应的光线后,我们调用 CastRay 函数,该函数调用 trace 来 查询光线与场景中最近的对象的交点。
  4. 然后,我们在此交点执行着色。我们设置了三种不同的着色情况,并且已经 为你提供了代码。

cast ray()

在框架当中,我们进行这个方法,在进行光线求交的逻辑中,存在switch选项,默认为EFLECTION_AND_REFRACTION。也就是说,在光线打中片元的时候,其实存在一个判断被击中材质做不同处理的流程; 框架中,在默认的材质中已经实现好了一个Bling-Phong模型,从返回项的公式中就可以判断出来:

 hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;

float fresnel()

顾名思义,这个地方实现了光学上的菲涅尔方程,用于计算用于计算光在两种介质交界面上的反射率(Reflectance),即有多少光被反射,多少被折射。 利用这一公式,我们就可以通过实现角度,光线角度,平面法线和介质处折射率等参数得到光线的衰减情况。

实现bool rayTriangleIntersect()

  • 在这个判断中,我们需要判定好在光线求交的过程中,光线和给定片元是否相交。利用的算法是Moller-Trumbore 。
  • 观察给定的参数,其中包括带判断片元三个顶点,视线角度和光线角度,有uv等值。其中前面几个参数和算法Moller-Trumbore完全合理。
  • uv坐标是三角形中的重心坐标部分,即u,v,1-u-v权重于三个坐标点的值就可以得到三角形内部任意一点。(在片元内部的充要条件是三个权重值均位于0-1之间且和为1)
  • tnear属于描述光线上一点的一个参数,用于建模光线交点唯一确定这一点的值。
bool rayTriangleIntersect(const Vector3f& v0,
                          const Vector3f& v1,
                          const Vector3f& v2,
                          const Vector3f& orig,
                          const Vector3f& dir,
                          float& tnear,
                          float& u,
                          float& v)
{
    const float eps = 1e-6f;               // 容忍误差

    //two vector
    Vector3f edge1 = v1 - v0;
    Vector3f edge2 = v2 - v0;
    Vector3f pvec =crossProduct(dir,edge2);
    float det = dotProduct(edge1, pvec);
    //upper function:using this way to get 混合积

    //ensure the light coming from front place
    if (det > -eps && det < eps)
        return false;
    float invDet = 1.0f / det;
    Vector3f tvec = orig - v0;
    u = dotProduct(tvec,pvec) * invDet;
    if (u < 0.0f || u > 1.0f)
        return false;
    Vector3f qvec = crossProduct(tvec,edge1);
    v = dotProduct(dir,qvec) * invDet;
    if (v < 0.0f || u + v > 1.0f)
        return false;
    tnear = dotProduct(edge2,qvec) * invDet;
    return tnear >= eps;
}

补全Render

  • 根据Todo的要求,我们需要把像素坐标变成射线方向,也就是遍历整个像素空间中的像素点,根据像素点的坐标位置,判断摄像机坐标系下的射线方向,也就是每个像素对应的摄影机的视线的方向。
  • 需要完成的逻辑:转换ndc空间中的向量,将t调整好的视线角度,相机方向以及对应屏幕信息传入光线追踪器中,提供着色并返回图像中理应呈现的颜色。
  • 注意传入下一步做求交之类操作的时候必须预先进行其的对应标准化,且我们得到的像素点是离散的网格结构,为其左下角顶点坐标,我们需要做合理的偏移,保证转换后的视线射线穿过的的是像素的中心点。
 for (int j = 0; j < scene.height; ++j)
 {
     for (int i = 0; i < scene.width; ++i)
     {
         // generate primary ray direction
         float x = 0.0f;
         float y = 0.0f;
         // generate primary ray direction
         float ndcX = (2.0f * (i + 0.5f) / scene.width) - 1.0f;
         float ndcY = 1.0f - (2.0f * (j + 0.5f) / scene.height);

         float x = ndcX * imageAspectRatio * scale;
         float y = ndcY * scale;

         Vector3f dir = Vector3f(x, y, -1);
         dir = normalize(dir);
         framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
     }
     UpdateProgress(j / (float)scene.height);
 }

效果

与之前的代码框架不同的是,此次框架中没有前端,因此输出位置需要找到输出部分方法观察。 在Render方法中,下述部分为图像部分。

 std::string outputPath = Utils::PathFromAsset("output/assigment5.ppm");
 FILE* fp = fopen(outputPath.c_str(), "wb");
 (void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
 for (auto i = 0; i < scene.height * scene.width; ++i) {
     static unsigned char color[3];
     color[0] = (char)(255 * clamp(0, 1, framebuffer[i].x));
     color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
     color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
     fwrite(color, 1, 3, fp);
 }
 fclose(fp);    

回到代码框架中打开文件资源管理器寻找输出结果。 完成了!可以发现对于半透明相关的效果以及对应不透明的物体的光线求交效果都不错。 上面的灵魂手绘是拿数位板随便画的,不包括对应的功能23333

后记

这部分的视线难度不是很难,而且没有加入Eigen库就说明需要自己实现的部分很少2333,只需要根据api完成对应的算法实现即可。要去继续学复变函数了,上大学以后天天好累qwq