不只是面部阴影,SDF的应用非常广泛,不如说SDF是制作平滑形变或者平面模拟立体物体的一种泛用思路,哪怕是为了看懂shadertoy上那些硬要用shadertoy渲染3D效果的人是怎么做出来的,也有探索的必要。

本文也算是个人对SDF一个粗浅的理解吧。

从面部阴影说起的对SDF的粗浅理解

面部阴影

原神的面部SDF制作阴影变化已经是非常常见的手法了,有很多篇文章都有讲解其中原理的,也有讲如何具体生成这张SDF图的。

链接都可以在参考链接找到,这里也再发一下,从这些文章中学到了很多:

https://zhuanlan.zhihu.com/p/279334552

https://zhuanlan.zhihu.com/p/361716315

https://zhuanlan.zhihu.com/p/337944099

SDF的了解,物体间的线性插值?

在挺久之前第一次做面部阴影的时候我在百度上搜索SDF的含义,了解到了有向距离场,但是完全不知道它和我手上这张SDF图有什么关系,随后抄到了代码之后也还是一知半解。

之后看了很多shadertoy上的sdf+raymarching,了解到了SDF的在变形中起到的具体作用。

sdf的定义非常简单,我自己理解下来就是:将一个空间表示为空间中的每个点到某个物体的最近距离,如果在物体内则表示为负,在物体外则为正(其实反过来也不要紧),这样表示方法下的空间称为有向距离场。

如果是在一个3D空间中要表示一个点相对于一个带有不规则形状物体的信息,可以有很多种表示方法,实际上我们通常使用的positionOS就是一个表示方法——空间中的三角形通过插值得到了一个positionOS,它包含了这个点相对于该物体处在什么位置,通过这个位置,shader得以计算各种东西,尽管在原来的模型中可能根本没有这个位置对应的顶点,但对于空间中的任意一点,它都能插值出一个positionOS,这也是为什么我们在unity或者ue里显然不会用raymarching+sdf去渲染一个简单的模型。

但如果是要表示一个点相对于两个这样的物体的中间形态信息呢?先解释一下这里所说的“中间形态”,中间形态指的是从一个状态到另一个状态的过程,比如说一个圆形变成一个方形,萝莉时期的emiteinna都知道什么是圆形什么是方形,但如果问我什么是lerp(圆形,方形,0.5),那我就说不上来,因为我很难用一个具体的公式去表示这个东西。

而SDF通过距离场来表示空间中的一个点相对物体的关系,我们都知道怎么去计算空间中的一个点到球表面的距离,以及怎么判断它是否在球内,同样对于正方体也是一样,而我们得到的是两个距离值dis_sphere,dis_cube,这时候如果来求lerp(dis_sphere,dis_cube,0.5)就相当容易,显然是这两个值的平均值。而我们知道空间中任何一点都能以sdf的方式表示它和物体的关系,所以通过这种方法,我们成功地求出了lerp(圆形,方形,0.5)是一个什么东西。

那么是一个什么东西呢?

公式

附上shadertoy代码:

代码
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

vec3 GetRD(vec2 uv,float z){
uv=uv-0.5;
uv.y*=iResolution.y/iResolution.x;
return normalize(vec3(uv,z));
}
float sdfCircle(vec3 center,float radius,vec3 p){
return length(p-center)-radius;
}
float sdfCube(vec3 center,vec3 a,vec3 p){
vec3 rp=p-center;
float x=sign((rp.x-a.x)*(rp.x+a.x))*min(abs(rp.x-a.x),abs(rp.x+a.x));
float y=sign((rp.y-a.y)*(rp.y+a.y))*min(abs(rp.y-a.y),abs(rp.y+a.y));
float z=sign((rp.z-a.z)*(rp.z+a.z))*min(abs(rp.z-a.z),abs(rp.z+a.z));
float sm=0.0;
if(x>0.)sm=sqrt(sm*sm+x*x);
if(y>0.)sm=sqrt(sm*sm+y*y);
if(z>0.)sm=sqrt(sm*sm+z*z);
if(sm==0.0)sm=max(x,max(y,z));
return sm;
}
vec3 rayMarching(vec3 ro,vec3 rd,vec3 color,vec3 background){
float ds=0.;
vec3 result=vec3(1,1,1);
float mxdist=50.;
float t=sin(iTime*0.7)*0.5+0.5;
for(float i=0.;i<=mxdist;i++){
if(ds>mxdist)return background;
vec3 p=ro+rd*ds;
float circ=sdfCircle(vec3(0.,0.,0.),1.,p);
float cub=sdfCube(vec3(0.,0.,0.),vec3(1.4,1.4,1.4),p);
float sdf=cub+(circ-cub)*t;
ds+=sdf;
if(sdf<0.01){
return color*result;
}
}
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv=fragCoord/iResolution.xy;
vec3 rd=GetRD(uv,5.);
vec3 ro=vec3(0.,0.,-30.);
fragColor=vec4(rayMarching(ro,rd,vec3(1.,1.,1.),vec3(0.,0.,0.)),1.);

}

在这张动图中,显然我们成功找到了lerp(球形,正方体,[0,1])。

再谈面部阴影,如何生成sdf图

为什么要用sdf去做面部阴影,是为了自定义一个面部阴影的形状让它更加可控,在此基础上让这个阴影能在指定的形状之内平滑变化。这不就是我们刚刚做的事情吗?

但是在shader中,我们不可能像计算空间一点到球体表面距离一样去求一个点到阴影块的距离,无论是从算力上讲,还是从如何用公式去表示点到一个不规则物体的距离上讲,因此sdf图担任起了这个职责。

曾经在may佬群里问过关于面部阴影为什么要用角度去做采样的x值,当时没有得到回复(呜呜),不过现在自己已经有了一个差不多的理解了。

sdf图中的灰度表示的是一个阈值,这个阈值以上的值统一计算为1,以下为0,类似于SD中的threshold。而这个阈值的含义就是上述举例中的“从一个图形到另一个图形的变化程度”。

引用一个用SD生成面部阴影SDF图的帖子:https://zhuanlan.zhihu.com/p/546880604

这里大佬选择用non uniform blur来做模糊,我当时会想,这真的是一个精确的sdf值吗?

但其实哪怕不是又无所谓呢?重新审视最开始的需求:自定义一个面部阴影的形状让它更加可控,在此基础上让这个阴影能在指定的形状之内平滑变化。实际上我并不需要保证这个平滑变化一定负责某种规律,它只需要平滑就可以了。而SDF之所以能完成任务,也是由于点到物体的距离这个东西,它一定是在固定物体情况下平滑变化的值。

于是再去理解sdf图的灰度值是什么意思:在有向距离场中,当前像素的值为lerp(A,B,x),而sdf图的灰度值就表示这中间的x,由于non uniform blur生成的灰度变化显然是平滑的,那么我们用它做sdf图做出来的效果显然也是平滑的。

总结一下,如何生成A-B的sdf图:只要保证偏向A的区域为1,偏向B的区域为0,中间平滑变化即可。

多张SDF的拼接

面部阴影的sdf显然并不是简单的从A到B的变化,而是从A到B再到C的变化。甚至这个变化可以无限叠加。那么这个部分又应该怎么完成呢?

SD有个最简单的功能叫做blend。

如果从A-B时,sdf图灰度的值域为[0,1],表示的是接近A的程度,1是到达A,0是到达B。那么当A-B-C时,A-B之间的值域显然就不能再是[0,1]了,这样的话C就没得表示了。那么应该是多少呢?这取决于变化的坡度,如果说B刚好处在整个变化的中间位置,那么很显然,A-B的值域就是[0.5,1],而B-C的值域是[0,0.5],非常容易理解。

如图,我把三张SDF图(表示ABCD四个状态)用SDblend在一起,很容易就能计算得第一个blend的opacity为0.5,第二个为0.66。

当然从这里也可以看出一个问题:SDF图可以拼接的条件是每一个像素在SDF变化的过程中必须是单调变化的。如果在A的时候某个像素为1,B时为0,C时又为1了,那显然这个映射就不成立了嘛。

图

不止是欧拉距离,曼哈顿距离呢?

其实刚刚已经提到了。

面部阴影的sdf图已经可以不止是sdf图了,哪怕我完全不去计算所谓的点到表面的距离也无所谓,于是突发奇想,在matlab上我开了一个sdf计算,直接用曼哈顿距离来生成一张sdf图。

附上matlab代码,就是很简单的BFS。

代码
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

width=512;
height=512;
%
sdf1=imread('SDF_03.bmp');
sdf1=rgb2gray(sdf1);
for i=1:height
for j=1:width
if sdf1(i,j)>0
sdf1(i,j)=255;
end
end
end
%
sdf2=imread('SDF_04.bmp');
sdf2=rgb2gray(sdf2);
for i=1:height
for j=1:width
if sdf2(i,j)>0
sdf2(i,j)=255;
end
end
end
%
qx=zeros(width*height+5,1);
qy=zeros(width*height+5,1);
qr=zeros(width*height+5,1);
sdfres=zeros(width,height);
%
left=1;
right=0;
vis=zeros(height,width);
for i=1:height
for j=1:width
if sdf1(i,j)==255
sdfres(i,j)=0;
vis(i,j)=1;
right=right+1;
qx(right,1)=i;
qy(right,1)=j;
qr(right,1)=0;
end
end
end

while left<=right
x=qx(left,1);
y=qy(left,1);
r=qr(left,1)+1;
%
dx=-1;
dy=1;
gx=x+dx;
gy=y+dy;
if gx>=1&&gx<=height&&gy>=1&&gy<=width&&vis(gx,gy)==0
vis(gx,gy)=1;
sdfres(gx,gy)=r;
right=right+1;
qx(right,1)=gx;
qy(right,1)=gy;
qr(right,1)=r;
end
%
dx=1;
dy=1;
gx=x+dx;
gy=y+dy;
if gx>=1&&gx<=height&&gy>=1&&gy<=width&&vis(gx,gy)==0
vis(gx,gy)=1;
sdfres(gx,gy)=r;
right=right+1;
qx(right,1)=gx;
qy(right,1)=gy;
qr(right,1)=r;
end
%
dx=1;
dy=-1;
gx=x+dx;
gy=y+dy;
if gx>=1&&gx<=height&&gy>=1&&gy<=width&&vis(gx,gy)==0
vis(gx,gy)=1;
sdfres(gx,gy)=r;
right=right+1;
qx(right,1)=gx;
qy(right,1)=gy;
qr(right,1)=r;
end
%
dx=-1;
dy=-1;
gx=x+dx;
gy=y+dy;
if gx>=1&&gx<=height&&gy>=1&&gy<=width&&vis(gx,gy)==0
vis(gx,gy)=1;
sdfres(gx,gy)=r;
right=right+1;
qx(right,1)=gx;
qy(right,1)=gy;
qr(right,1)=r;
end

left=left+1;
end
for i=1:height
for j=1:width
if sdf2(i,j)==0
sdfres(i,j)=0;
end
end
end
sdfres=(sdfres-min(sdfres(:)))*(255.0-0)/(max(sdfres(:))-min(sdfres(:)))+0;
for i=1:height
for j=1:width
if sdf2(i,j)==0
sdfres(i,j)=0;
else
sdfres(i,j)=255-sdfres(i,j);
end
end
end
sdfres=uint8(sdfres);
imwrite(sdfres,'SDFout2.bmp');
b=imread('SDFout2.bmp');
imshow(b);


得到的三张SDF图用SD去进行blend,然后塞进shadertoy,看一看能变换出什么样的结果。

图

附上sdf图

图

可以发现曼哈顿距离和欧拉距离相比还是有些差距的,同时混合也需要通过技巧去保证每个部分变化的速率相同,但做出来的效果显然也是平滑变化的。

也算是进行了一个小小的科研吧。