作业要求
- Render() in Renderer.cpp: 将你的光线生成过程粘贴到此处,并且按照新框
架更新相应调用的格式。 - Triangle::getIntersection in Triangle.hpp: 将你的光线-三角形相交函数
粘贴到此处,并且按照新框架更新相应相交信息的格式。
在本次编程练习中,你需要实现以下函数: - IntersectP(const Ray& ray, const Vector3f& invDir,
const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的
作用是判断包围盒 BoundingBox 与光线是否相交,你需要按照课程介绍的算
法实现求交过程。 - getIntersection(BVHBuildNode* node, const Ray ray)in BVH.cpp: 建
立 BVH 之后,我们可以用它加速求交过程。该过程递归进行,你将在其中调
用你实现的 Bounds3::IntersectP.
具体实现
- Render()函数,与之前不同的是,在这里定义了一个光线Ray类,所以在算出来光线的方向之后需要生成一个Ray类的对象,其中光线的原点就是相机的位置,具体代码如下:
void Renderer::Render(const Scene& scene)
{
std::vector<Vector3f> framebuffer(scene.width * scene.height);
float scale = tan(deg2rad(scene.fov * 0.5));
float imageAspectRatio = scene.width / (float)scene.height;
Vector3f eye_pos(-1, 5, 10);
int m = 0;
for (uint32_t j = 0; j < scene.height; ++j) {
for (uint32_t 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*
// Don't forget to normalize this direction!
Vector3f dir = normalize(Vector3f(x, y, -1));
Ray r(eye_pos,dir);
framebuffer[m++] = scene.castRay(r, 0);
}
UpdateProgress(j / (float)scene.height);
}
UpdateProgress(1.f);
// 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] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].x));
color[1] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].y));
color[2] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].z));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}
- Triangle::getIntersection()函数,与上个作业不同的是,在这里把交点的信息封装成了一个Intersection对象,所以在这 之前需要熟悉一下Intersection类里面的信息,如下所示。
struct Intersection
{
Intersection(){
happened=false;
coords=Vector3f();
normal=Vector3f();
distance= std::numeric_limits<double>::max();
obj =nullptr;
m=nullptr;
}
bool happened; //表示光线是否与物体相交
Vector3f coords; //相交的交点坐标
Vector3f normal; //相交点所在平面的法向量
double distance; //相交点距离原点也就是相机的距离
Object* obj; //相交物体的类型,通过给的代码可以看出,这些类型主要有Sphere,Triangle,MeshTriangle
Material* m; //相交物体的材质
};
具体实现如下:
inline Intersection Triangle::getIntersection(Ray ray)
{
Intersection inter;
if (dotProduct(ray.direction, normal) > 0)
return inter;
double u, v, t_tmp = 0;
Vector3f pvec = crossProduct(ray.direction, e2);
double det = dotProduct(e1, pvec);
if (fabs(det) < EPSILON)
return inter;
double det_inv = 1. / det;
Vector3f tvec = ray.origin - v0;
u = dotProduct(tvec, pvec) * det_inv;
if (u < 0 || u > 1)
return inter;
Vector3f qvec = crossProduct(tvec, e1);
v = dotProduct(ray.direction, qvec) * det_inv;
if (v < 0 || u + v > 1)
return inter;
t_tmp = dotProduct(e2, qvec) * det_inv;
// TODO find ray triangle intersection
if(t_tmp < 0) //t小于0没有交点
return inter;
Vector3f tmp = ray.origin + t_tmp*ray.direction;
inter.happened = true;
inter.coords = tmp;
inter.distance = sqrtf(dotProduct(t_tmp*ray.direction,t_tmp*ray.direction));
inter.normal = this->normal;
inter.m = this->m;
inter.obj = this;
return inter;
}
- Bounds3::IntersectP()函数,该函数是判断光线是否与包围盒相交,需要知道课上讲的实现过程以及这个函数所给参数意义
- 原理
我们以2D AABB为例子,因此只有x,y两对平面,3D情况可类推:
首先如上图最左边所示,求出光线与x平面的交点,将先进入的交点(偏小的那个)记为 tmin, 后出去的交点(偏大的那个)记为 tmax,紧接着如中间图所示计算出光线与y平面的两个交点同样记为另外一组tmin, tmax,当然计算的过程中要注意如果任意的 t < 0,那么这代表的是光线反向传播与对应平面的交点。找出tmin中的较大值记为tenter,tmax中的较小值记为texit。接着还需要继续判断tenter和texit之间的关系才能确定光线和包围盒是否相交,如下图所示:
- 参数意义:ray就是光线;invDir表示一个向量,这个向量和ray中direction向量有关系,如果direction是(x,y,z),那么invDir就是(1.0/x,1.0/y,1.0/z),方便后面计算光线与平面的交点;dirIsNeg表示光线的方向;
- 计算光线与平面的交点
知道原理和参数意义之后就剩下计算问题了,具体方法如下
式子中的Ox,dx都是知道的,其中dx可以用invDir来代替就可以写成乘法的形式。现在只剩Px不知道,我们来看Bounds3这个类,它为我们提供了pMin,pMax两个三维向量,存储的就是包围盒的两个顶点,pMin表示包围盒x,y,z值最小的顶点,pMax表示包围盒x,y,z值最大的顶点。把pMin里面的x值换成Px相当于求出光线到达包围盒最前面那个面的时间,pMax里面的x值换成Px相当于求出光线到达包围盒最后面那个面的时间,依次就可以求出光线到达最左最右面,最上最下面的时间。代码如下:
- 原理
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir,
const std::array<int, 3>& dirIsNeg) const
{
// invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
// dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
// TODO test if ray bound intersects
float txmin = (pMin.x - ray.origin.x) * invDir.x;
float txmax = (pMax.x - ray.origin.x) * invDir.x;
float tymin = (pMin.y - ray.origin.y) * invDir.y;
float tymax = (pMax.y - ray.origin.y) * invDir.y;
float tzmin = (pMin.z - ray.origin.z) * invDir.z;
float tzmax = (pMax.z - ray.origin.z) * invDir.z;
//dirIsNeg表示光线的方向,如果是负的则为0,正的则为1
if(!dirIsNeg[0])
std::swap(txmin,txmax);
if(!dirIsNeg[1])
std::swap(tymin,tymax);
if(!dirIsNeg[2])
std::swap(tzmin,tzmax);
float tenter = std::max(std::max(txmin,tymin),txmin);
float texit = std::min(std::min(txmax,tymax),tzmax);
if(texit >= 0.0 && texit > tenter)
return true;
return false;
}
- BVHAccel::getIntersection()函数,该函数返回光线与包围盒内物体的交点信息,大致过程如下
- 光线是否与包围盒相交,不相交直接返回
- 相交的话判断是不是叶子结点,如果是就调用该节点中物体的getIntersection()得到交点信息;(根据函数上面BVHAccel::recursiveBuild()函数可知,叶子结点里面只有一个物体)
- 不是叶子结点就继续递归下去,找出光线与两个子树的交点信息,返回距离最近的
代码如下:
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
// TODO Traverse the BVH to find intersection
std::array<int, 3> dirIsNeg;
dirIsNeg[0] = (ray.direction[0]>0);
dirIsNeg[1] = (ray.direction[1]>0);
dirIsNeg[2] = (ray.direction[2]>0);
Intersection inter,interLeft,interRight;
if(!node->bounds.IntersectP(ray,ray.direction_inv,dirIsNeg)) //没有交点
return inter;
if(node->left == nullptr && node->right == nullptr) //是叶子结点
{
inter = node->object->getIntersection(ray);
return inter;
}
//不是叶子结点
interLeft = getIntersection(node->left,ray);
interRight = getIntersection(node->right,ray);
if(interLeft.distance < interRight.distance) return interLeft;
else return interRight;
}