smallpt: Global Illumination in 99 lines of C++
正向光线追踪符合常识:光线从发光物体出发出,“撞击”到被观察物体上,经过一系列光线传输进入人眼。
因此正向光线追踪的基本流程可以简述为:
从上述流程不难可以看出,正向光线追踪需要追所有光线。而在这所有光线中,只有一部分光线会“撞击”到观察体,“撞击”到观察体的光线也只有部分会射入人眼。因此追踪所有光线,计算量大且做无用功较多。
追踪光线的方向与正向相反:从眼睛处射出光线,追踪光线射击到物体后,是否能回到光源。如果能则说明该点被光源照亮,否则物体上该点可能被间接照亮,需要进一步判断。
逆向光线追踪为什么比正向光线追踪号好呢?
因为从图形学的角度来看
人眼发射出的光线是有限的,而光源发射出的光线是无限的:人眼接收图像是二维的像素组成的。每个像素记录着该点的颜色。也即对于每个像素计算其颜色, 代表着该像素上一次光线追踪的结果。
因此逆向光线追踪的基本流程可以简述为:
FOR 每个像素点 :
构造人眼入射光线Ray
光线追踪Ray :
计算与光线Ray相交的最近的物体Obj
IF obj == null :
该像素点颜色为缺省值:全局环境光AmbientColor
Continue
ELSE
看反射光Reflection是否能直接与光源相连(未被其他物体遮挡)
IF 没有被遮挡
该像素点颜色为光源颜色在该材质上的作用
ELSE IF 被遮挡
该像素点颜色根据反射光的光线追踪结果得到。
个人理解,不对欢迎指正
蒙特卡洛思想介绍
蒙特卡罗光线追踪对逆向光线追踪模型进行改进,其中最大的区别在于把概率模型引入光线追踪。
根据上述分析光线追踪算法中最重要的步骤可以分解成两个:
下面由于准备材料不够,我先只主要介绍方面2:
漫反射材质表现为表面不规则,因此反射光线的方向无法确定判断。它朝可能的任意方向反射。
我们假定对于漫反射材质,反射光线的方向范围可以限定在以撞击点为圆心,撞击点法相为中心的半圆内。
假设:
入射光线为Ray(x0, d0)。其中x0表示光线起点 d代表光线方向
入射光线与物体表面的相交点为x
入射光线与物体表面相交点x出的法向为n
目标:求反射光线Reflection Ray(x, d1).
1.构建以x点为中心,n为一个坐标轴的笛卡尔直角坐标系
w = n
u=((fabs(w.x)>.1?Vec(0,1,0):Vec(1,0,0)) * w).norm()
v=w * u
u/v/w即组成一个笛卡尔直角坐标系
2.将反射光线方向d1分解为u/v/w表示形式
如下图所示
d1 = |d1| * cosα * cosθ * u + |d1| * sinα * w + |d1| * cosα * sinθ * v
其中α为 [0, PI/2]中的随机数;θ为[0,2*PI]中的随机数
这两个步骤对应smallpt: Global Illumination in 99 lines of C++中代码片段56-60:
if (obj.refl == DIFF){ // Ideal DIFFUSE reflection
double r1=2*M_PI*erand48(Xi), r2=erand48(Xi), r2s=sqrt(r2);
Vec w=nl, u=((fabs(w.x)>.1?Vec(0,1):Vec(1))%w).norm(), v=w%u;
Vec d = (u*cos(r1)*r2s + v*sin(r1)*r2s + w*sqrt(1-r2)).norm();
return obj.e + f.mult(radiance(Ray(x,d),depth,Xi));
}
镜面反射比较简单,反射光线可假定严格按照反射定律来求(入射角定于反射角)
假设:
入射光线为Ray(x0, d0)。其中x0表示光线起点 d代表光线方向
入射光线与物体表面的相交点为x
入射光线与物体表面相交点x出的法向为n
目标:求反射光线Reflection Ray(x, d1).
具体计算步骤如下图所示
d1 = d0 - n * 2 * (n * d0)
这两个步骤对应smallpt: Global Illumination in 99 lines of C++中代码片段61-62:
else if (obj.refl == SPEC) // Ideal SPECULAR reflection
return obj.e + f.mult(radiance(Ray(x,r.d-n*2*n.dot(r.d)),depth,Xi));
非透明材质在光线追踪的过程中,要不是漫反射,要不是镜面反射。与非透明材质不同,透明材质在光线作用下,反射和折射是同时存在的(全反射除外)。因此在光线追踪透明材质时,需要同时考虑这两种发射情况。
简单介绍一下斯涅尔定律,它主要是描述清楚了入射角与折射角之间的关系:
n1sinθ1 = n2sinθ2
其中n1/n2是两个介质的折射率;θ1/θ2分别是入射角/折射角
折射光线方向计算:
假设:
入射光线为Ray(x0, d0)。其中x0表示光线起点 d代表光线方向
入射光线与物体表面的相交点为x
入射光线与物体表面相交点x出的法向为n
两种介质的折射率分别为n1、n2
目标:求折射光线Reflection Ray(x, d1).
这个步骤对应smallpt: Global Illumination in 99 lines of C++中代码片段68:
Vec tdir = (r.d*nnt - n*((into?1:-1)*(ddn*nnt+sqrt(cos2t)))).norm();
考虑全反射:入射角大于某一临界角θc(光线远离法线)时,折射光线将会消失,所有的入射光线将被反射。
当折射角等于90°时,入射角即达到临界角。
这部分步骤对应smallpt: Global Illumination in 99 lines of C++中代码片段63-67:
Ray reflRay(x, r.d-n*2*n.dot(r.d)); // Ideal dielectric REFRACTION
bool into = n.dot(nl)>0; // Ray from outside going in?
double nc=1, nt=1.5, nnt=into?nc/nt:nt/nc, ddn=r.d.dot(nl), cos2t;
if ((cos2t=1-nnt*nnt*(1-ddn*ddn))<0) // Total internal reflection 全反射
return obj.e + f.mult(radiance(reflRay,depth,Xi));
这部分步骤对应smallpt: Global Illumination in 99 lines of C++中代码片段69-73
double a=nt-nc, b=nt+nc, R0=a*a/(b*b), c = 1-(into?-ddn:tdir.dot(n));
double Re=R0+(1-R0)*c*c*c*c*c,Tr=1-Re,P=.25+.5*Re,RP=Re/P,TP=Tr/(1-P);
return obj.e + f.mult(depth>2 ? (erand48(Xi)<P ? // Russian roulette 俄罗斯赌盘
radiance(reflRay,depth,Xi)*RP:radiance(Ray(x,tdir),depth,Xi)*TP) :
radiance(reflRay,depth,Xi)*Re+radiance(Ray(x,tdir),depth,Xi)*Tr);
待更新
参考:
1.smallpt: Global Illumination in 99 lines of C++
2.scratchapixel: Introduction to Ray Tracing: a Simple Method for Creating 3D Images
3.scratchapixel:Rendering an Image of a 3D Scene: A Light Simulator
4.百度百科:斯涅尔定律、菲涅耳公式
5.知乎:如何用 C++ 实现光线跟踪软渲染器?