RayMarching入门

1,599 阅读1分钟

1-距离场

1-1-基本概念

SDF是Signed-distance-field的简写,译作有向距离场,它是一个记录模型中心点到模型表面的最小距离的函数。

以球体为例解释一下SDF的概念。

image-20220705180011674

球体的SDF函数是:

f(p)=|p-o|-r
  • p 空间内部任意一点
  • o 球心
  • r 球体的半径

通过这个函数,我们可以明显的看出:

  • f(p)>0 时,p在球外
  • f(p)=0 时,p在球上
  • f(p)<0 时,p在球内

1-2-距离场的作用

  • 绘制图形

image-20220707080934840

img

  • 描边

image-20220707081222455

  • 光晕

image-20220707081254880

  • 投影

image-20220707081337350

  • 内阴影

image-20220707081410543

  • 浮雕效果

image-20220707081337350

  • 光效

img

  • 变形

img

  • 建模

image-20220707082037365

接下来我们说一下SDF模型是如何显示的。

2-RayMarching

使用RayMarching 算法可以将SDF模型显示出来。

RayMarching 译作光线步进,或者射线推进,其意思就是让射线的起点沿着射线方向逐步推进,每次推进的距离就是射线起点到SDF模型的距离。

image-20220707102732861

利用RayMarching 显示模型的步骤:

  1. 将相机视点作为射线的起点。
  2. 在相机前面放一张栅格图像。
  3. 以相机视点为起点,向栅格图像的每个栅格做射线。
  4. 让相机视点沿射线方向进行逐步推进,每次推进的距离都是相机视点到SDF模型表面的距离。

3-RayMarching 代码实现

3-1-环境搭建

1.在vscode中安装一个Shader Toy

image-20220707235622265

Shader Toy就是其字面意思-Shader 玩具,它可以让大家无需在用webgl搭建项目,只需关注片元着色器。

2.建立一个test.glsl文件

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  fragColor = vec4(0,0,1,1);
}

3.右击页面,在提示面板中选择Shader Toy:Show GLSL Preview

image-20220708000307505

这样便可以在Shader Toy的显示窗口中看见一张蓝色的画布。

image-20220708000551619

3-2-绘制SDF模型

建立sphere.glsl 着色文件,然后写入着色代码:

// 相机视点位
vec3 ro=vec3(0,0,0);
// 球体的球心位置
vec3 sp=vec3(0,0,4);
// 球体的半径
float r=1.0;

/* 将fragCoord的xy位置映射到[-1,1]的坐标系中 */
vec3 fixUV(in vec2 fc) {
  vec2 uv=(2. * fc - iResolution.xy) / min(iResolution.x, iResolution.y);
  return vec3(uv,1);
}

/* 获取从相机视点到片元的射线
ro 相机视点
uv 片元位
 */
vec3 getRayDir(in vec3 ro, in vec3 uv) {
  vec3 dir = uv - ro;
  return normalize(dir);
}

/* 球体的SDF模型
p 点位
r 半径
 */
float sdfSphere(vec3 p,float r) {
  return length(p) - r;
}

/* 
获取每次光线推进中,推进后的视点到球体表面的距离
p 视点
o 球心
r 球体半径
*/
float getSphereDist(vec3 p,vec3 o,float r) {
  float sdf =sdfSphere(o-p,r);
  return sdf;
}

/* 光线推进
ro 相机视点位
rd 射线方向
 */
vec4 startRay(vec3 ro, vec3 rd) {
  // 视点位
  vec3 eye=ro;
  // 视点到球体表面的距离
  float dist=getSphereDist(eye,sp,r);
  // 片元颜色
  vec4 color = vec4(0,0,0,1);
  // 按照射线方向推进视点,20为最大推进步数
  for(int i = 0; i < 20; i++) {
    // 当推进视点距离球体表面小于0.01,默认射线碰撞到球体
    if(dist < 0.01) {
      color = vec4(1);
      break;
    }
    // 沿射线方向推进视点
    eye+= ro + rd * dist;
    // 更新视点到球体表面的距离
    dist=getSphereDist(eye,sp,r);
  }
  return color;
}


/* 绘图函数,针对于画布中的每个片元都会执行一次,执行方式是并行的。
fragColor 输出参数,用于定义当前片元的颜色。
fragCoord 输入参数,当前片元的位置,原点在画布左下角,右侧边界为画布的像素宽,顶部边界为画布的像素高
*/
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
  // 当前片元的栅格图像位
  vec3 uv = fixUV(fragCoord.xy);
  // 计算从相机视点到当前片元的射线
  vec3 rd=getRayDir(ro,uv);
  // 光线推进
  fragColor = startRay(ro,rd);
}

效果如下:

image-20220708003735635

现在球体已经显示出来了。

这个球体的法线我们根据其SDF模型的特性,也可以很容易获取。

有了法线,我们再建立一个平行光,那就可以绘制一个有体感的球体。

image-20211102115542615

再向其中添加一个视线,便可以算出球体的高光。

image-20211102154524076

对于其具体算法,可以参考我们在WebGL里讲过的

总结

对于SDF模型的建立是有一些非常便捷的方法的,若大家想深度研究,我们可以参考一下这个网站:

www.shadertoy.com/view/MsVGWG

RayMarching 可以制作许多丰富多彩的效果,ShaderToy 中的大部分效果都是用其实现的。

然而RayMarching 也有一个硬伤,因为它要基于每一个像素点做运算,当画布较大时,其计算量就会很大,导致电脑变卡。这也是为什么ShaderToy 里的很多案例还没打开,浏览器就崩了。

简单说一下,在我个人的认知中,RayMarching的应用场景:

  • 适合很难顶点建模,或者顶点建模很费面的动态场景,比如光晕、火焰、体积云、毛发等。
  • 适合单帧的高质量渲染,不适合连续渲染。如果你要在浏览器里开发一个基于RayMarching的编辑器,以此来建模或者设计某种效果,那就可以将编辑和渲染分离。就像那些建模软件一样,很流畅的建完模后,要花很长时间去渲染。

参考链接:

inter-illusion.com/assets/I2Sm…

微信