Ray-tracing (1)—Overview
“Ray tracing isn’t too slow; computers are too slow.” (Kajiya 1986)
我们已经知道光栅化(Rasterization)存在一些问题,其中最大的问题在于它不能够很好的处理一些全局的效果如:
- 软阴影
- Glossy reflection——有一些高光,但是没有像镜子那么光滑的高光,比如铜镜
- 光线的多次弹射(间接光照)
而光线追踪(Ray Tracing)可以比较好的解决这些问题!
ps: 虽然光线追踪渲染出来的效果很好,但是它渲染的速度非常慢!
ps again: Thumbnail
《月色真美》。
Ray-tracing和Rasterization的区别
Ray-tracing和Rasterization都是用来解决visibility问题的。而visibility又包含这样的两个问题:
- 计算空间中一点和第一个可见表面间的visibility,如图中的E点和P点;
- 计算空间中任意两点间的visibility,比如图中的P点和L1 or L2点;
前者解决的就是哪些点会在屏幕上显示的问题,而后者更偏重于解决shading的问题,比如阴影的问题(该点是否可以被光源看见),间接光照(该点是否可以被其他物体反射的光照到)等等。
因此可以发现Rasterization前者做的比较好,而后者确做的不怎么好,比如在阴影方面,Rasterization就比较复杂(步骤如下):
- 将camera放在光源位置对着场景渲染一次得到一张深度图;
- camera在视角方向再渲染一次
- 把从视角方向看到的点投影回光源和之前的深度图比较,决定哪些点可以被光源看到哪些点看不到,在视角方向可以看到但在光源方向看不到的点就在阴影中。
可见对于阴影的渲染光栅化就需要渲染两次,并且得到的阴影还是硬阴影,而得到软阴影会更难!而对于Ray-tracing只需要把点和光源相连看看中间有无遮挡物即可!方便了许多!
因此可见相比于Rasterization,Ray-tracing在shading方面会做的更好!
Rasterization工作流
- 遍历场景中所有的几何图元(primitives);
- 把这些primitives投影到屏幕上;
- 遍历屏幕上的每一个像素判断哪一些像素被刚刚投影到屏幕上的primitives覆盖了。
Rasterization伪代码 1
2
3
4
5
6
7
8
9
10
11
12
13// loop over all pixels
Vec3f *framebuffer = new Vec3f[imageWidth * imageHeight];
for (int k = 0; k < numObjectsInScene; ++k) {
// project object onto the scene using perspective projection
...
for (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
if (pixelCoversGeometry(i, j, objects[k]) {
framebuffer[j * imageWidth + i] = objects[k].color;
}
}
}
}
Ray-tracing工作流
- 遍历屏幕上的每一个像素,从像素中心投射光线到场景中;
- 遍历场景中所有物体的所有primitives,计算和光线相交最近的点,这个点就是从刚刚的像素上我们可以看到的点。
Ray-tracing伪代码 (Casting rays & Intersections) 1
2
3
4
5
6
7
8
9
10
11
12
13for (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
//在实际代码中我们一般把遍历物体和三角形的代码放到一个叫做`trace()`的函数中。
for (int k = 0; k < numObjects; ++k) {
for (int n = 0; n < objects[k].numTriangles; ++n) {
Ray ray = buildCameraRay(i, j);
if (intersect(ray, objects[k].triangles[n])) {
framebuffer[j * imageWidth + i] = shade(ray, objects[k], n);
}
}
}
}
}
从上面可以发现Ray-tracing和Rasterization二者代码的内外循环是相反的,Rasterization先遍历primitives再遍历像素,而Ray-tracing先遍历像素再遍历primitives;因此Ray-tracing是image centric而Rasterization是object centric。
使用ray-tracing计算真实感图像的步骤
Casting rays: 向场景中投射光线
在这一步中我们遍历image plane上的每一个像素,然后向场景中投射光线(从image plane上投射出来的光线我们称作camera or primary rays,假如这光线投射到场景中之后击中了物体,从而发生了弹射,我们把新的光线称作secondary rays)。
这里利用了光路的可逆性,我们知道在生活中,光源射出光线,打到物体上,物体吸收部分光再把光线弹射到我们的眼中。但是如果在计算机中模拟这个过程的话会发现其实场景中只有很少的一部分光线会打到人眼中,这样效率是十分低下的。因此我们反过来做,我们假想从眼睛投射出光线,然后光线沿着之前的光路逆着回到光源。
Intersections: 光线求交
遍历场景中所有物体的所有三角面看测试是否与光线相交。
注意:在大多数情况下都是考虑的和polygon几何体的三角面求交,一般我们把其他类型的几何体(如subdivision surface, NURBS)都转换成polygon,然后再把polygon三角化!
为什么要转换为polygon呢?
场景中显然只有一种类型的模型这样计算是很方便的,这样就不会给polygon编一套程序,又给NURBS编一套程序。同时其他类型的几何体转换成为polygon是比较简单,且polygon三角化也很简单。
polygon又为什么要三角化呢?
这样做的原因在于不管是光线追踪还是光栅化都比较喜欢三角面,因为:
- 三角形三个顶点永远共面;
- 使用三角形的话则可以很方便的使用重心坐标做相关计算(比如插值)
Shading
渲染(rendering)要解决两个问题visibility和shading。
- visibility解决的是物体可不可见以及物体的形状是什么样子的问题。ray-tracing和rasterization就是解决visibility的技术。
- Shading建立了描述物体外观的模型(如Phong model),从而模拟物体的外表即物体最终看起来是什么样的(看起来是什么颜色的,材质是什么样的等等)。
对于shading, ray-tracing会比其他的渲染技术(比如rasterization)更有优势。因为要想模拟物体看起来是什么样的实际上就是在模拟反射了多少光到观察者的眼中,因此我们需要搜集各种各样落在物体表面的光照信息(direct light,indirect light。。。),显然这样的话ray-tracing会更方便。
Shading的计算可以放在shader中完成!
Ray-tracing和light transport algorithm的区别
- light transport algorithm模拟了光在场景中如何传播;回答了光击中物体表面之后如何反射折射,朝哪个方向发射secondary rays的问题。light transport algorithm对于shading来说非常的重要,因为shading需要搜集场景中的光照信息从而计算颜色,而light transport algorithm模拟了光在物体表面是如何折射反射的因此可以说和shading紧密相关。
- Ray-tracing是为了计算点与点之间visibility的技术。
- 当要模拟光线在各个物体表面弹来弹去,此时我们就需要计算光线和这些表面的交点(visibility的第一个任务),此时显然使用Ray-tracing就会方便很多(Ray-tracing本身就定义了一根光线!我们只需要根据相关的规则(light transport algorithm)来告诉这根光线怎么弹射就可以模拟这个过程了!),注意,这里不是说Rasterization完全不能做,而是用Rasterization做的话会很麻烦,因此一般light transport algorithm总是和Ray-tracing联系在一起!
- light transport algorithm可以使用Ray-tracing来计算visibility也可以用Rasterization来计算,甚至可以是两者结合起来一起计算!
- 在一般情况下,想要得到一张具有真实感的图像只用一种light transport algorithm可能无法得到很棒的效果,一般我们会混合使用不同的light transport algorithm来分别渲染不同的效果之后再叠加到一张照片上。
- 常见的light transport algorithm如Whitted (-style) ray-tracing,path tracing等等。
Whitted (-style) ray-tracing
Whitted (-style) ray-tracing是一种light transport Algorithm,它描述了当光线击中不同类型的物体表面的时候该如何进行弹射,具体如下:
- 当碰到不透明( opaque or diffuse)物体时,直接使用光照模型(如phong模型)返回计算的颜色。(光线碰到不透明物体则返回)
- 当碰到只有反射属性的表面(如镜子)的时候那么只需要发出反射光线(基于入射光和反射光的关系进行计算。),如下图。
- 当碰到透明(transparent)物体表面时,那么既要发出反射光线也需要放出折射光线(使用Snell’s law进行计算),如下图。
从以上的分析可以知道只要碰到了透明物体,那么每次至少会产生两根光线,如果继续打到透明物体那么每根光线又至少会产生两根光线!这个过程是指数增长的!光线不断弹射的过程可以看成是一个递归的过程,显然不断的增长下去最终会爆炸的!因此我们需要限制递归的层数。
渲染流程
如下仅仅展示的是伪代码!
主函数,在主函数中我们会设置场景中模型灯光屏幕窗口大小等等资源,同时在这里启动渲染(
render()
)!main 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int main()
{
std::vector<Object *> objects;
std::vector<Light *> lights;
// add objects and lights
objects.push_back(new Object);
...
Options options;
options.width = 640;
options.height = 480;
options.maxDepth = 5;
...
render(objects, lights, options);
...
return 0;
}在
render()
函数中,我们遍历每一个像素,并且产生primary ray,然后把primary ray投射到场景中并把相关的结果写到framebuffer
里。render 1
2
3
4
5
6
7
8
9
10
11void render(...)
{
for (j = 0; j < options.height; ++j) {
for (i = 0; i < options.width; ++i) {
// generate primary ray using
...
framebuffer[j * options.width + i] = rayCast(orig, dir, objects, lights, options, 0);
}
}
}在
castRay()
函数中实现了上面提到的 Whitted’s light transport algorithm。(注意该函数是一个递归函数!)castRay 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
45Vec3f castRay(const Vec3f &orig, const Vec3f &dir, ..., uint32_t depth)
{
if (depth > options.maxDepth)
return options.backgroundColor;
float tNear = INFINITY; // distance to the intersected object
Object *hitObject = NULL; // pointer to the intersected object
Vec3f hitColor = 0; // the color of the intersected point
if (trace(orig, dir, objects, tnear, hitObject....) {
switch (hitObject->materialType) {
case REFLECTION_AND_REFRACTION:
// compute reflection and refraction ray
...
// cast reflection and refraction ray, don't forget to increment depth
Vec3f reflectionColor = castRay(hitPoint, reflectionDirection, ..., depth + 1);
Vec3f refractionColor = castRay(hitPoint, refractionDirection, ..., depth + 1);
float kr; // how much light is reflected, computed by Fresnel equation
fresnel(dir, N, hitObject->ior, kr);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
case REFLECTION:
// compute reflection ray
...
// cast reflection ray, increment depth
Vec3f reflectionColor = castRay(hitPoint, reflectionDirection, ..., depth + 1);
hitColor = reflectionColor;
break;
default:
// compute phong illumination model
for (uint32_t i = 0; i < lights.size(); ++i) {
// compute shadow ray
Vec3f lightDirection = lights[i].position - hitPoint;
float len2 = dot(lightDirection, lightDirection); // length^2
float tNearShadow = INFINITY;
// is hitPoint in shadow of this light and is the intersection point closes than the light itself?
bool isInShadow = (trace(hitPoint, normalize(lightDirection), tNearShadow, ...) && tNearShadow * tNearShadow < len2);
// compute the Phong model terms
hitColor = (1 - isInShadow) * (...);
}
break;
}
}
return hitColor;
}在上面的
castRay()
函数中有见过这个函数,它的作用遍历场景中所有物体的所有三角形然后找到和光线相交的交点!trace 1
2
3
4
5
6
7
8
9
10
11
12
13
14bool trace(const Vec3f &orig, const Vec3f &dir, float tNear, ...)
{
hitObject = NULL;
for (uint32_t i = 0; i < objects.size(); ++i) {
float tNearK = INFINITY;
if (objects[i]->intersect(orig, dir, tNearK, ...) && tNearK < tNear) {
hitObject = objects[i];
tNear = tNearK;
...
}
}
return (hitObject != NULL);
}