前言

GAMES101作业系列为GAMES101作业记录(包含作业1-8),文章中会解释作业内容并附上代码,同时对代码框架进行简要说明。
已完成的代码可在我的GitHubGAMES101作业代码仓库获取。

GAMES101课程相关基础知识可参看本博客GAMES101知识梳理系列


作业描述

本次作业的任务是完成光线追踪中光线的生成和光线与三角形求交的算法。
主要工作如下:

  • Render() in Renderer.cpp:为每个像素生成一条对应的光线,然后调用函数castRay()来得到颜色,最后将颜色存储在帧缓冲区的相应像素中。
  • rayTriangleIntersect() in Triangle.hpp:实现Moller-Trumbore算法。

完成作业

Renderer

// in Renderer.cpp
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.
    Vector3f eye_pos(0);
    int m = 0;
    for (int j = 0; j < scene.height; ++j)
    {
        for (int i = 0; i < scene.width; ++i)
        {
            // generate primary ray direction
            float x;
            float y;
            // 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*
            x = 2 * scale * imageAspectRatio / scene.width * (i + 0.5f) - scale * imageAspectRatio;
            y = -2 * scale / scene.height * (j + 0.5f) + scale;
            Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
            dir = normalize(dir);
            framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
        }
        UpdateProgress(j / (float)scene.height);
    }

    // save framebuffer to file
    FILE* fp = fopen("binary.ppm", "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);    
}

从作业代码框架中可知,我们的成像平面位于z=-1。
框架中已经根据scene中定义的fov和屏幕宽高帮我们计算好scaleimageAspectRatio,利用其就可计算出从相机到对应像素中心的连线的x,y分量。
特别注意

  1. 由于利用像素中心进行计算,记得将i,j加0.5;
  2. 像素从左上到右下横向遍历,屏幕中在z轴上,计算x,y时记得进行半个屏幕宽高的平移;
  3. 计算出的dir(x,y,-1)就相当于从相机到屏幕像素的连线,而光线方向则需要对dir再进行归一化;

Triangle

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, E2, S, S1, S2, result1, result;
    E1 = v1 - v0;
    E2 = v2 - v0;
    S = orig - v0;
    S1 = crossProduct(dir, E2);
    S2 = crossProduct(S, E1);
    result1 = Vector3f(dotProduct(S2, E2), dotProduct(S1, S), dotProduct(S2, dir));
    result = result1 / dotProduct(S1, E1);
    tnear = result.x;
    u = result.y;
    v = result.z;
    if (tnear > 0 && u > 0 && v > 0 && (1 - u - v) > 0)
    {
        return true;
    }
    return false;
}

这部分其实没什么可说的,就是套Möller Trumbore公式。需要注意的就是,最后记得判断一下有效性,首先tnear必须大于0,这样保证三角形是在光线前方,再者必须保证u,v(对应课程中公式里重心坐标表示的b1,b2b_1,b_2)在0-1之间,保证交点在三角形内。

效果

Result


代码框架说明

本次作业的说明文档给出了不少框架的说明,对于完成作业来说已经基本上足够,这里仅做简要补充。

// in Document
global.hpp:包含了整个框架中会使用的基本函数和变量。
Vector.hpp: 由于我们不再使用 Eigen 库,因此我们在此处提供了常见的向量操作,例如:dotProduct,crossProduct,normalize。
Object.hpp: 渲染物体的父类。Triangle 和 Sphere 类都是从该类继承的。
Scene.hpp: 定义要渲染的场景。包括设置参数,物体以及灯光。
Renderer.hpp: 渲染器类,它实现了所有光线追踪的操作。

Renderer

渲染器类中的Render成员函数控制整个光线追踪渲染的流程,根据Scene类中定义的屏幕信息逐像素生成光线,然后调用castRay函数完成光线追踪与着色,将结果存入帧缓冲区,绘制出图像。

castRay

Vector3f castRay(
        const Vector3f &orig, const Vector3f &dir, const Scene& scene,
        int depth)
{
    if (depth > scene.maxDepth) {
        return Vector3f(0.0,0.0,0.0);
    }

    Vector3f hitColor = scene.backgroundColor;
    if (auto payload = trace(orig, dir, scene.get_objects()); payload)
    {
        Vector3f hitPoint = orig + dir * payload->tNear;
        Vector3f N; // normal
        Vector2f st; // st coordinates
        payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);
        switch (payload->hit_obj->materialType) {
            ...
        }
    }

    return hitColor;
}

castRay函数首先调用trace函数进行求交计算,trace函数中根据光线与物体信息,调用相应的intersect函数进行求交,即与三角形或球面求交,具体方法如课程中所述。

然后根据求交的结果算出交点,获取交点的表面信息,用于计算着色结果和光线进一步反射或折射的方向。

switch (payload->hit_obj->materialType) {
    case REFLECTION_AND_REFRACTION:
    {
        Vector3f reflectionDirection = normalize(reflect(dir, N));
        Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
        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;
        Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
        Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
        float kr = fresnel(dir, N, payload->hit_obj->ior);
        hitColor = reflectionColor * kr + refractionColor * (1 - kr);
        break;
    }
    case REFLECTION:
    {
        float kr = fresnel(dir, N, payload->hit_obj->ior);
        Vector3f reflectionDirection = reflect(dir, N);
        Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                        hitPoint + N * scene.epsilon :
                                        hitPoint - N * scene.epsilon;
        hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
        break;
    }
    default:
    {
        // [comment]
        // We use the Phong illumation model int the default case. The phong model
        // is composed of a diffuse and a specular reflection component.
        // [/comment]
        Vector3f lightAmt = 0, specularColor = 0;
        Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                                    hitPoint + N * scene.epsilon :
                                    hitPoint - N * scene.epsilon;
        // [comment]
        // Loop over all lights in the scene and sum their contribution up
        // We also apply the lambert cosine law
        // [/comment]
        for (auto& light : scene.get_lights()) {
            Vector3f lightDir = light->position - hitPoint;
            // square of the distance between hitPoint and the light
            float lightDistance2 = dotProduct(lightDir, lightDir);
            lightDir = normalize(lightDir);
            float LdotN = std::max(0.f, dotProduct(lightDir, N));
            // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
            auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
            bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

            lightAmt += inShadow ? 0 : light->intensity * LdotN;
            Vector3f reflectionDirection = reflect(-lightDir, N);

            specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
                payload->hit_obj->specularExponent) * light->intensity;
        }

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

switch语句就是根据交点所在表面的材质(REFLECTION_AND_REFRACTION,REFLECTION,DIFFUSE_AND_GLOSSY)分成反射与折射、反射、漫反射与高光三种情况进行对应的着色计算。

case REFLECTION_AND_REFRACTION:
{
    Vector3f reflectionDirection = normalize(reflect(dir, N));
    Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
    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;
    Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
    Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
    float kr = fresnel(dir, N, payload->hit_obj->ior);
    hitColor = reflectionColor * kr + refractionColor * (1 - kr);
    break;
}         

对于REFLECTION_AND_REFRACTION,即存在反射也存在折射,根据反射与折射的定理计算其反射光线与折射光线,对于每条新光线再次调用castRay计算其相交与着色,其中每次调用castRay都将depth加1,当depth大于Scene设定的最大次数时就不再追踪(其中代码框架默认设置为5次)。
REFLECTION_AND_REFRACTION情况下,其最终着色结果实际上是对各次新光线追踪着色结果的加权,其中每次折射与反射的比例与强度利用fresnel方程计算。

case REFLECTION:
{
    float kr = fresnel(dir, N, payload->hit_obj->ior);
    Vector3f reflectionDirection = reflect(dir, N);
    Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                    hitPoint + N * scene.epsilon :
                                    hitPoint - N * scene.epsilon;
    hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
    break;
}

对于REFLECTION与上述REFLECTION_AND_REFRACTION类似,只是没有折射只有反射,就是根据反射定理计算反射光线再调用castRay进行追踪,其反射光线强度也是利用fresnel方程计算,最终着色结果就是各次反射光线着色的结果的加权。

default:
{
    // [comment]
    // We use the Phong illumation model int the default case. The phong model
    // is composed of a diffuse and a specular reflection component.
    // [/comment]
    Vector3f lightAmt = 0, specularColor = 0;
    Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                                hitPoint + N * scene.epsilon :
                                hitPoint - N * scene.epsilon;
    // [comment]
    // Loop over all lights in the scene and sum their contribution up
    // We also apply the lambert cosine law
    // [/comment]
    for (auto& light : scene.get_lights()) {
        Vector3f lightDir = light->position - hitPoint;
        // square of the distance between hitPoint and the light
        float lightDistance2 = dotProduct(lightDir, lightDir);
        lightDir = normalize(lightDir);
        float LdotN = std::max(0.f, dotProduct(lightDir, N));
        // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
        auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
        bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

        lightAmt += inShadow ? 0 : light->intensity * LdotN;
        Vector3f reflectionDirection = reflect(-lightDir, N);

        specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
            payload->hit_obj->specularExponent) * light->intensity;
    }

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

默认材质的着色是利用Phong模型进行着色,由于只考虑漫反射与高光项,因此产生的是硬阴影。