任务
需要修改的函数是:
Renderer.cpp 中的 Render() :这里你需要为每个像素生成一条对应的光线,然后调用函数 castRay() 来得到颜色,最后将颜色存储在帧缓冲区的相应像素中。
Triangle.hpp 中的 rayTriangleIntersect() : v0, v1, v2 是三角形的三个顶点,orig 是光线的起点, dir 是光线单位化的方向向量。 tnear, u, v 是你需要使用我们课上推导的 Moller-Trumbore 算法来更新的参数。
实现
Render
在这里我们要做的就是将像素的位置变换成像素在空间的坐标,然后根据像素在空间的坐标和相机的坐标,得到该像素对应的光线,从而实现光线追踪。
一个像素是通过下面的步骤得来的 ,那么假如知道一个像素的位置,我们可以倒着推出其在世界坐标的位置。
①将像素坐标转换到图像坐标。
像素坐标左上角为(0,0),范围是x∈[0,1],y∈[0,1],而图像坐标原点则为正中心,先转化成NDC坐标,坐标范围是x∈[-1,1],y∈[-1,1],再通过宽高比计算出图像的坐标,x∈[-width/2,width/2],y∈[-height/2,height/2],因此我们先计算出像素中心点的图像坐标。
设某一个像素点的坐标为(x0,y0),则
x = (2 * (x0+0.5)/width - 1 )*imageAspectRatio
y = (1 -2*(y0+0.5)/scene.height )
括号内是点在NDC坐标的位置,x乘以宽高比就得到了图像坐标。
②将图像坐标转化为相机坐标
得到了点在图像坐标的位置后,就可以将其转化为相机坐标了,这时候只需要知道该图像与相机的距离,就可以推算出其大小。因为图像离相机无论多远,都会规范化到[-1,1]的NDC坐标上,而第一步就是将其变为NDC坐标,再调整了一下宽高而已,并且经过上面的处理后这里的高总为[-1,1]。和相机的距离可以通过视角的大小的一半的正切值求出。
tan(forY/2) =( height/2 ) / 距离,在这里也就是,1/距离。因此距离就是 1/tan(forY/2),结合上面的推到,可以得到最终的相机坐标系的x,y位置。
float scale = std::tan(deg2rad(scene.fov * 0.5f));
...............
x = (2 * ((float)i+0.5)/scene.width - 1 )*imageAspectRatio*scale;
y = (1.0f -2*((float)j+0.5)/scene.height )*scale;
③将相机坐标转化为世界坐标
其实就是乘以视图矩阵的逆矩阵就好了,该作业框架里,直接将相机放在了世界坐标的原点,所以我们不需要进行此变换。
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;
Vector3f eye_pos(0);
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
float x;
float y;
x = (2 * ((float)i+0.5)/scene.width - 1 )*imageAspectRatio*scale;
y = (1.0f -2*((float)j+0.5)/scene.height )*scale;
Vector3f dir = normalize(Vector3f(x, y, -1));
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
}
UpdateProgress( j / (float)scene.height);
}
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);
}
rayTriangleIntersect
这里直接根据下面的公式代入数据了,过程的推导可以查阅相关教程,计算出结果后,需要先判断t是否大于0,重心坐标的三个值是否都大于0,都大于0说明光线与三角形相交。
cpp
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1, const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{
Vector3f E1 = v1-v0;
Vector3f E2 = v2-v0;
Vector3f S = orig - v0;
Vector3f S1 = crossProduct(dir,E2);
Vector3f S2 = crossProduct(S,E1);
tnear = dotProduct(S2,E2)/dotProduct(S1,E1);
u = dotProduct(S1,S)/dotProduct(S1,E1);
v = dotProduct(S2,dir)/dotProduct(S1,E1);
if(u>=0 && v >= 0 && (1-u-v)>=0 && tnear >= 0){
return true;
}
return false;
}
结果
值得注意的点
在该作业中,对透明的球已经实现了菲涅尔反射的模型。观察渲染的图片里透明球的边缘,可以注意到比较亮,查询代码发现了菲涅尔系数的求解,因此这里提一下菲涅尔反射系数。
cpp
float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
float cosi = clamp(-1, 1, dotProduct(I, N)); //确保光线合法
float etai = 1, etat = ior; //etai是入射介质的折射率,etat是出射物质的折射率
//cosi>0,说明光是从物体内射向空气的,因此交换两个折射率
if (cosi > 0) { std::swap(etai, etat); }
// 利用斯涅尔公式计算出射角的正弦值
float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
// 如果大于1,说明发生了全反射,因此反射系数为1。
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;
}
使用斯涅尔公式求解sint,其实这个高中就学过了。
菲涅尔反射系数的精确求解法
根据公式计算出s和p偏振光的反射系数,因为光源是非偏振光,因此将两个反射系数取平均就能得到最终的反射系数
菲涅尔系数的近似求解法
代码中的反射系数明显采取了精确的求法。在castRay的代码中,可以看到其用武之地
cpp
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);
这里是对应的反射与折射材质(REFLECTION_AND_REFRACTION),使用菲涅尔反射系数,可以真实地分配反射与折射光的强度。