games101——作业5


总览

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

  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 算法来更新的参数。

开始编写

在本次作业中,你将使用一个新的代码框架。和之前作业相似的是,你可以
选择在自己电脑的系统或者虚拟机上完成作业。请下载项目的框架代码,并使用以下命令像以前一样构建项目:

$ mkdir build
$ cd build
$ cmake ..
$ make

之后,你就可以使用./Raytracing 来运行代码。现在我们对代码框架中的一
些类做一下概括性的介绍:
• global.hpp:包含了整个框架中会使用的基本函数和变量。
• Vector.hpp: 由于我们不再使用 Eigen 库,因此我们在此处提供了常见的向量操作,例如:dotProductcrossProductnormalize
• Object.hpp: 渲染物体的父类。TriangleSphere 类都是从该类继承的。
• Scene.hpp: 定义要渲染的场景。包括设置参数,物体以及灯光。
• Renderer.hpp: 渲染器类,它实现了所有光线追踪的操作。


代码框架详解

main.cpp

从 main.cpp 入手,首先将场景的屏幕的尺寸为 1280 × 960 1280\times960 1280×960

Scene scene(1280, 960);

然后在场景中的加入了两个球体 sph1sph2,创建时指定其球心坐标以及半径,sph1 的反射类型为漫反射,sph2 的反射类型为反射+折射,ior 为其材质折射率

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));

之后又在场景中加入了两个三角形,或者说是一个由两个三角形组成的矩形,设定其顶点坐标,st坐标,以及反射类型为漫反射

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));

最后渲染场景

Renderer r;
r.Render(scene);

Render

Render 方法中,首先定义了尺度scale与宽高比imageAspectRatio,以及相机位置在 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)

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;

然后对于每一个像素,从相机向像素射出一条射线,这里屏幕要从光栅空间转换到世界空间中,具体说明放在作业代码部分。

对于射出的光线获得的颜色,使用 castRay 获取,获取到的颜色信息保存到 framebuffer 中,最后写入 binary.ppm 文件中

// 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); 

下面具体看 castRay 里的代码

castRay

一开始对 depth 进行比较,这里应该是对光线折射次数的定义(因为Whitted风格的光线追踪考虑光线是不断反射的),在这里场景中的光线折射次数限定为 5 次。然后给颜色初始化为背景颜色

if (depth > scene.maxDepth) {
   return Vector3f(0.0,0.0,0.0);
}
Vector3f hitColor = scene.backgroundColor;

然后使用 trace 判断光线是否与场景中的物体有交点,有交点执行之后的代码,没有交点直接返回背景颜色

if (auto payload = trace(orig, dir, scene.get_objects()); payload)


trace 获得的 payload->tNear 按照光线方程,可以计算出对应的交点坐标 hitPoint

Vector3f hitPoint = orig + dir * payload->tNear;

然后使用 getSurfaceProperties 计算交点所在平面的法向量,以及交点的st坐标(只有三角形有)。

Vector3f N; // normal
Vector2f st; // st coordinates
payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);

对于球体求法向量,就是球中心连向交点就是法向量方向

    void getSurfaceProperties(const Vector3f& P, const Vector3f&, const uint32_t&, const Vector2f&,
                              Vector3f& N, Vector2f&) const override
    {
        N = normalize(P - center);
    }

对于三角形求法向量,就是两个边法向量的叉积

    void getSurfaceProperties(const Vector3f&, const Vector3f&, const uint32_t& index, const Vector2f& uv, Vector3f& N,
                              Vector2f& st) const override
    {
        const Vector3f& v0 = vertices[vertexIndex[index * 3]];
        const Vector3f& v1 = vertices[vertexIndex[index * 3 + 1]];
        const Vector3f& v2 = vertices[vertexIndex[index * 3 + 2]];
        Vector3f e0 = normalize(v1 - v0);
        Vector3f e1 = normalize(v2 - v1);
        N = normalize(crossProduct(e0, e1));
        const Vector2f& st0 = stCoordinates[vertexIndex[index * 3]];
        const Vector2f& st1 = stCoordinates[vertexIndex[index * 3 + 1]];
        const Vector2f& st2 = stCoordinates[vertexIndex[index * 3 + 2]];
        st = st0 * (1 - uv.x - uv.y) + st1 * uv.x + st2 * uv.y;
    }

然后就是根据触碰到的物体的材质,执行不同的算法获得对应颜色

switch (payload->hit_obj->materialType)

REFLECTION_AND_REFRACTION

字面意思,既有反射也有折射,首先使用reflect函数计算反射方向,reflect 函数如下

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

然后使用refract函数计算折射方向,refract 函数如下

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;
    float k = 1 - eta * eta * (1 - cosi * cosi);
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

折射方向推导如下


然后计算反射光线与折射光线的起始点,这里为什么要 ± N ∗ e p s i l o n \pm N*epsilon ±Nepsilon,是因为之后可能会继续判断射线是否与物体有接触,所以要加上或减取一个很小的值,防止有接触到当前点。

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;

然后因为在 Whitted风格的光线追踪模型中 REFLECTION_AND_REFRACTION 的光完全由反射和折射光决定,所以之后再用 castRay 计算出反射颜色reflectionColor与折射颜色refractionColor

Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);

那么涉及到反射与折射,使用菲涅尔项计算出对应的反射比例kr,然后加权出对应的颜色 hitColor

float kr = fresnel(dir, N, payload->hit_obj->ior);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;

fresnel 的对应公式与代码如下

float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;
    if (cosi > 0) {  std::swap(etai, etat); }
    // Compute sini using Snell's law
    float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
    // Total internal reflection
    if (sint >= 1) {
        return 1;
    }
    else {
        float cost = sqrtf(std::max(0.f, 1 - sint * sint));
        cosi = fabsf(cosi);
        float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
        float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
        return (Rs * Rs + Rp * Rp) / 2;
    }
    // As a consequence of the conservation of energy, transmittance is given by:
    // kt = 1 - kr;
}

REFLECTION

这个与上面 REFLECTION_AND_REFRACTION类似,就是没有折射项。

 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(DIFFUSE_AND_GLOSSY)

默认项就是漫反射类型的材质,其使用 Phong 模型计算对应的漫反射项与镜面反射项。其公式如下,这里不使用 L a L_a La 项:

首先判断当前点与光线的连线是否与物体接触(即是否被遮挡住),如果被遮挡住,其 漫反射项 lightAmt 就是 0

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 就是漫反射项*Kd+镜面反射项*Ks,这里使用 evalDiffuseColor(st) 渲染出地板的效果,具体什么原理还未搞懂,欢迎大佬指教

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

trace

trace 就是判断当前射线是否与空间中的物体有交点,如果有交点,返回最近的交点。

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;
}

当然每个物体判断方式各不相同,这里放在作业代码部分进一步说明


作业代码

屏幕映射回世界坐标

世界坐标轴屏幕中心位于 ( 0 , 0 , − 1 ) (0,0,-1) (0,0,1)



因此这里对应获得 xy 的代码为

float x = (2.0f*(float(i)+0.5f)/scene.width-1.0f)*scale*imageAspectRatio;
float y = (1.0f-2.0f*(float(j)+0.5f)/scene.height)*scale;

判断光线与物体的交点

球体

光线与球体交点的过程与公式如下(就是光线方程代入球体方程得出)



其代码如下

bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t&, Vector2f&) const override
    {
        // analytic solution
        Vector3f L = orig - center;
        float a = dotProduct(dir, dir);
        float b = 2 * dotProduct(dir, L);
        float c = dotProduct(L, L) - radius2;
        float t0, t1;
        if (!solveQuadratic(a, b, c, t0, t1))
            return false;
        if (t0 < 0)
            t0 = t1;
        if (t0 < 0)
            return false;
        tnear = t0;

        return true;
    }

三角形

判断光线与三角形是否有交点,一般先判断光线与三角形所在平面是否有交点,在判断交点是否在三角形内部。这里使用的 Möller Trumbore Algorithm可以更快判断光线与三角形交点,其公式如下,具体推导可以看这篇文章
其代码如下,注意最后判断是否在三角形内的条件(前提不能在射线后面,即tnear>0)

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 n = 1.0f/dotProduct(S1, E1);
    Vector3f res(dotProduct(S2,E2),dotProduct(S1,S),dotProduct(S2,dir));
    res = n*res;
    tnear = res.x;
    u = res.y;
    v = res.z;
    if(tnear > 0.f && 1-u-v>=0.f && u>=0.f && v>=0.f)    
        return true;
    else  
        return false;
}

最终的运行结果如下

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
在这部分的课程中,我们将专注于使用光线追踪来渲染图像。在光线追踪中 最重要的操作之一就是找到光线与物体的交点。一旦找到光线与物体的交点,就 可以执行着色并返回像素颜色。在这次作业中,我们需要实现两个部分:光线的 生成和光线与三角的相交。本次代码框架的工作流程为: 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 算法来更新的参数。
### 回答1: games101作业5是一个有趣的编程作业,要求学生使用OpenGL编写一个基础的游戏框架。 首先,我们需要实现一个窗口和一个渲染器。窗口用来显示游戏画面,渲染器负责将图形渲染到窗口上。为了实现这两个功能,我们可以使用OpenGL的库函数来创建窗口和渲染器对象。 接下来,我们需要添加一些基本的游戏元素,比如角色、地图和物体。角色可以是一个可移动的对象,地图可以是一个二维或三维的场景,物体可以是一些可以与角色交互的元素,比如道具或敌人。这些游戏元素可以使用OpenGL的图形绘制函数来创建和渲染。 然后,我们需要处理用户的输入,比如键盘输入和鼠标输入。根据用户的输入,我们可以控制角色的移动或进行其他操作。为了实现这一功能,我们可以使用OpenGL的事件处理函数来监听用户的输入事件。 最后,我们可以添加一些游戏逻辑和交互效果,比如碰撞检测、游戏得分和游戏结束等。这些功能可以通过编写一些自定义的函数来实现,并且可以在每一帧渲染时更新游戏状态。 总之,games101作业5是一个锻炼OpenGL编程技巧的作业。通过完成这个作业,我们可以学习到如何使用OpenGL创建一个基础的游戏框架,并且可以了解到游戏开发中的一些基本概念和技术。 ### 回答2: 游戏101作业5主要涉及游戏中的AI设计和实现。AI(人工智能)在游戏中起着重要的作用,它可以为游戏添加更多的挑战性和可玩性。在作业5中,我们需要设计一个实时策略游戏,并实现其中的AI。 首先,我们需要设计一个游戏世界,并将其划分为地图、单位和资源三个部分。地图是游戏的背景,单位是游戏中的角色,资源是用来发展单位和地图的基础。然后,我们需要设计游戏的规则和目标,确保游戏有明确的胜利条件和失败条件。 接下来,我们需要设计游戏的AI策略。AI策略的目标是使AI角色能够根据当前情况做出正确的决策。这需要采用一定的算法和技术来实现。例如,可以使用路径规划算法来决定单位的行动路线,使用决策树或神经网络来评估当前局势和选择最佳策略。AI还需要考虑游戏的难度和平衡性,确保游戏能够提供足够的挑战同时又不至于过于困难。 最后,我们需要用编程语言来实现游戏和AI。可以使用Python或者其他适合游戏开发的语言来编写游戏的逻辑和AI算法。在实现过程中,需要注意代码的结构和性能,确保游戏的流畅运行和AI的快速响应。 总结来说,游戏101作业5是一个关于游戏AI设计与实现的任务。通过设计游戏世界、制定规则和目标以及实现AI策略,我们可以创建一个具有挑战性和可玩性的实时策略游戏。通过编程语言的实现,我们可以使游戏AI能够根据当前情况做出明智的决策。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值