GAMES101——作业5 光线与三角形相交(菲涅尔反射率)
任务
实现
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;
③将相机坐标转化为世界坐标
其实就是乘以视图矩阵的逆矩阵就好了,该作业框架里,直接将相机放在了世界坐标的原点,所以我们不需要进行此变换。
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说明光线与三角形相交。
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;
}
结果
值得注意的点
在该作业中,对透明的球已经实现了菲涅尔反射的模型。观察渲染的图片里透明球的边缘,可以注意到比较亮,查询代码发现了菲涅尔系数的求解,因此这里提一下菲涅尔反射系数。
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的代码中,可以看到其用武之地
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),使用菲涅尔反射系数,可以真实地分配反射与折射光的强度。