
SDF的平移旋转和颜色
SDF(Signed Distance Function), 顾名思义,是计算距离的函数。之前我们有提到了球体的SDF。但其实还有很多别的形状的SDF。
这里推荐大家去看iq大神的博客,他就是建立了Shadertoy这个网站的人。Inigo Quilez
上次我们给RayMarching渲染添加了基础的内容,但是还没有颜色。我们在raymarching这一步里添加上碰撞时的颜色信息,不只返回距离值,还返回最近物体的RGB颜色值。这里图方便我就直接用vec4()
了,如果想要更规范一点也可以定义一个结构体。我们添加一个新的函数MinDist()
,这里传入的两个vec4
是SDF返回的距离和物体的RGB值。RGB值要在这一步处理,要不然碰撞时我们就不知道究竟是碰到什么颜色的物体了。
这里我们的getDist()
函数也变了,让我们先把SDF函数写到外面去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| vec4 MinDist(vec4 a, vec4 b) { return a.x < b.x ? a:b; }
mat3x3 rotateY(in float theta) { return mat3x3 ( 1, 0, 0, 0, cos(theta), -sin(theta), 0, sin(theta), cos(theta) ); }
mat3x3 rotateX(in float theta) { return mat3x3 ( cos(theta), 0, sin(theta), 0, 1, 0, -sin(theta), 0, cos(theta) ); }
float sdPlane(vec3 p) { return p.y; }
float sdSphere( vec3 p, float s ) { return length(p)-s; }
float sdBox( vec3 p, vec3 b ) { vec3 q = abs(p) - b; return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); }
float sdOctahedron( vec3 p, float s) { p = abs(p); float m = p.x+p.y+p.z-s; vec3 q; if( 3.0*p.x < m ) q = p.xyz; else if( 3.0*p.y < m ) q = p.yzx; else if( 3.0*p.z < m ) q = p.zxy; else return m*0.57735027;
float k = clamp(0.5*(q.z-q.y+s),0.0,s); return length(vec3(q.x,q.y-s+k,q.z-k)); }
vec4 getDist(vec3 p) { vec4 d = vec4(1e10, 0.0, 0.0, 0.0); mat3x3 rotateX = rotateX(iTime); mat3x3 rotateY = rotateY(iTime);
d = minDist(d, vec4(sdSphere(p - vec3(-1, -0.5, 2.0), 0.5), 1.0, 0.83, 0.4)); d = minDist(d, vec4(sdPlane(p), 1.0, 1.0, 1.0)); d = minDist(d, vec4(sdOctahedron(p - vec3(3, cos(iTime), -1.0), 1.0), 0.5, 0.2, 0.6)); d = minDist(d, vec4(sdBox(rotateX * rotateY * p - rotateX * rotateY * vec3(0.6, 0.5, 2), vec3(0.6, 0.6, 0.6)), 0.5, 0.5, 1)); return d; }
|
调用sdf函数的时候减去位移向量就可以移动我们的物体,为了旋转我们定义了x轴和y轴的旋转矩阵,不会的可以回家补习线性代数,然后左乘位置向量就是绕着世界坐标系旋转。为了在自身坐标系下旋转,我们需要在变换后再减去位移旋转矩阵左乘位移向量的值。
rotateY(iTime)
里的iTime
也可以填角度的值,不过我想让它一直转而已。
半兰伯特光照模型
我们的getDist()
现在返回的是vec4
了,所以我们的渲染函数也要改变一下。上次的getLight()
被我们修名成render()
,不再传进点的位置,而是传进光线起点ro
和光线方向rd
,这会方便我们在渲染函数里调用我们的软阴影。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| vec3 render(vec3 ro, vec3 rd, vec2 uv) { vec3 dif; float d = rayMarch(ro, rd); vec3 p = ro + rd * d;
if (d > 50.0) { dif = vec3(0.38, 0.57, 0.88) - max(rd.y,0.0)*0.8; return dif; }
vec3 lightPos = vec3(2, 15, 2); lightPos.x += cos(iTime) * 8.0; lightPos.z += sin(iTime) * 8.0;
vec3 l = normalize(lightPos - p); vec3 c = vec3(1.0); vec3 n = getNormal(p, c);
#ifdef HALF_LAMBERT float nDot = pow(0.5 * dot(n, l) + 0.5, 2.0); #else float nDot = dot(n, l); #endif
dif = vec3(1.0, 1.0, 0.9) * c * clamp(nDot, 0.0, 1.0);
#ifdef FLOOR_GRID if (p.y < -0.98 || abs(p.x)> 20.0 || abs(p.z) > 20.0) { dif -= float((int(p.x+100.0) % 2) ^ (int(p.z+100.0)) % 2) * 0.1;
} #endif
return dif; }
|
颜色的渲染直接用光线强度乘上代表颜色的c
就好了。我们这里用到了半兰伯特光照模型,解决了光线背面部分过暗的问题。至于为什么是一半,其实无所谓。这只是经验公式,最早是VALVE的工程师提出来的。你当然也可以设计自己的光照模型。
到现在这一步你就应该可以看到物体颜色了。

这里的if (d > 50.0)
的条件是为了判断距离太远时渲染天空的,可以根据自己想要的效果适当调整。
软阴影
我们之前添加阴影的方法当用作在现在的渲染函数里时,需要传入当前点的位置和光线方向,如果还是之前的逻辑的话,我们可以写出以下代码:
1 2 3 4 5 6 7 8 9 10
| float shadow(in vec3 ro, in vec3 rd, float mint, float maxt) { for(float t = mint; t < maxt;) { float h = getDist(ro + rd * t).x; if(h <0.001) return 0.4; t += h; } return 1.0; }
|

我们从物体表面ro
出发,向光线方向rd
做raymarch,碰到物体返回0.4做阴影强度;没碰到则返回1.0,强度不变。现在多出来的mint
和maxt
是为了让光线只在一定范围内检测遮挡物体,可以减少运算量,也方便调整效果。
我们现在来加上软阴影效果,也就是让阴影的轮廓变得更加柔和。这能增加我们渲染的真实感,因为现实生活中多光源和漫反射的存在,阴影不一定有明显的轮廓。
这里上一张图,说明一下软阴影的基本原理。

对于原来的硬阴影,我们依旧保留,并称它为本影区。我们在本影区的外围再加上一层半影区。之前我们对于本影区会严格要求光线只有被遮挡后才会产生阴影,但对于半影区我们可以不用了。
我们还是从物体表面ro
出发,向光线方向rd
做raymarch,然后设任意一点到物体的最短距离为d
,光线走过的距离为t
。那么根据常识来考虑,这一点的半影强度应该与t成正比,与d成反比。最近距离(d)越小,阴影越强。最近距离发生距离(t)越大,阴影越强。t这里我再解释一下,因为阴影是越靠进物体越强,越远离物体越弱,所以t与阴影强度是成正比的。
最后由于阴影系数越小,阴影越强,所以应该把t / d
反过来,变成d / t
。然后我们就可以写出代码了。这里我们还添加了系数k,用于调整软阴影强度。本影的return 0.3
和半影的max(0.3, )
是因为我不想让阴影是一篇死黑,你也可以自行调整。
1 2 3 4 5 6 7 8 9 10 11 12
| float softshadow(in vec3 ro, in vec3 rd, float mint, float maxt, float k) { float res = 1.0; for (float t=mint; t<maxt;) { float d = getDist(ro + rd * t).x; if (d < 0.001) return 0.3; res = max(0.3, min(res, k * (d / t))); t += d; } return res; }
|
现在再让我们向渲染函数的最后添加上dif *= softshadow(p, l, 0.02, 10.0, 8.0)
,就可以看到效果了。
。
可以发现我们的软阴影在物体表面表现的不是很好,我打算在后面解决。现在我们可以先作一下弊,让软阴影只在地板上投射,高于地板的地方我们用硬阴影替代或者索性不用阴影。我们对render()
稍作修改:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #ifdef FLOOR_GRID if (p.y < -0.98 || abs(p.x)> 20.0 || abs(p.z) > 20.0) { dif -= float((int(p.x+100.0) % 2) ^ (int(p.z+100.0)) % 2) * 0.1; dif *= softshadow(p, l, 0.02, 10.0, 6.0); } else { } #endif
return dif;
|
同时我也把k从8调整到了6,增强了软阴影的效果,大家可以对比着看一下。

移动相机
我们的相机一直都是固定的,都没有点互动,实在是太无聊了。我们接下来干点有意思的,让我们可以用鼠标转动相机。具体操作其实也该移动旋转SDF差不多,不过我们需要同时移动相机起点和目标像素(uv)。
再放一张图让大家理解一下,

其实我们这里不需要考虑CameraDir
,只要直接变换uv
就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void main() { mat3x3 rotateX = rotateX((-iMouse.x / iResolution.x) * 20.0); mat3x3 rotateY = rotateY((iMouse.y / iResolution.y) * 2.0);
vec2 uv2 = (gl_FragCoord.xy - 0.5f * iResolution.xy) / iResolution.y; vec3 uv = vec3(uv2, 1.0); vec3 cameraP = vec3(0.0, 0.0, 0.0);
uv = rotateX * rotateY * uv; cameraP = rotateX * rotateY * cameraP;
vec3 trs = vec3(0, 0, -2);
cameraP += trs; uv += trs; vec3 rd = normalize(uv - cameraP);
vec3 color = render(cameraP, rd, uv.xy); gl_FragColor = vec4(color, 1.0); }
|
我创建了两个旋转矩阵,把鼠标坐标作为输入,然后把二维的uv2
转换成三维的uv
,又定义了cameraP
。我先旋转了uv
和cameraP
,再加上了我定义的位移向量trs
。那么最后我们渲染时的光线方向就是新的uv - cameraP
了,不要忘记正则化。
然后我们的相机就可以跟着我们的鼠标转动了,如果你还想让相机位移,改变trs
向量即可。

结尾
下次我们有可能会继续完善这个渲染程序,也可能来讲讲噪音和地形生成,尽请期待。