图形学Lab2——光线追踪实验(RayTracing)
必做实验2——光线追踪实验
实现光线追踪函数的补充代码,并在最后增加拓展画布的长度、增加了一个球体的绘制、渲染。
实验要求
- 光线追踪Render.cpp
- 三角形求交,使用Moller-Trumbore算法Triangle.cpp
- 交点反射方向计算Render.cpp
- 交点处Phong模型着色Render.cpp
- 加分项:修改材质参数,尝试增加场景中的物体,使用Opengl加载自己的模型并正确实现光线追踪
实验内容介绍
补全代码部分:
- 计算反射方向
reflect(I, N)
给定入射光线方向向量I,法向线方向向量N,要求的是反射的光线的方向向量。
推导的过程,考虑反射的过程是:入射光沿法线的方向分量相反,垂直于法线的方向向量乘二倍。把这两种组合起来就可以了。
Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
// TODO
return 2*dotProduct(I, N) * N - I;
}
- 调用fresnel方程函数,计算反射光线的能量占比
这一个部分我们首先观察原问题中对于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;
}
可以发现调用fresnel方程函数计算时,需要依次传入三个参数:入射光、法线方向、物体材质。fresnel方程函数主要是根据fresnel反射方程与Snell规则而来计算反射光线的能量占比。
所以我们可有:
float kr = fresnel(dir, N, payload->hit_obj->ior);
- 递归透射光线
如果光线产生了透射的效果,那么我们就对透射出来的光线这个部分进行递归求解,采用递归调用CastRay
函数即可,同时用hitColor记录颜色值。
递归调用过程注意最高层数,同时调用的时候层数要depth+1,比如我们设定最高反射/折射层数为5,那么这个递归调用在达到相应的层级之后就会中止运行并返回结果。
hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth+1) * kr;
- 对于材质表面既有反射又有折射的情况下,我们需要调用castRay来分别计算其反射光线与透射光线,具体的调用过程同3,这个地方需要记得区分反射与折射,对于反射和折射这两个过程我们需要将这两者分开,分别计算他们的orig、dir之后再传入。其调用所产生的结果是Vecotr3f类型的变量,所以我们需要提前通过设置变量来存储
Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth+1); // 反射光线
Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth+1); // 透射光线
- 对于局部光照的Phong模型处理。
对于Phong模型,在不考虑环境光的情况下,hitColor(该光线获得的光照) = diffuse(漫反射光) + specular(镜面光)。
对于漫反射部分,我们可以通过以下部分来计算:
float Kd = payload->hit_obj->Kd;
Vector3f diffuseColor = payload->hit_obj->evalDiffuseColor(st);
Vector3f Id = lightAmt;
hitColor_diffuse = (Id * diffuseColor * Kd);
对于镜面光部分,我们可以通过以下计算:
float Ks = payload->hit_obj->Ks;
Vector3f Is = lightSpecular;
Vector3f specularColor(1.0);
hitColor_specular = Is * specularColor * Ks;
最后我们可求得Phong光照模型得出的结果:
hitColor = hitColor_diffuse + hitColor_specular;
- 光线的生成
这一个部分在实验指导ppt中有介绍,主要的部分是对于通过透视矩阵等的计算来得到相应的光线属性值。
落实到代码上时,也就是我们对于scene的height和width分别用j, i遍历时,进行以下操作(不要忘记归一化):
x = ((i + 0.5) * 2 - (float)scene.width) / (float)scene.width * imageAspectRatio * scale;
y = (((float)scene.height - (j + 0.5) * 2) / (float)scene.height) * scale;
z = -1.0;
Vector3f dir = normalize(Vector3f(x, y, z));
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
- 对于Triangle模型之中,我们需要计算光线与三角形求交的部分功能函数功能实现。
在主体函数对于该求交的部分功能函数的调用过程中,其中采取的变量设置是通过临时变量的形式来实现的,也就是说我们在功能函数的设计部分即使是传址调用也可以进行修改。
参考GAMES101课程所讲授的内容,这一部分根据Möller Trumbore Algorithm,可以更快判断光线与三角形交点的计算我们可以将分子分母各自拿出来进行计算。
Vector3f E1, E2, S, S1, S2, re;
E1 = v1 - v0;
E2 = v2 - v0;
S = orig - v0;
S1 = crossProduct(dir, E2);
S2 = crossProduct(S, E1);
float invbottom = 1.0f / dotProduct(S1, E1);
tnear = dotProduct(S2, E2) * invbottom;
u = dotProduct(S1, S) * invbottom;
v = dotProduct(S2, dir) * invbottom;
if (tnear > 0 && u>=0 && v>=0 && (u+v)<=1)
return true;
return false;
- 为了让情景更加多元,我们加入了一个小球,并调整了其颜色参数等;同时更改了画布的大小,更加方便地看出场景内的布局。
auto sph3 = std::make_unique<Sphere>(Vector3f(5, 2, -10), 3);
sph1->materialType = DIFFUSE_AND_GLOSSY;
sph1->diffuseColor = Vector3f(0.8, 0.0, 0.0);
...
scene.Add(std::move(sph3));
...
Vector3f verts[4] = {{-10,-6,-12}, {10,-6,-12}, {10,-6,-32}, {-10,-6,-32}};
实验结果
我们最后实现的效果,是补全了实验要求中的代码,并在其基础上添加了小球,更改了新添加的小球材质为漫反射,半径大小更大;更改了画布的大小,使得更加清晰明了。
补全代码,形成图像效果:
进行更改,修改优化之后图像效果:
实验总结
本次实验主要做了关于光线的实验,了解了RayTracing的基本过程框架,如何绘制图像,光线光路强度等计算调整过程。
同时,如何将课内知识与具体的实践过程相互融合这个命题依然重要。在计算RayTracing的过程中,尤其是最后的光线计算,计算过程有点绕,需要通过代码以及老师上课讲的知识结合学习,有一个方面没学彻底就比较吃力。课上内容是很重要的,将课本知识内容与实践内容相结合的话,就更能深化课本上学习的知识。
遇到的问题主要是:
(1)生成光线的计算问题,自己推导的公式计算有错并且没有归一化,导致最后生成的图片只有蓝色背景色,没有其他信息。
(2)参数的确定,不要设置的太大,可以用二分法来尝试修改参数,在设置新物体的参数时可以仿照老物体设置;
(3)调整编译器的C++版本至适用的版本,在老版本中,某些库是不支持直接引用的。
代码附录部分
Renderer.cpp
#include <fstream>
#include "Vector.hpp"
#include "Renderer.hpp"
#include "Scene.hpp"
#include <optional>
inline float deg2rad(const float °)
{ return deg * M_PI/180.0; }
// Compute reflection direction
Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
// TODO
return 2*dotProduct(I, N) * N - I;
}
// [comment]
// Compute refraction direction using Snell's law
//
// We need to handle with care the two possible situations:
//
// - When the ray is inside the object
//
// - When the ray is outside.
//
// If the ray is outside, you need to make cosi positive cosi = -N.I
//
// If the ray is inside, you need to invert the refractive indices and negate the normal N
// [/comment]
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;
}
// [comment]
// Compute Fresnel equation
//
// \param I is the incident view direction
//
// \param N is the normal at the intersection point
//
// \param ior is the material refractive index
// [/comment]
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;
}
// [comment]
// Returns true if the ray intersects an object, false otherwise.
//
// \param orig is the ray origin
// \param dir is the ray direction
// \param objects is the list of objects the scene contains
// \param[out] tNear contains the distance to the cloesest intersected object.
// \param[out] index stores the index of the intersect triangle if the interesected object is a mesh.
// \param[out] uv stores the u and v barycentric coordinates of the intersected point
// \param[out] *hitObject stores the pointer to the intersected object (used to retrieve material information, etc.)
// \param isShadowRay is it a shadow ray. We can return from the function sooner as soon as we have found a hit.
// [/comment]
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;
}
// [comment]
// Implementation of the Whitted-style light transport algorithm (E [S*] (D|G) L)
//
// This function is the function that compute the color at the intersection point
// of a ray defined by a position and a direction. Note that thus function is recursive (it calls itself).
//
// If the material of the intersected object is either reflective or reflective and refractive,
// then we compute the reflection/refraction direction and cast two new rays into the scene
// by calling the castRay() function recursively. When the surface is transparent, we mix
// the reflection and refraction color using the result of the fresnel equations (it computes
// the amount of reflection and refraction depending on the surface normal, incident view direction
// and surface refractive index).
//
// If the surface is diffuse/glossy we use the Phong illumation model to compute the color
// at the intersection point.
// [/comment]
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;
// 光线和场景所有物体的最近交点
auto payload = trace(orig, dir, scene.get_objects());
if (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) {
// 材质表面只存在反射
case REFLECTION:
{
// TODO 计算菲涅尔方程,获取反射光线的能量占比
float kr = fresnel(dir, N, payload->hit_obj->ior);
// 得到反射方向
Vector3f reflectionDirection = normalize(reflect(dir, N));
// 得到反射光线起点:注意起点在物体内部还是外部很重要
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint + N * scene.epsilon :
hitPoint - N * scene.epsilon;
// TODO 递归投射光线
hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth+1) * kr;
break;
}
// 材质表面既有反射,又有折射,比如透明/半透明材质
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;
// 反射光线递归调用castRay获取追踪结果 TODO
Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth+1);
// 折射光线递归调用castRay获取追踪结果 TODO
Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth+1);
// 结合菲涅尔定律: 反射和折射光各自的能量占多少用kr表示
float kr = fresnel(dir, N, payload->hit_obj->ior);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
}
// 默认使用Phong光照模型: diffuse + specular
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, lightSpecular = 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));
// 判断交点是否在阴影里: 从光源出发,向lightDir方向发出一条光线,如果得到的交点比hitPoint更近, 说明hitPoint在阴影中
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);
// 高光
lightSpecular += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
payload->hit_obj->specularExponent) * light->intensity;
}
// Phong model: Kd * diffuseColor * Id + Ks * specularColor * Is
// 漫反射部分
float Kd = payload->hit_obj->Kd;
Vector3f diffuseColor = payload->hit_obj->evalDiffuseColor(st);
Vector3f Id = lightAmt;
// 光泽反射部分
// 由于反射光一般情况下就是光源颜色,颜色不变,所以这里没有一个evalSpecularColor;
// 请注意:对于金属来说,不同金属对不同波长的光反射率不同, 具有不同的specularColor
float Ks = payload->hit_obj->Ks;
Vector3f Is = lightSpecular;
// Vector3f specularColor(1.0);
// 安装Phong模型公式,补充完整
// hitColor = ?? // TODO;
hitColor = (Id * diffuseColor * Kd) + (Is * Ks);
break;
}
}
}
return hitColor;
}
// [comment]
// The main render function. This where we iterate over all pixels in the image, generate
// primary rays and cast these rays into the scene. The content of the framebuffer is
// saved to a file.
// [/comment]
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;
std::cout << scale << ", " << imageAspectRatio << std::endl;
// 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: 光线的生成
// x = 2 * scale * imageAspectRatio / scene.width * (i + 0.5) - scale * imageAspectRatio;
// y = 2 * scale / scene.height * (j + 0.5) + scale;
x = ((i + 0.5) * 2 - (float)scene.width) / (float)scene.width * imageAspectRatio * scale;
y = (((float)scene.height - (j + 0.5) * 2) / (float)scene.height) * scale;
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);
}
// 保存结果, ppm文件在ubuntu上可以查看,在window上需要转换格式
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);
}
Triangle.hpp
#pragma once
#include "Object.hpp"
#include <cstring>
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{
// 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.
// TODO 光线与三角形求交
Vector3f E1, E2, S, S1, S2, re;
E1 = v1 - v0;
E2 = v2 - v0;
S = orig - v0;
S1 = crossProduct(dir, E2);
S2 = crossProduct(S, E1);
// re = Vector3f(dotProduct(S2, E2), dotProduct(S1, S), dotProduct(S2, dir));
float invbottom = 1.0f / dotProduct(S1, E1);
tnear = dotProduct(S2, E2) * invbottom;
u = dotProduct(S1, S) * invbottom;
v = dotProduct(S2, dir) * invbottom;
if (tnear > 0 && u>=0 && v>=0 && (u+v)<=1)
{
return true;
}
return false;
}
class MeshTriangle : public Object
{
public:
MeshTriangle(const Vector3f* verts, const uint32_t* vertsIndex, const uint32_t& numTris, const Vector2f* st)
{
uint32_t maxIndex = 0;
for (uint32_t i = 0; i < numTris * 3; ++i)
if (vertsIndex[i] > maxIndex)
maxIndex = vertsIndex[i];
maxIndex += 1;
vertices = std::unique_ptr<Vector3f[]>(new Vector3f[maxIndex]);
memcpy(vertices.get(), verts, sizeof(Vector3f) * maxIndex);
vertexIndex = std::unique_ptr<uint32_t[]>(new uint32_t[numTris * 3]);
memcpy(vertexIndex.get(), vertsIndex, sizeof(uint32_t) * numTris * 3);
numTriangles = numTris;
stCoordinates = std::unique_ptr<Vector2f[]>(new Vector2f[maxIndex]);
memcpy(stCoordinates.get(), st, sizeof(Vector2f) * maxIndex);
}
// 是否相交
bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t& index,
Vector2f& uv) const override
{
bool intersect = false;
for (uint32_t k = 0; k < numTriangles; ++k)
{
const Vector3f& v0 = vertices[vertexIndex[k * 3]];
const Vector3f& v1 = vertices[vertexIndex[k * 3 + 1]];
const Vector3f& v2 = vertices[vertexIndex[k * 3 + 2]];
float t, u, v;
if (rayTriangleIntersect(v0, v1, v2, orig, dir, t, u, v) && t < tnear)
{
tnear = t;
uv.x = u;
uv.y = v;
index = k;
intersect |= true;
}
}
return intersect;
}
// 物体表面的相关属性
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);
// normal
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;
}
// 为了实现每隔一定距离,更改一下颜色; 生成结果图片有一个红黄相间的地板
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);
}
std::unique_ptr<Vector3f[]> vertices;
uint32_t numTriangles;
std::unique_ptr<uint32_t[]> vertexIndex;
std::unique_ptr<Vector2f[]> stCoordinates;
};
main.cpp
#include "Scene.hpp"
#include "Sphere.hpp"
#include "Triangle.hpp"
#include "Light.hpp"
#include "Renderer.hpp"
// In the main function of the program, we create the scene (create objects and lights)
// as well as set the options for the render (image width and height, maximum recursion
// depth, field-of-view, etc.). We then call the render function().
int main()
{
// 定义场景
Scene scene(1280, 960);
// 球1
auto sph1 = std::make_unique<Sphere>(Vector3f(-1, 0, -12), 2);
// 球1材质
sph1->materialType = DIFFUSE_AND_GLOSSY;
sph1->diffuseColor = Vector3f(0.2, 0.7, 0.8);
// 球2
auto sph2 = std::make_unique<Sphere>(Vector3f(0.5, -0.5, -8), 1.5);
sph2->ior = 1.5;
sph2->materialType = REFLECTION_AND_REFRACTION;
// Sphere 3
auto sph3 = std::make_unique<Sphere>(Vector3f(5, 2, -10), 3);
// 球1材质
sph1->materialType = DIFFUSE_AND_GLOSSY;
sph1->diffuseColor = Vector3f(0.8, 0.0, 0.0);
// 添加到场景
scene.Add(std::move(sph1));
scene.Add(std::move(sph2));
// scene.Add(std::move(sph3));
// 四个顶点
// Vector3f verts[4] = {{-5,-3,-6}, {5,-3,-6}, {5,-3,-16}, {-5,-3,-16}};
Vector3f verts[4] = {{-10,-6,-12}, {10,-6,-12}, {10,-6,-32}, {-10,-6,-32}};
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);
return 0;
}