1-距离场
1-1-基本概念
SDF是Signed-distance-field的简写,译作有向距离场,它是一个记录模型中心点到模型表面的最小距离的函数。
以球体为例解释一下SDF的概念。
球体的SDF函数是:
f(p)=|p-o|-r
- p 空间内部任意一点
- o 球心
- r 球体的半径
通过这个函数,我们可以明显的看出:
- f(p)>0 时,p在球外
- f(p)=0 时,p在球上
- f(p)<0 时,p在球内
1-2-距离场的作用
- 绘制图形
- 描边
- 光晕
- 投影
- 内阴影
- 浮雕效果
- 光效
- 变形
- 建模
接下来我们说一下SDF模型是如何显示的。
2-RayMarching
使用RayMarching 算法可以将SDF模型显示出来。
RayMarching 译作光线步进,或者射线推进,其意思就是让射线的起点沿着射线方向逐步推进,每次推进的距离就是射线起点到SDF模型的距离。
利用RayMarching 显示模型的步骤:
- 将相机视点作为射线的起点。
- 在相机前面放一张栅格图像。
- 以相机视点为起点,向栅格图像的每个栅格做射线。
- 让相机视点沿射线方向进行逐步推进,每次推进的距离都是相机视点到SDF模型表面的距离。
3-RayMarching 代码实现
3-1-环境搭建
1.在vscode中安装一个Shader Toy
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
这样便可以在Shader Toy的显示窗口中看见一张蓝色的画布。
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);
}
效果如下:
现在球体已经显示出来了。
这个球体的法线我们根据其SDF模型的特性,也可以很容易获取。
有了法线,我们再建立一个平行光,那就可以绘制一个有体感的球体。
再向其中添加一个视线,便可以算出球体的高光。
对于其具体算法,可以参考我们在WebGL里讲过的光。
总结
对于SDF模型的建立是有一些非常便捷的方法的,若大家想深度研究,我们可以参考一下这个网站:
RayMarching 可以制作许多丰富多彩的效果,ShaderToy 中的大部分效果都是用其实现的。
然而RayMarching 也有一个硬伤,因为它要基于每一个像素点做运算,当画布较大时,其计算量就会很大,导致电脑变卡。这也是为什么ShaderToy 里的很多案例还没打开,浏览器就崩了。
简单说一下,在我个人的认知中,RayMarching的应用场景:
- 适合很难顶点建模,或者顶点建模很费面的动态场景,比如光晕、火焰、体积云、毛发等。
- 适合单帧的高质量渲染,不适合连续渲染。如果你要在浏览器里开发一个基于RayMarching的编辑器,以此来建模或者设计某种效果,那就可以将编辑和渲染分离。就像那些建模软件一样,很流畅的建完模后,要花很长时间去渲染。
参考链接:
inter-illusion.com/assets/I2Sm…