参考: https://zhuanlan.zhihu.com/p/431092843计算机图形学基础 - 光线追踪 (通过这篇文章可以理解光线追踪算法中屏幕坐标转换为近裁剪平面坐标)
:::tips (1)修改函数1:Renderer.cpp 中的 Render():这里你需要为每个像素生成一条对应的光线,然后调用函数 castRay() 来得到颜色,最后将颜色存储在帧缓冲区的相 应像素中 :::
INFO
思路:这里其实是将屏幕空间中每个像素坐标转化为对应的相机空间中近裁剪平面的坐标。
PS:这里卡了一会,不理解为什么光线追踪的渲染方式中没有计算投影矩阵(场景中物体没有乘以透视投影矩阵),这是因为光线追踪的计算方式与光栅化不同。 光栅化之所以需要进行投影变换,是因为光栅化需要将物体的坐标转换为透视投影后的坐标,再逐像素的判断像素点是否在物体上,从而进行光栅化。 而光线追踪的方式是直接从相机触发向场景中发出光线,判断是否打到物体,直接就是在世界空间中的,物体不需要再进行投影变换。(如下图所示)
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;
// Use this variable as the eye position to start your rays.
Vector3f eye_pos(0);//摄像机位置(0, 0, 0)
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
//屏幕像素(二维)->裁剪空间(摄像机看向+z,使用左手坐标系)->观察空间(摄像机看向-z,使用右手坐标系)
// 观测空间下:generate primary ray direction
//此处的i, j是屏幕像素(二维)坐标
float x = (2 * (i + 0.5f) / (float)(scene.width-1) - 1) * imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5f) / (float)(scene.height-1)) * 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*
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);
}
// 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);
}
推导过程如下:
- 在main()中第一句说明了窗口的分辨率,1280x960。我们希望每一个像素都由它的中心点表示,所以给i,j都加上0.5的偏移量,所以此时像素坐标:
- 然后将屏幕像素映射到[-1,1]2 (规则立方体)。先映射到:由于屏幕坐标是从左上角开始,所以y取负,,**然后再映射到[-1,1]**2 :
- 最后再对应到近裁剪平面(z=-1)上,通过
fov
(视角)和aspect
(宽高比)。(注,这里的宽高比就是屏幕宽高比,之前不理解)
cpp
float x = (2 * (i + 0.5f) / (float)scene.width - 1) * imageAspectRatio * scale;
float y = (1 - 2 * (j + 0.5f) / (float)scene.height) * scale;
INFO
修改函数2:Triangle.hpp 中的 rayTriangleIntersect(): v0, v1, v2 是三角形的三个顶点,orig 是光线的起点,dir 是光线单位化的方向向量。tnear, u, v 是你需要使用我们课上推导的 Moller-Trumbore 算法来更新的参数。
思路就是通过Moller-Trumbore 算法来计算判断光线与物体求交:
cpp
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.
auto e1 = v1 - v0, e2 = v2 - v0, s = orig - v0;
auto s1 = crossProduct(dir, e2), s2 = crossProduct(s, e1);
float t = dotProduct(s2, e2) / dotProduct(s1, e1);
float b1 = dotProduct(s1, s) / dotProduct(s1, e1);
float b2 = dotProduct(s2, dir) / dotProduct(s1, e1);
if (t > 0.0 && b1 >= 0.0 && b2 >= 0.0 && (1 - b1 - b2) >= 0.0)
{
tnear = t;
u = b1;
v = b2;
return true;
}
return false;
}