Shadertoy学习 vol. 6 SDF变换,半兰伯特光照,软阴影和移动相机

result

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(p - vec3(1, -0.5, 2), vec3(0.5, 0.5, 0.5)), 0.5, 0.5, 1));
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;
//dif = clamp(normalize(vec3(98, 146, 226)) * uv.y + 0.1 * 4.5, 0.0, 1.0); //Sky color
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)//floor color
{
dif -= float((int(p.x+100.0) % 2) ^ (int(p.z+100.0)) % 2) * 0.1;

}
#endif

return dif;
}

颜色的渲染直接用光线强度乘上代表颜色的c就好了。我们这里用到了半兰伯特光照模型,解决了光线背面部分过暗的问题。至于为什么是一半,其实无所谓。这只是经验公式,最早是VALVE的工程师提出来的。你当然也可以设计自己的光照模型。

到现在这一步你就应该可以看到物体颜色了。
Half Lambert with color

这里的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;
}

Hard Shadow
我们从物体表面ro出发,向光线方向rd做raymarch,碰到物体返回0.4做阴影强度;没碰到则返回1.0,强度不变。现在多出来的mintmaxt是为了让光线只在一定范围内检测遮挡物体,可以减少运算量,也方便调整效果。

我们现在来加上软阴影效果,也就是让阴影的轮廓变得更加柔和。这能增加我们渲染的真实感,因为现实生活中多光源和漫反射的存在,阴影不一定有明显的轮廓。
这里上一张图,说明一下软阴影的基本原理。
Explanation
对于原来的硬阴影,我们依旧保留,并称它为本影区。我们在本影区的外围再加上一层半影区。之前我们对于本影区会严格要求光线只有被遮挡后才会产生阴影,但对于半影区我们可以不用了。

我们还是从物体表面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),就可以看到效果了。
Result

可以发现我们的软阴影在物体表面表现的不是很好,我打算在后面解决。现在我们可以先作一下弊,让软阴影只在地板上投射,高于地板的地方我们用硬阴影替代或者索性不用阴影。我们对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)//floor color
{
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
{
//dif *= shadow(p, l, 0.1, 5.0);
}
#endif

return dif;

同时我也把k从8调整到了6,增强了软阴影的效果,大家可以对比着看一下。
Stronger softshadows

移动相机

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

再放一张图让大家理解一下,
Camera
其实我们这里不需要考虑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。我先旋转了uvcameraP,再加上了我定义的位移向量trs。那么最后我们渲染时的光线方向就是新的uv - cameraP了,不要忘记正则化。

然后我们的相机就可以跟着我们的鼠标转动了,如果你还想让相机位移,改变trs向量即可。
低下了头

结尾

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

Comments