0%

CPU光线追踪C++OpenGL实现

由于套暑研要准备一个简单的简历,我打算总结一下之前做的一些项目,发博客并上传Github。虽然我能明确地知道,做这些事情对看我简历的人来说毫无影响,但写博客记录自己人生是我自己想做的一件事。

做之前这些项目的时候我都没有去写博客,当时我还没有想好博客该怎么用。以后应该不会出现这些情况了。只要是有一些规模的项目,我都会把从技能学习到编程实现的整个过程发到博客上。

写这个项目是因为我图形学的导师在写一本图形学教程书,其中有一些习题要附上源代码。他让我帮忙实现真实感渲染章节的习题,顺便学习一下这一章的内容。

由于是项目总结,内容会十分简略。

CPU光线追踪C++OpenGL实现

Github地址

https://github.com/SingleZombie/Ray-Tracing-OpenGL

编译项目需要包含一些额外的文件,具体的要求见Github里的readme。

任务定义

实现一个使用光线追踪技术渲染的场景:一个带反射的球放在黑白相间的棋盘格地面上。

知识背景

光线追踪的思想比较简单:以摄像机为端点,向屏幕上每一个像素的处发射一条光线。光线接触到第一个物体后,既会接收物体自身的颜色,还会进行反射和折射,产生更多的光线。这些后续的反射、折射以及物体本身的颜色都会对最开始的这条光线产生贡献。这条光线的强度就能转化成产生光线的像素的颜色值。

从实现的角度来看,光线追踪就是一个递归的过程。为了计算一条光线的强度,还要计算它反射光和折射光的强度。

由于光每次都可以反射和折射,光线与物体的碰撞次数与递归层数成指数级增长,并且客观上光线的反射和折射是无穷无尽的,光线追踪算法需要一些停止递归的规则。递归层数过多、光没碰到物体都能让算法中止。不过即使有了一些限制规则,不经过优化的光线追踪算法运行得依然很慢。

设计

当时我写的伪代码如下:(不过是写完了程序再写的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// plane是一个平面结构,分量有平面上任意一点、平面法向量、材质
// ball是一个球体结构,分量有中心点、半径、材质
// scene是一个场景类,存储了平面、球等物体,存储了光照信息,可以进行光线追踪计算
// pixelPos表示屏幕上某个像素在世界坐标的位置
// ray是一个光线结构,表示从观察点射向pixelPos的一条光线
// color(i, j)表示屏幕坐标为(i, j)处的颜色
// CreatePlane(p, n)用平面上一点p和法向量n构造平面
// CreateSphere(p, r)用点p和半径r构造球体
// pixelToWorldPos(i, j)计算屏幕(i, j)处像素在世界坐标的位置
// Ray(p, d)用点p和方向向量d构造光线

plane = CreatePlane(vec3(0, 0, 0), vec3(0, 1, 0));
plane.setMaterial(checkMaterial);
ball = CreateSphere(vec3(0, 1, 0), 1);
ball.setMaterial(ballMaterial);
scene.add(plane);
scene.add(ball);

for i = 1:640
for j = 1:480
pixelPos = pixelToWorldPos(i, j);
ray = Ray(viewPos, pixelPos – viewPos);
color(i, j) = scene.traceRay(ray, 0);
end
end

// traceRay(ray, recursionTime)接收参数当前光线ray,当前递归次数recursionTime,递归计算并返回光照强度light
// MAX_RECURSION_TIME为常量最大递归次数,递归超过次次数函数会停止执行
// intersectedPoint表示当前光线照射到的第一个物体的照射点
// intersectedEntity表示当前光线照射到的第一个物体
// normal表示照射点处物体的法向量
// material是一个材质结构,表示照射点处物体的材质,分量有着色比例kShade、反射光比例kReflect、折射光比例kRefract。
// light是表示当前点的光照强度
// reflectDir表示光线经照射点反射后光的方向
// refractDir表示光线经照射点折射后光的方向
// getIntersection(ray)是场景scene的函数,用于计算场景中光线与物体的交点以及照射到的物体
// shade(entity, fragPos, ray)接收参数物体、着色点、光线,利用Phong等局部着色模型计算出该点的光强

function traceRay(ray, recursionTime)
if (recursionTime >= MAX_RECURSION_TIME) then
return;
end
(intersectedPoint, intersectedEntity) = getIntersection(ray)
if (intersectedEntity == NULL) then
return;
end
normal = intersectedEntity.calNormal(intersectedPoint);
material = intersectedEntity.calMaterial(intersectedPoint);
light = material.kShade *
shade(intersectedEntity, intersectedPoint, ray);
if (material.kReflect > 0) then
reflectDir = reflect(ray.direcion, normal);
light += material.kReflect *
traceRay(Ray(intersectedPoint, reflectDir), recursionTime + 1);
end
if (material.kRefract > 0) then
if (intersectedEntity.rayEnterEntity(ray)) then
refractDir =
refract(ray.direction, -normal, material.index, airIndex);
else
refractDir =
refract(ray.direction, normal, airIndex, material.index);
end
light += material.kRefract *
traceRay(Ray(intersectedPoint, refractDir), recursionTime + 1);
end
return light;
end

实现方法

从软件工程的角度来看,根据光线追踪的描述,很快可以得到编程所涉及的对象:射线(光线)、物体、光源、场景。物体、光源属于场景,每帧从视点向所有像素发出光线与物体判断相交,计算颜色时要用到光源、物体参数。再具体来说,光源包含颜色等参数,物体包含自身的几何信息及材质信息。有了类和类间关系,代码结构很容易写出来。

从算法实现的角度来看,只要理解了光线追踪算法,看懂了上面的伪代码,很容易写出对应的代码。

由于光线追踪算法的像素值完全来自射线和物体的交,物体可以以解析式的形式定义。比如球只需要定义一个中心点位置,一个半径。在判断射线和物体的交时,求解一个射线的参数方程即可。

结果