光线跟踪之简介

2016-04-25
        我以前学习OpenGL 的时候,总是在一些基本的概念(display list,sampler,vao,vbo等等)上浪费了不少时间,然而,在前面的blog 里也提到过,这些知识都不是图形学真正重点的东西,只是OpenGL 这种实现中的概念而已。我希望,打算学学渲染的同学,应该都首先尝试着写出ray tracer,而不是是去使用DX11或者OpenGL、Vulkan。OpenGL只是一种为了加速而采用很多投机取巧的方式,刚开始的时候学习它反而会给自己带来困扰。所以,我尝试着以ray tracing来介绍图形学中较为基础的知识点。这一系列的blog都有对应的一个完整的程序。后续也会有多篇此系列的文章。 
        既然要写最简单的实例程序,我们就尽量减少代码量、文件个数,用纯C来写。为什么光线追踪程序大多是C++ 写的呢? 是否采用C++来写对于简单程序来讲并不是很重要。 当程序的规模增大时,C++的继承多态机制能够更好的组织代码。这里的实例程序就不用考虑这个因素了。
  读者应该对图形学、光线追踪、OpenGL等有一些了解为好。下面参考文档的前几个都是很好的学习材料。在这里,我尝试这以自己的经验讲解一下图形学中常见的问题,而不是按照教科书的那种方式来讲(我也没有那种能力),我只是想从一个学习者的角度来看,哪些步骤对初学者比较有难度的,需要多投放精力的。
一、 图形学的基础知识
坐标系
        我们需要构建一个三维的世界,在这个世界中放置物体,我采用较为常用的右手坐标系,多数建模工具采用,这也是OpenGL 中采用的,DX采用的是左手坐标系。不过,坐标系的选择是没有大的关系的,系统内保持一致即可。 
                                                                     http://leonardo.wotaneage.com/attachments/right_hand_coordinate.png
 
 
          
上帝说要有光,于是就有了光
        光源实在太重要了,没有光,我们什么也看不见。光源也是有很多种类的,如最常见的点光源(灯泡),面光源(街上的广告灯), 平行光(太阳光),环境光()。渲染程序中,一般是直接叠加多种光源来模拟光照效果。而且,关于光照效果的模拟,有多种模型,什么意思呢?就相当于一道题,可以选择几何法来解,也可以选择代数法解题。最常见的Phone模型的原理如下图所示:
                                        http://leonardo.wotaneage.com/attachments/phong_shading.png
        本文示例用点光源。我们假设点光源极小,不像灯泡有比较大的发光钨丝,所以我们看不到这个点光源。
再有物体   
        如果只有光而没有物体,我们看到的效果就如同观察太空的效果,几乎什么都看不到:全黑的。光照射到物体上,才能被我们看见。所以,我们需要把物体的形状、位置,用代码表示出来。有了物体,才能选用光照模型进行计算。
构建世界
        把三维的物体,放到二维的view plane 上观察,就相当于把三维的世界投影(projection)到二维世界。渲染程序中最常用的两种投影模式:正交投影(平行投影),透视投影。
        我们假设xoz 平面代表地面,是绿色的。物体盒子是蓝色,
           http://leonardo.wotaneage.com/attachments/my_orthogonal_proj.png
        在orthogonal 投影下,就看不到地面了,只能看到一个正方形。正交投影在CAD工具软件中有使用。 
             http://leonardo.wotaneage.com/attachments/my_perspective_proj.png
        在透视投影下,我们能够看到地面,和”天空“,类似与人眼的观察效果。越远的物体,投影到view plane 上的面积越小。透视投影是我们关注的重点。
     
二、ray traing 过程  构建最简场景并用程序表示
光线跟踪的工作流程        
        光源发出的光到达物体表面之后,会发生发射和折射,反射分为镜面反射、漫反射。现实世界的光线会发生多次反射折射,我们称之为全局光照。ray tracing 是global illumination的一种实现, 模拟我们真是世界的光照方式,只不过采用与真实过程相反的过程,用来计算镜面反射、折射、阴影。全局光照的另外一种实现是 Radiosity,用来实现漫反射。ray tracer中我们一般需要表现出三种ray:
  1. 眼睛”发出的“,经过view plane 栅格的ray(e_ray)
  2. e_ray 与物体交点到光源连线(用来检测是否是阴影区域,还是直接光照区域)
  3. 折射光线      
        假设物体是一个地面,我们在后续的学习过程中再让物体逐渐复杂化。那么平面应该如何表示呢? 
       typedef struct {
                Point p;
                Vector normal;
        } Plane;    这样就可以表示一个平面了, 而且是无限大的。不妨指定 xoy 平面,Plane basePlane = {0,0,0, 0,1,0};
 
        本来,光线照射到物体上,反射出无数的光线,其中很小一部分会进入人的眼睛。如果模拟这个正向的过程,计算量上来看那是不可行的。因为光源发出的光是无限密致的,物体表面可以无限分割,有无限细节。所以,只能 采用逆向过程。当然,这也会引入一些问题:需要判断判断整个过程中模拟的光线是否真正存在。
三,程序计算例子
        我使用最简单的渲染场景:一个平面 + 一个点光源。最简单版本的算法伪代码:
for (ray in rays)
    for(obj in objects)
    intersectPoint = intersect(ray, obj)
    if ( NULL != intersectPoint ) {
        ray2Light = light - intersectPoint  // 点相减,获得向量
        distance = vectorLenght( ray2Light)
        double scale = 1 / distance^2 // 根据距离获取光源光线的衰减率
        color = materialColor * scale // color 是最终呈现在view plane 上的像素
    }
    else
         color = 天空的颜色
        在这里,我们偷懒一下,把模型简单化处理。我们假设eye 位置是固定的,view plane 也是固定的,以世界坐标的形式给出。我们再次假设光源发出的光没有衰减,从无穷远出发过来的光也没有衰减(好清爽的世界啊)。设置地面平面为红色色, 光源为纯白色,视点、光源处在平面上方,我们能够看到的应该是一望无际的蓝色和黑色的天空。 就如:
       http://leonardo.wotaneage.com/attachments/single_plane_imagine.png
 
  实际上,光源的光线会随着距离衰减的。 常见的有两种光线衰减公式:
  1.  i1 / i2 = d2^2  / d1^2  =>  i1 单位距离的衰减因子, d1 单位距离,故 i2 = 1 / d2^2
  2.  
        对于二次曲线来讲,一次部分可以的影响不大。也就是说第一公式就是第二个公式的简化版本。
        假设视线与物体交点P,P到光源L 的向量 pl, P 点最终颜色就是rgb三个分量 各乘以 1/ lenght(pl) ^ 2。 所以,我们看到的是地面上一个逐渐变暗的光圈,而且,变化的速度还很快。有如路灯的效果。也有项目采用线性速度变化的公式,计算量和渲染效果平衡。视线没有和平面相交时,就照射到了天空,在这里,我强制把天空设置为白色,以便把无穷远处区分出来。
     
           http://leonardo.wotaneage.com/attachments/single_plane_real.png             
        最简单的光线最终程序就完成了。我们走出了第一步。接下来,还有几篇blog会讲解ray tracer基础的其他部分。
 
 四、结果的保存
        我们计算完了整个过程,要怎么样才能把计算结果展现出来呢。 这是非常简单的。最终绘制在view  plane上的像素,我们直接保存为bmp 格式的图片。bmp格式采用位映射存储格式,不采用压缩,扫描方式:按从左到右、从下到上的顺序,和view plane上像素的排放格式是一致的。有开源lib 能帮助写bmp 文件。
       本文代码在https://github.com/cloudqiu1110/tinyrt.git single_plane 分支                                                
     
 
五、相关讨论
 对比Ray Tracer 和 OpenGL
       我相信,很多同学都是对游戏里面那种渲染是更感兴趣一些的,实时的、有交互的,就如同我们刚开始学习编程的时候,对有界面的程序更加感兴趣一些。
        OpenGL实时性比较高,而Ray Tracer 一般比较慢,为什么呢?
for each (obj)
got projected pixel area
 for each pixel in depth buffer
 got nearest pixel by depth comparation
        OpenGL 遍历object, 渲染为二维图片到buffer(带有depth),  比较pixel depth, 决定哪些像素被舍弃。这是硬件实现的,而不需要程序负责。 OpenGL采用局部光照。 只考虑光源到模型表面的照射效果,物体之间的光照效果是不考虑的,遮挡效果用像素的深度来判断。 在游戏中,也会要求有全局光照的效果。那是怎么实现的呢?其实,是采用了一种取巧的方法,就是提前把模型建好,在3ds max,maya里面预烘培为贴图,游戏中直接应用贴图来模拟阴影效果。 
for each (ray)
          for each (obj)
          pixel = intersect( ray, obj)
       实际上相当于把一层三维空间的计算 降维到 二维空间的计算了,所以计算量就少了。也就是把物体相互之间的光照效果忽略掉了,这也是“全局光照”和“局部光照”的区别。      
        “栅格化”(rasterization) :  并不是只有OpenGL 才有这个术语的,也不是3D 图形渲染最早使用的,别忘了,之前还有过2D 图形加速器的。 请参考wiki。
 
        最后总结一下,知乎上有个话题“ 现阶段应该怎么学习计算机图形学呢?”,我觉得对看到这篇博客的同学都应该围观一下“ 文刀秋二“的回答。我以前就是以为学会了OpenGL,就算是学会了图形学,至少是入门了。但是,这只是我的一厢情愿而已。 
 
ref : 
  1. http://web.cse.ohio-state.edu/~hwshen/681/Site/Slides_files/basic_algo.pdf     这篇文档是必看的
  2. http://www.jianshu.com/p/0375389e6a3e      
  3. http://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-ray-tracing   必须看
  4. https://en.wikipedia.org/wiki/Ray_tracing_%28graphics%29  
  5. Ray Tracing from the Ground up
  6. 《3D数学基础:图形与游戏开发》
  7. https://en.wikipedia.org/wiki/Rasterisation
  8. https://en.wikipedia.org/wiki/Rendering_(computer_graphics)
  9. https://en.wikipedia.org/wiki/Raster_graphics
  10. http://www.scratchapixel.com/lessons/3d-basic-rendering/introduction-to-ray-tracing  非常不错的教程
  11. https://www.ics.uci.edu/~gopi/CS211B/RayTracing%20tutorial.pdf
  12. https://raw.githubusercontent.com/quietshu/-paper-cg-RayTracing/master/RayTracing.pdf     类似于catelog, Good
  13. https://www.ics.uci.edu/~gopi/CS211B/RayTracing%20tutorial.pdf
 
如果有任何意见,欢迎留言讨论。


[ 主页 ]
COMMENTS
POST A COMMENT

(optional)



(optional)