浅聊一下shader中的光线追踪

发布于 2023-03-31 20:29:58

背景

今天给大家来谈谈下shader高级中的光线追踪,我想在座的大佬有可能没有听过光线追踪,但一定见过大名鼎鼎的下面这张图。
奥德路.png
几乎你在看到的用shader做的大多数三维场景均是采用光线追踪技术为基础做的。当然还有非常多有名气的例子。
先简单通俗理解一下光线追踪原理。其实就是通过射线法不断去迭代,最终找到场景中模型的边界。类似我下面这张图。我们今天给大家主要介绍的是“球体光线追踪算法”。今天我们尝试在3D场景中绘制一个球体为例子。
球平面.png

整体架构图

下图是我关于光线追踪的一个整体关系图。

原理.png

对象的意义

我先简单解释下下图的几个要点对象的意义:

1、我将canvas或屏幕设置成z=0的临界。设想一下此刻屏幕上是由很多大小的像素组成,并未每个极小的像素点都有自己的坐标。我们可以用(uv.x,uv.y)来代替。如下图所示

uv架构.png

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号射线”在中间没有遇到任何阻碍,其实就是没有碰到球的任何表面。是不是会射向无限远的地方,那估计我们电脑会无线叠加,那估计就跑费了。所以我们需要设置距离相机最远距离,如果该位置超过此值就默认停止。

射线1000.png

好了我们来实现一下吧

实践

步骤一:坐标标准化(这部分我们不在阐述,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);
}

本节内容到此结束,下期节目我们再见!

0 条评论

发布
问题