今天给大家来谈谈下shader高级中的光线追踪,我想在座的大佬有可能没有听过光线追踪,但一定见过大名鼎鼎的下面这张图。
几乎你在看到的用shader做的大多数三维场景均是采用光线追踪技术为基础做的。当然还有非常多有名气的例子。
先简单通俗理解一下光线追踪原理。其实就是通过射线法不断去迭代,最终找到场景中模型的边界。类似我下面这张图。我们今天给大家主要介绍的是“球体光线追踪算法”。今天我们尝试在3D场景中绘制一个球体为例子。
下图是我关于光线追踪的一个整体关系图。
我先简单解释下下图的几个要点对象的意义:
1、我将canvas或屏幕设置成z=0的临界。设想一下此刻屏幕上是由很多大小的像素组成,并未每个极小的像素点都有自己的坐标。我们可以用(uv.x,uv.y)来代替。如下图所示
2、黑色是我们场景中假设的相机。确定一个相机我们需要三个元素,(在webgl初级课程中给大家明确讲到过)分别是position updirection eyedirection
。假如此刻我们假设相机参数如下:
vec3 position = vec3(0.,0.,10.)
vec3 updirection = vec3(0.,1.,0.);
vec3 eyedirection = vec3();
这里我重点强调下eyedirection
,它代表的是从摄像机射出的不同角度的射线,每条射线分别穿过canvas
上不同的uv
坐标点。我们基于蒙特卡洛技术以一个调皮的光线为例子去探讨。
3、下面黄色的东西是我们的地平面。我们绘制的场景是让我们的球体能够立在我们的地平面上。我们地平面实际存在一个高度,h
。
4、最后解释下我们蓝色球体。这部分实际上和我之前写的SDF是一样的,我们本质可以归类为球体的SDF,文章地址
float sdSphere(vec3 p, float r)
{
return length(p) - r;
}
讲解完各自对象代表的意义,我再介绍下各个对象之间的相关关系,也是我们后面代码的实现思路。
步骤一:摄像机假设发射一条长射线(射线a,也叫”阿波罗11号射线“),我们以这条射线为半径不断去绘制圆,直到我们绘制的圆与球体相交后,需要找到该圆与射线相交的点,作为新的中间“加油站”。然后在这个“加油站”我们称之为P1,之后我们再以P1为圆心,再去沿着射线方向继续绘制圆,直到绘制的圆与球体再次相交后,需要找到该圆与射线相交的点(等同于P1到球体的最短距离),作为新的中间“加油站”。然后在这个“加油站”我们称之为P2(P2坐标等同于P1坐标+P1到球体的最短距离)。按照此规律射线上的点不断推进.......我们称这个过程为“光线行进”。
步骤二:步骤一中我们用到P1、P2点到球的最短距离,这个公式我们需要用到的就是 球体的SDF。直到我们光线的最短距离求的值比如小于一个很小的临界值,我们就判断为该光线与球体表面的点相交啦。这时候我们会保存该点到相机的距离,从而将该球体表面的点的位置记录在内存里。
float sdSphere(vec3 p, float r)
{
return length(p) - r;
}
步骤三:假设我们发出的“阿波罗11号射线”在中间没有遇到任何阻碍,其实就是没有碰到球的任何表面。是不是会射向无限远的地方,那估计我们电脑会无线叠加,那估计就跑费了。所以我们需要设置距离相机最远距离,如果该位置超过此值就默认停止。
好了我们来实现一下吧
步骤一:坐标标准化(这部分我们不在阐述,shader中级专门讲解了)
vec2 uv = (gl_FragCoord.xy-u_resolution.xy*.5)/min(u_resolution.x,u_resolution.y);
步骤二:确定相机参数,position和eyedrection
vec3 eyeposition = vec3(0, 0,1.); //相机的位置,此刻想像一下他是在我们屏幕的左边。
vec3 eyedrection =normalize( vec3(uv,0.0)-eyeposition); //求从相机射出的不同射线。
步骤三:rayMaching函数,我们来看看代码如何实现的哈。
float rayMaching(vec3 eyedrection,vec3 eyePosition){
float d = START_POSITION;//初始点的位置
for(int i = 0;i<MAX_ITERATIO_NNUMBER;i++){
vec3 p =eyePosition+ d* eyedrection; //光线行进,不断去往前推进点
float newd = sdfSphere(p ,1.);//获得当前点与球的最短距离
d+=newd;//将位置进行累计,记录到内存,当下一次进行“光线行进”时候当做参数
if(newd<MIN_DISTANCE||d>END_POSITION){ //最短距离小于很小很小的一个值停止或者大于最大距离的时候停止
break;
}
}
return d; //最后我们将每条射线与球的交点位置返回
}
球体SDF别忘了,稍微做了小小变形,将球体的位置添加进去了。
float sdfSphere(vec3 p, float r){
vec3 sphere = vec3(0.,0.,-5.);
return length(p-sphere)-r;
}
步骤四:如果光线返回的距离大于100(不一定是100哈,这个值只要是大于或者等于相机距离的最远距离即可),说明光线冲向了无限远,没有和球体有交点,反之则击中了球体,进而设定其颜色
if (d > 100.0) {
col = vec3(1.);
} else {
col = vec3(0.6,0.2,0.2);
}
当然你也可以根据与球体最小距离判断,进而改变球体的颜色,请注意此时光线追踪函数rayMaching
返回的值应该是每次实时计算与球体的距离
if (d < 0.01) {
col = vec3(1.);
} else {
col = vec3(0.6,0.2,0.2);
}
整体代码如下
// Author: ice
// Title: 光线追踪
#ifdef GL_ES
precision mediump float;
#endif
#define MAX_ITERATIO_NNUMBER 255
#define MIN_DISTANCE 0.001
#define START_POSITION 0.
#define END_POSITION 100.
uniform vec2 u_resolution;
float sdfSphere(vec3 p, float r){
vec3 sphere = vec3(0.,1.,-5.);
float plane = p.y-0.;
return min(length(p-sphere)-r,plane) ;
}
float rayMaching(vec3 eyedrection,vec3 eyePosition){
float d = START_POSITION;
for(int i = 0;i<MAX_ITERATIO_NNUMBER;i++){
vec3 p =eyePosition+ d* eyedrection;
float newd = sdfSphere(p ,1.);
d+=newd;
if(newd<MIN_DISTANCE||d>END_POSITION){
break;
}
}
return d ;
}
void main() {
vec2 uv = (gl_FragCoord.xy*2.-u_resolution.xy)/min(u_resolution.x,u_resolution.y);
vec3 col = vec3(0);
vec3 eyeposition = vec3(0, .3,.9);
vec3 eyedrection =normalize( vec3(uv,0.)-eyeposition);
float d = rayMaching(eyedrection,eyeposition);
if(d<20.){
col = vec3(0.376,0.830,0.758);
}else{
col = vec3(1.);
}
gl_FragColor = vec4(col,1.0);
}
本节内容到此结束,下期节目我们再见!