闲言碎语
emmm,上一次写还是2022年4月份的事情了,真的有点恍如隔世,4月到9月主要是在准备保研的事情,然后10月到12月基本上是在适应实习生活(没错,保完研之后因为种种原因就直接开始实习了,害,好吧,其实基本上就是奢靡的摆烂生活==,因为压力忽然没了,基本就是在公司混日子,没干啥事),就有很多事情本来早就该弄了,但是自己一直拖拖拉拉,最近慢慢适应了工作节奏,就陆陆续续开始搞一些曾经想搞但一直没时间搞的东西,由于本科毕设是做渲染相关的,所以games101渲染部分的作业得好好做一做,争取在年前能够入个门。
好了回归正题,作业5总体来说不是很难,主要是把整个框架看懂花了点时间,实际上敲代码的话是比较容易的,感觉核心还是看懂整个框架,收获的东西相较于作业会更多。
目录
- 闲言碎语
- 框架梳理
- 任务一:生成光线
- 任务二:光线与三角形求交
- 最终效果
- 感悟
- 参考链接
框架梳理
这个作业框架实际上是实现了简单的光线追踪流程(无加速算法),从全局来看就是先往scene里面添加物体(包括物体本身的材质信息和各种属性)和光源,其中物体既可以是显示表示(三角网格),也可以是隐式表示(球,用方程)。添加完之后就调用Render函数进行渲染。以上就是对应图形学两个环节,建模和渲染,如果要让他们动起来的话就是还要加个模拟。
渲染流程大概就是先生成光线(视点与屏幕像素中心点的连线),然后光线与场景相交找到第一个与之相交的点(这里相交的方法是遍历空间中所有物体,依次与光线相交求出相交点,然后找到最近的相交点返回。其中,对于不同物体的求交方法是不一样的,比如对于球这种隐式表示的,就用解析解的方法直接求交点,对于三角网格这种显示表示的,就依次遍历其所有三角形,用线和三角形求交的方法求交点),之后根据各种渲染的方法对该点着色(不同材质的着色方法不一样,代码里面写了三种材质,对应三种不同的着色方法,并且这里还需要判断点是否处在阴影中,若在阴影中,则不需要着色),然后该点的颜色就会最终返回,作为最终渲染图像的一个像素值。射出全部光线就能得到全部像素值,最终得到渲染图像。
由于这次框架比较简单,这次就不一边梳理框架一边看代码了,因为我觉得代码千变万化,不同场景都会有变化,但是光线追踪核心的思想和流程是不变的,只要核心的思想懂了,代码怎么变都是可以看懂的。而且我觉得看框架的代码也是一个自主学习的过程,如果讲的太透,也没有什么意思,还是自主学习更加有趣,也会有更多的收获,我相信只要坚持看下去,每个人肯定是能把代码框架看懂的,遇到看不懂的地方可以再回来看一下我上面的思路,希望对你有一定帮助。
当你看懂框架的代码后,一定是能看懂我上面的这段话的。
如果实在看不懂框架,可以参考:https://blog.csdn.net/qq_41835314/article/details/124969379(这位老哥可以说是讲的相当的细了==)
任务一:生成光线
这个不难,本质上就是坐标转换,算出屏幕像素中心点在世界坐标系的坐标,与视点相减,就是光线方向。实际上可以这么理解,这里坐标转化的过程与透视投影那一部分几乎是一模一样的思路,可以把屏幕像素看作视锥的一个底面,脑子里要有一张图,然后视点就是视锥的尖端,按照这种角度去理解应该会更好理解一点,scale和imageAspectRatio的意义也容易明白。
可以参考:https://blog.csdn.net/Q_pril/article/details/123825665,已经讲的比较透彻了。
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);
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
// generate primary ray direction
// 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*
float x,world_scene_width;
float y, world_scene_height;
//世界坐标下屏幕的真实长度和宽度
world_scene_width = 1 * scale * 2 * imageAspectRatio;
world_scene_height = 1 * scale * 2;
//x范围从 0-(scene.width-1) 转换为 0-1
x = (i + 0.5) / (scene.width - 1);
//x范围从 0-1 转换为 -1-1
x = x * 2 - 1;
//x范围从 -1-1 转换为 -world_scene_width/2-world_scene_width/2
x = x * world_scene_width / 2;
//y同理,其范围从0-(scene.height-1)最终转换为world_scene_height/2-(-world_scene_height/2)
y= (-2 * (j + 0.5) / (scene.height-1) + 1)* world_scene_height/2;
Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
dir = normalize(dir);
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);
}
任务二:光线与三角形求交
这个也很简单,就是把老师ppt上面的Moller Trumbore求交算法实现出来,实际上老师上课讲了两种方法,另外一种是解析几何的方法,这种算是一种直接求解法,本质上是通过重心坐标构造一个线性方程进行求解。按老师说法是比解析几何的方法要快的。个人理解就是解析几何的过程是先求平面和光线的交点,然后再通过叉乘判断交点是否在三角形内部。而Moller Trumbore算法是直接求出交点的重心坐标,通过简单的逻辑判断是否在三角形内部,相较于解析几何方法,不需要再进行一次叉乘计算判断,一步到位。
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.
//这里也不难,说白了就是把ppt上的Moller-Trumbore算法写出来就行
Vector3f E1 = v1 - v0,
E2 = v2 - v0,
S = orig - v0,
S1 = crossProduct(dir, E2),
S2 = crossProduct(S, E1);
float S1E1 = dotProduct(S1, E1);
tnear = dotProduct(S2, E2) / S1E1;
u= dotProduct(S1, S) / S1E1;
v= dotProduct(S2, dir) / S1E1;
if (tnear < 0) return false;
if ((1 - u - v) > 0 && u > 0 && v > 0) return true;
return false;
}
最终效果
由于这次作业比较简单,没遇到bug,一次就出来了,最后效果看上去和老师给的差不多,还有点小激动==
感悟
好久没写算法了,前段时间都在写工程的一些东西,现在写这些简单的都有点生疏,慢慢拾起来吧,然后的话作业做完,看懂框架之后,感觉自己对光线追踪的过程有了进一步的理解,编程实践真的很重要==
参考链接
1.https://blog.csdn.net/Q_pril/article/details/123825665
2.https://blog.csdn.net/ycrsw/article/details/124199544
3.https://blog.csdn.net/qq_41835314/article/details/124969379