前面呢,我有说过,顶点着色器对于3D
非常的重要,并且算是必不可少的一环,跟片元着色器一样,二者缺一不可。
在3D
的世界里创建3D
的图形,在顶点着色器里计算它的光照,形状等要素,最后在片元着色器里计算它的颜色,并且把它显示在画面上。
但是随着我的深入学习,我发现,这些操作,好像,可以全部交给片元着色器来做的样子?!这样做的话,会发生什么呢??
作为我这番言语的证据,我先贴出我在 shadertoy 网站上看到的一个很厉害的案例:
显然,这是一个2D
平面,但是上面显示的是3D
的场景,充满了3D
的图形。这所谓的3D
图形完完全全是用的片元着色器写出来的!无论图形,光影,都跟顶点着色器没有丝毫的关系。
到底怎么办到的?能够允许我们在2D
的平面描绘出3D
的世界?
接下来,隆重介绍:Raymarching
,学名“光线步进”。
我们来探索探索它到底是什么神奇的东西吧~
启动模板
让我们回到2D
阶段最常使用的glsl
文件以及shader toy preview
。
并且以这段代码开始:
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 uv=fragCoord/iResolution.xy;
// fragColor=vec4(uv.x,0.,0.,1.);
// fragColor=vec4(0.,uv.y,0.,1.);
fragColor=vec4(uv,0.,1.);
}
第一个 Raymarching 场景
先画出一张示意图:
先简单说一下Raymarching
的流程:根据我个人的理解,它是先为相机指定了一个位置,把光线从相机所在的位置发射出来,光线会不断地前进,每次行进的时候会检测它是否和场景中的物体发生了碰撞,如果撞到了,就对撞到的物体进行进一步的着色处理(包括颜色赋值,计算光照等等),反之,如果没有撞到的话,就会让光线进行行进,直到所有的流程全部结束。
uv 居中
在最开始的课程里教过的,对uv
进行居中处理,方便之后的绘图:
uv = (uv - .5) * 2.;
uv.x *= iResolution.x / iResolution.y;
创建光线
我们来创建一束光线,先确定光线的起点ro
(Ray Origin),设置为vec3(0., 0., 1.)
;这个也可以说是相机的位置。
vec3 ro = vec3(0., 0., 1.);
接下来来计算光线的方向向量rd
(Ray Direction):
vec3 rd = normalize(vec3(uv, 0.) - ro);
这其中,这里的vec3(uv, 0.0)
创建了一个三维向量,表示屏幕上的点在3D
空间中的位置。减去相机起点ro
,得到一个从相机位置指向屏幕上的点的向量。最后,通过normalize
函数将这个向量归一化为单位向量。
三维的 SDF 函数
之前的文章里已经学习过SDF
函数的概念了,但是,我们之前学习的都是2D
的,而Raymarching
是一个3D
的时候,因此我们需要把这个SDF
函数也转换为3D
的。
记得吧?圆形的SDF
函数:
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
接受的参数里面,是一个 vec2 的二维参数,仅仅针对圆形的话,我们只需要把 vec2 改成 vec3 就完事儿了。
如果你需要别的图形,其他形状的 3D SDF 需要专门的公式,还是 iq 大佬的博客可以查询到基本上所有的 3D 形状。
float sdShape(vec3 p, float r) {
return length(p) - r;
}
这样,我们就得到了一个3D
球体的SDF
函数。
定义一个map
函数,我们把接下来的SDF
函数的结果都存放在其中,比如这个球体就是:
float map (vec3 p) {
float shape = sdShape(p - vec3(0., 0., -2.), 1.);
return shape;
}
在map
函数中,用球体的SDF
函数创建一个球体shape
,位置设定为(0., 0., -2.)
,半径设置为 1。
关于这边为什么要定为-2?
因为实际上就是相当于如果把第三维度定为 0 的话,会让检测函数在最开始就检测到碰撞到了物体,这样一来场景直接成为纯色了,不过如果设置为-1 的话整体会大一圈,方便观察
光线步进
光线尚未行进的时候,渲染结果里面什么都没有,输出的颜色都是默认的黑色:
vec3 col = vec3(0.);
float depth = 0.;
for (int i = 0; i < 64; i++) {
vec3 p = ro + rd * depth;
float dist = map(p);
depth += dist;
if (dist < 0.001) {
col = vec3(1.);
break;
}
}
fragColor=vec4(col,1.);
我们先设定行进距离depth
为 0,设定一个循环,让光线从相机里射出来,并且不断地向前步进,当前前进的位置p
就是等于原点+光线向量*行进距离的乘积,前进的同时计算SDF
函数计算输出的距离dist
,一起累加到行进距离depth
上,当输出的dist
无线趋近于 0 的时候,就说明光线触碰到物体了,这个时候终止掉光线,并且给光线碰到的物体赋予颜色,这里使用的是白色。
这时,画面上就会出现一个白色的圆形,其实这玩意儿是一个球体,但是因为没有光照,所以看上去像是个圆形。
光照
在光照模型的章节,我们学会了如何给一个3D
物体添加光照,
而Raymarching
也是通过这种方式来添加的,只不过,稍微有了一些小小的变化,因为我们放弃了顶点着色器(严格来说其实可以说是用顶点着色器来固定了覆盖整个屏幕的平面的位置),自然也就没有了法向量这个关键属性,但是,我们可以在Raymarching
里主动把法向量算出来,在iq
博客中,有一篇文章给出了法向量的计算公式,可以直接搬来用:把其中的f
函数替换成上面我们声明的map
函数。
vec3 calcNormal(vec3 p) {
const float h = .0001;
const vec2 k = vec2(1., -1.);
return normalize(k.xyy * map(p + k.xyy * h)
+ k.yyx * map(p + k.yyx * h)
+ k.yxy * map(p + k.yxy * h)
+ k.xxx * map(p + k.xxx * h));
}
在光线碰撞之后计算出 normal,要注意,没有光照的时候,要把 col 设置为黑色。
if (dist < 0.001) {
// col = vec3(1.);
col = vec3(0.);
vec3 normal = calcNormal(p);
break;
}
然后咱们把之前的Blinn-Phong
光照模型的代码搬过来,把其中的vWorldPosition
换成p
。vNormal
换成我们计算出来的normal
。
float sdShape(vec3 p, float r) {
return length(p) - r;
}
float map (vec3 p) {
float shape = sdShape(p - vec3(0., 0., -2.), 1.);
return shape;
}
vec3 calcNormal(vec3 p) {
const float h = .0001;
const vec2 k = vec2(1., -1.);
return normalize(k.xyy * map(p + k.xyy * h)
+ k.yyx * map(p + k.yyx * h)
+ k.yxy * map(p + k.yxy * h)
+ k.xxx * map(p + k.xxx * h));
}
void mainImage(out vec4 fragColor,in vec2 fragCoord){
vec2 uv=fragCoord/iResolution.xy;
uv = uv * 2.0 - 1.0;
uv.x *= iResolution.x / iResolution.y;
vec3 ro = vec3(0.,0.,1.);
vec3 rd = normalize(vec3(uv, 0.) - ro);
vec3 col = vec3(0.);
float depth = 0.;
for (int i = 0; i < 64; i++) {
vec3 p = ro + rd * depth;
float dist = map(p);
depth += dist;
if (dist < 0.001) {
// col = vec3(1.);
col = vec3(0.);
vec3 normal = calcNormal(p);
vec3 objectColor=vec3(1.);
vec3 lightColor=vec3(.875,.286,.333);
// 环境光
float ambIntensity=.2;
vec3 ambient=lightColor*ambIntensity;
col+=ambient*objectColor;
// 漫反射
vec3 lightPos=vec3(0., 0., 5.);
vec3 lightDir=normalize(lightPos-p);
float diff=dot(normal,lightDir);
diff=max(diff,0.);
vec3 diffuse=lightColor*diff;
col+=diffuse*objectColor;
// 镜面反射
vec3 reflectDir=reflect(-lightDir,normal);
vec3 viewDir=normalize(cameraPosition-p);
float spec=dot(viewDir,reflectDir);
spec=max(spec,0.);
float shininess=32.;
spec=pow(spec,shininess);
vec3 specular=lightColor*spec;
col+=specular*objectColor;
break;
}
}
// fragColor=vec4(uv.x,0.,0.,1.);
// fragColor=vec4(0.,uv.y,0.,1.);
fragColor=vec4(col,1.);
}
上面就是总代码了,可以看到物体成功的被光照影响了,变成了一个实在的球。