游戏蛮牛学习群(纯技术交流,不闲聊):539178957
游戏蛮牛 手机端
开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2286

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 1284|回复: 56

[慕容小匹夫] Signed Distance Field Shadow in Unity

[复制链接]  [移动端链接]
排名
2077
昨日变化

54

主题

297

帖子

3080

积分

Rank: 9Rank: 9Rank: 9

UID
44527
好友
61
蛮牛币
2621
威望
0
注册时间
2014-9-12
在线时间
578 小时
最后登录
2018-8-16

专栏作家活力之星认证开发者

发表于 2018-6-14 13:17:43 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
本帖最后由 慕容小匹夫 于 2018-6-14 15:03 编辑

屏幕快照 2018-06-10 下午3.38.31.png

0x00 前言
最近读到了一个今年GDC上很棒的分享,是Sebastian Aaltonen带来的利用Ray-tracing实现一些有趣的效果的分享。

其中有一段他介绍到了对Signed Distance Field Shadow的改进,主要体现在消除SDF阴影的一些artifact上。

第一次看到Signed Distance Field Shadow是在大神Inigo Quilez的博客上,较传统的阴影实现方式,例如shadow map,视觉效果要好很多。可以看到下图中物体的阴影随着距离由近到远也逐渐由清晰渐渐过渡到模糊的效果,表现更加自然而真实。

相比较而言,Unity中的阴影实现效果就简单并且死板了许多。
下面我们就在Unity中来实现RayMarching,并利用SDF绘制一些简单的物体,最后实现一下阴影的效果。

0x01 在Unity中实现SDF
首先,RayMarching算法处理的是屏幕上的每一个像素,因此在Unity中我们自然而然会想到利用屏幕后处理的方式来实现RayMarching。
所以,RayMarching的主要逻辑都在Fragment Shader内实现,而Vertex Shader则主要用来获取顶点属性中所保存的射线信息,之后经过插值传入Fragment Shader中,供每一个Fragment来使用。此时整个屏幕是一个四边形,一共有4个顶点,这4个顶点就可以用来记录屏幕上的4根射线,而这4根射线的方向就可以直接取摄像机的平截头体的4条边的方向,之后再经过插值生成射向某个片元的射线。
这里我们可以直接调用Unity提供的Camera.CalculateFrustumCorners方法,这里是相关文档(https://docs.unity3d.com/ScriptReference/Camera.CalculateFrustumCorners.html)。
下面是这个方法的签名:
[AppleScript] 纯文本查看 复制代码
public void CalculateFrustumCorners(Rect viewport, float z, 
              Camera.MonoOrStereoscopicEye eye, Vector3[] outCorners);
其中作为我们需要的4个outCorners也是作为参数传入这个方法的。不过需要注意的是该方法获取的平截头体的4条边是在local space的,所以我们需要将它们转移到world space,以供Fragment Shader中使用。
这样我们就得到了4个向量,但是这4个向量要怎么向Shader中传递效率才高呢?如果每一个向量传递一次,则效率并不高。所以这里我们使用一个矩阵来保存这4个向量,而向shader中传送数据就只需要传送一个矩阵。
[AppleScript] 纯文本查看 复制代码
    Transform camtr = cam.transform;
    Vector3[] frustumCorners = new Vector3[4];
    cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), 
        cam.farClipPlane, cam.stereoActiveEye, frustumCorners);
    var bottomLeft = camtr.TransformVector(frustumCorners[0]);
    var topLeft = camtr.TransformVector(frustumCorners[1]);
    var topRight = camtr.TransformVector(frustumCorners[2]);
    var bottomRight = camtr.TransformVector(frustumCorners[3]);

    Matrix4x4 frustumCornersArray = Matrix4x4.identity;
    frustumCornersArray.SetRow(0, bottomLeft);
    frustumCornersArray.SetRow(1, bottomRight);
    frustumCornersArray.SetRow(2, topLeft);
    frustumCornersArray.SetRow(3, topRight);
    return frustumCornersArray;
射线的数据准备好了,向shader中传送数据在Unity中也十分简单,只需要调用SetMatrix就好。但是这里又出现了一个新的问题,那就是shader如何正确的确定它所处理的是哪根射线呢?如果不能确定顶点所对应的射线,那么之后的插值结果就不会正确。所以在Vertex Shader中我们需要一个Index来从传入的矩阵中正确的取出射线方向。
那么Index要如何确定呢?
聪明的你一定想到了,对一个四边形来说,它的UV数据是很有规律的。所以我们就可以在Vertex Shader中利用UV数据来确定正确的射线:
[AppleScript] 纯文本查看 复制代码
    index = v.uv.x + (2 * o.uv.y);
    o.ray = _Corners[index].xyz;
OK,之后只要在Fragment Shader中使用经过插值的ray数据,就能获取当前Fragment所对应的射线方向了。到此,我们已经将射线引入了Shader中。
接下来我们来定义一个SDF,使用SDF来定义我们将要渲染的内容。我们可以在Inigo Quilez的博客上获取很多常见物体的SDF定义,链接在这里:(http://.org/www/articles/distfunctions/distfunctions.htm)。
下面我们就在Unity中利用SDF渲染一个六棱体:
[AppleScript] 纯文本查看 复制代码
float sdHexPrism( float3 p, float2 h )
{
    float3 q = abs(p);
    return max(q.z-h.y,max((q.x*0.866025+q.y*0.5),q.y)-h.x);
}
针对不同的物体定义都需要一个SDF来描述该物体,但是如果在我们的RayMarching算法中每次想要渲染不同的形状时都要修改一下SDF的话似乎十分不方便,所以通常我们还会定义一个更高层的抽象——也可以叫做SDF函数——这个函数常常被称作map,它的输入是一个点坐标,输出则是该点距离SDF所定义的物体表面的最近距离。
而有了map这个高层的抽象,我们可以很方便的在map的内部实现中按照自己的需求修改SDF,例如将一些基础的物体进行合并、拆分等等。从这个角度讲,map其实定义了我们要渲染的整改场景,因此正个场景的信息我们是已知的,这一点在之后渲染阴影的时候会用到。
不过,我们还是先来看一个简单的例子,下面就是我们画六棱体的例子中所使用的map的定义:
[AppleScript] 纯文本查看 复制代码
        float map(float3 rp)
        {
            float ret = sdHexPrism(rp, float2(4, 5));

            return ret;
        }
之后我们在Fragment Shader中实现该Fragment上的RayMarching逻辑,在引入SDF之后,RayMarching的每一次Marching的距离就可以根据SDF的结果来设定了,我想大家应该都见过类似这样的图解:
可以看到,每一次marching的距离就是当前采样点到SDF定义的表面的最近距离,直到采样点和表面重合,即光线和表面相交了。
所以我们只需要在Fragment Shader中跑一个for循环,每一次迭代都调用一次map来确认当前采样点距离SDF的最近距离surfaceDistance,如果surfaceDistance不为0,则下一次marching的距离就是surfaceDistance;如果为0,则证明光线和表面相交,我们只需要确定这点的颜色就好了。
除此之外,我们需要相机的位置rayOrigin做为射线的起点,这个值我们可以通过在脚本中调用SetVector将相机的位置传给GPU。此外我们还需要该Fragment上的射线方向rayDirection,我们可以直接获取,因为它就是顶点属性中的ray经过插值之后的结果。
所以这是一个很简单的逻辑:
[AppleScript] 纯文本查看 复制代码
        fixed4 raymarching(float3 rayOrigin, float3 rayDirection)
        {

            fixed4 ret = fixed4(0, 0, 0, 0);

            int maxStep = 64;

            float rayDistance = 0;

            for(int i = 0; i < maxStep; I++)
            {
                float3 p = rayOrigin + rayDirection * rayDistance;
                float surfaceDistance = map(p);
                if(surfaceDistance < 0.001)
                {
                    ret = fixed4(1, 0, 0, 1);
                    break;
                }

                rayDistance += surfaceDistance;
            }
            return ret;
        }
OK,光线和表面相交之后,输出一个红色。
我们来看一下实际的结果:

可以看到,场景的Hierachy中空空如也,但是屏幕上却出现了一个纯色的六棱体。

0x02 梯度、法线和光照
当然,这个效果并不吸引人,因此我们显然要加入一些光照效果来提升表现力。那么求表面的法线就是必须要做的一件事情了。
milo的《用 C 语言画光(四):反射 》这篇文章中也有相关的内容,即距离场变化最大的方向便是法线方向。根据矢量微积分(vector calculus),一个纯量场(scalar field)的最大变化方向就是其梯度(gradient),所以这个问题就转化为求形状边界位置的 SDF 梯度——即求各个方向的变化率,也就是要求导了。
不过我们显然没有必要真正的计算求导,只需要找一个能够得到近似效果的方式就好了。我们常常使用这个下面这个算式来近似SDF梯度,即在这一点的表面法线:

代码也就十分简单了:
[AppleScript] 纯文本查看 复制代码
        //计算法线
        float3 calcNorm(float3 p)
        {
            float eps = 0.001;

            float3 norm = float3(
                map(p + float3(eps, 0, 0)) - map(p - float3(eps, 0, 0)),
                map(p + float3(0, eps, 0)) - map(p - float3(0, eps, 0)),
                map(p + float3(0, 0, eps)) - map(p - float3(0, 0, eps))
            );

            return normalize(norm);
        }
我们可以把法线信息输出成颜色,就得到了下图中的结果。
而实现一个简单的漫反射也是一件十分简单的事情:
[AppleScript] 纯文本查看 复制代码
          ret = dot(-_LightDir, calcNorm(p));
          ret.a = 1;
这样我们就获得一个有简单光照效果的六棱体了。
0x03 阴影
六棱体上有了简单的漫反射效果,接下来就要在此基础上实现基于SDF的阴影效果了。SDF的一个优势就在于场景内的距离信息全都是可知的,因此可以很方便地用来实现类似阴影这样的效果,并且可以根据距离来更自然地实现阴影的衰减,从而生成一个更加真实的阴影。
不过在此之前,我会将场景修改的稍微复杂一点,当然,这里我只是增加了3个物体的SDF的定义——Sphere、Plane和Cube,并且简单的修改下map函数,重新组织了一下整个场景。
[AppleScript] 纯文本查看 复制代码
        float sdSphere(float3 rp, float3 c, float r)
        {
            return distance(rp,c)-r;
        }

        float sdCube( float3 p, float3 b, float r )
        {
          return length(max(abs(p)-b,0.0))-r;
        }

        float sdPlane( float3 p )
        {
            return p.y + 1;
        }

        float map(float3 rp)
        {
            float ret;
            float sp = sdSphere(rp, float3(1.0,0.0,0.0), 1.0);
            float sp2 = sdSphere(rp, float3(1.0,2.0,0.0), 1.0);
            float cb = sdCube(rp+float3(2.1,-1.0,0.0), float3(2.0,2.0, 2.0), 0.0);
            float py = sdPlane(rp.y);
            ret = (sp < py) ? sp : py;
            ret = (ret < sp2) ? ret : sp2;
            ret = (ret < cb) ? ret : cb;
            return ret;
        }
这样,整个场景就变成了这个样子,由2个球体和1个正方体以及一个平面组成。
接下来我们来实现阴影,其实阴影的形成本身也很简单。沿着光线的方向,如果光线被某个表面遮挡则会在后面的表面上生成阴影。
那么在代码中,一个简单的基于SDF的阴影实现就很简单了:针对到达物体表面的采样点,以该点为起点,沿着光线来的方向,发射另一根射向光源的射线。如果这根射线也击中了某个物体的表面,则证明该采样点处于阴影之中——其实还是raymarching。
下面我们来完成一个最简单的阴影实现,即阴影中是统一的黑色。
[AppleScript] 纯文本查看 复制代码
        float calcShadow(float3 rayOrigin, float3 rayDirection)
        {
            int maxDistance = 64;

            float rayDistance = 0.01;

            for(rayDistance ; rayDistance < maxDistance;)
            {
                float3 p = rayOrigin + rayDirection * rayDistance;
                float surfaceDistance = map(p);
                if(surfaceDistance < 0.001)
                {
                    return 0.0;
                }

                rayDistance += surfaceDistance;
            }
            return 1.0;
        }
当然这里需要注意的是,第一次迭代时不要直接把采样点传入到map中,否则的话会直接return。
ok,这样一个很硬的阴影就创建好了,没有多余的pass,没有多余的贴图,使用SDF创建阴影就是这么简单。

大家都知道,阴影通常是由所谓的本影和半影组成的,其中本影主要指的是物体表面上那些没有被光源直接照射的区域,呈现全黑的状态,而所谓的半影则是那些半明半暗的过渡部分。可以看到我们实现的这种阴影其实只包括本影,而没有半影的效果。
所以在这个纯黑的本影的基础上,再增加一些不是纯黑的半影效果,那么最后的阴影会更加真实。所以接下来我们就要考虑,黑色本影之外的表面上的那些点的颜色了。
这时我们把距离的因素考虑进去:
[AppleScript] 纯文本查看 复制代码
      ret = min(ret, 10 * surfaceDistance /rayDistance );

可以看到,这样一来在之前纯黑的本影之外,不再是像最初的实现中将影子直接截断,而是多了一圈模糊的半影来过渡。
不过,我相信眼尖的你一定发现了一些问题。那就是Cube的半影部分出现了条带状的artifact。

这主要是由于在计算阴影的RayMarching的过程中,采样出现了问题。
在今年的GDC上,Sebastian Aaltonen分享了一个新的方案来解决这个问题:

根据上一次的采样D-1和这一次的采样D的数据,来计算或者是估算一个这条射线上距离SDF表面最近的点E,并用E来计算半影。
在分享中Sebastian也给出了他修改后的半影计算公式:
[AppleScript] 纯文本查看 复制代码
Triangulation formula: res = min(res, 
(r2*sqrt(4*(r1*r1)-h*h))*rcp(2*hprev)/(t-h*h*rcp(2*hprev))) 
事实上Inigo也已经根据Sebastian的分享,改进了他的SDF阴影的效果。下面我们就根据Inigo和Sebastian的实现,在Unity中解决掉这个半影部分的条带状的artifact吧。
[AppleScript] 纯文本查看 复制代码
        //Adapted from:iquilezles
        float calcSoftshadow( float3 ro, float3 rd, float mint, float tmax)
        {

            float res = 1.0;
            float t = mint;
            float ph = 1e10;

            for( int i=0; i<32; i++ )
            {
                float h = map( ro + rd*t );
                float y = h*h/(2.0*ph);
                float d = sqrt(h*h-y*y);
                res = min( res, 10.0*d/max(0.0,t-y) );
                ph = h;

                t += h;

                if( res<0.0001 || t>tmax ) 
                    break;

            }
            return clamp( res, 0.0, 1.0 );
        }
其中ph是上一次采样时的圆形的半径,h是当前这次的采样的圆形半径。
修改后的阴影效果:

0x04 后记
这样,我们就在Unity中实现了SDF渲染以及基于SDF的阴影渲染,并且解决了讨厌的条带状的artifact。
本文的项目可以在这里获取:
游客,如果您要查看本帖隐藏内容请回复

Ref:


  • 《GPU-based clay simulation and ray-tracing tech in Claybook》
  • 《raymarching distance fields》
  • 《Raymarching Distance Fields: Concepts and Implementation in Unity》
[size=0em]​


回复

使用道具 举报

5熟悉之中
520/1000
排名
7703
昨日变化
2

8

主题

123

帖子

520

积分

Rank: 5Rank: 5

UID
99623
好友
0
蛮牛币
1797
威望
0
注册时间
2015-5-12
在线时间
243 小时
最后登录
2018-8-15
发表于 2018-6-14 15:37:18 | 显示全部楼层
6666666666666

回复

使用道具 举报

4四处流浪
301/500
排名
7703
昨日变化
2

0

主题

95

帖子

301

积分

Rank: 4

UID
245227
好友
0
蛮牛币
310
威望
0
注册时间
2017-9-21
在线时间
59 小时
最后登录
2018-8-17
发表于 2018-6-14 16:00:01 | 显示全部楼层
666666666

回复

使用道具 举报

5熟悉之中
872/1000
排名
3277
昨日变化
15

1

主题

248

帖子

872

积分

Rank: 5Rank: 5

UID
56245
好友
0
蛮牛币
2188
威望
0
注册时间
2014-11-18
在线时间
207 小时
最后登录
2018-8-18
发表于 2018-6-14 16:20:49 | 显示全部楼层
高深莫测啊,差距太大啦~

回复 支持 反对

使用道具 举报

4四处流浪
335/500
排名
7477
昨日变化
3

0

主题

62

帖子

335

积分

Rank: 4

UID
2375
好友
0
蛮牛币
206
威望
0
注册时间
2013-8-21
在线时间
119 小时
最后登录
2018-8-17

社区QQ达人

发表于 2018-6-14 16:28:52 | 显示全部楼层
扫地僧 求回邮件呀!!!!

回复 支持 反对

使用道具 举报

7日久生情
1587/5000
排名
2727
昨日变化
7

5

主题

661

帖子

1587

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
239879
好友
1
蛮牛币
3100
威望
0
注册时间
2017-8-26
在线时间
433 小时
最后登录
2018-8-18
发表于 2018-6-14 17:02:44 | 显示全部楼层

回复

使用道具 举报

5熟悉之中
735/1000
排名
4179
昨日变化
1

0

主题

94

帖子

735

积分

Rank: 5Rank: 5

UID
240467
好友
0
蛮牛币
175
威望
0
注册时间
2017-8-30
在线时间
321 小时
最后登录
2018-8-3
发表于 2018-6-14 17:59:51 | 显示全部楼层
6666666666666666666666666
[发帖际遇]: 一个袋子砸在了 泪无痕 头上,泪无痕 赚了 1 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

7日久生情
1888/5000
排名
2013
昨日变化
13

0

主题

723

帖子

1888

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
148286
好友
0
蛮牛币
3643
威望
0
注册时间
2016-5-11
在线时间
537 小时
最后登录
2018-8-18
发表于 2018-6-14 18:00:55 | 显示全部楼层

回复

使用道具 举报

6蛮牛粉丝
1186/1500
排名
1918
昨日变化
7

0

主题

328

帖子

1186

积分

Rank: 6Rank: 6Rank: 6

UID
87577
好友
0
蛮牛币
3854
威望
0
注册时间
2015-3-31
在线时间
208 小时
最后登录
2018-8-17
发表于 2018-6-15 08:26:12 | 显示全部楼层
too good too strong!

回复 支持 反对

使用道具 举报

5熟悉之中
731/1000
排名
2997
昨日变化
6

1

主题

58

帖子

731

积分

Rank: 5Rank: 5

UID
226397
好友
1
蛮牛币
906
威望
0
注册时间
2017-6-12
在线时间
224 小时
最后登录
2018-7-24
发表于 2018-6-15 09:04:40 | 显示全部楼层
66666666666666666666666666666666

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1298/1500
排名
1515
昨日变化
3

1

主题

98

帖子

1298

积分

Rank: 6Rank: 6Rank: 6

UID
121885
好友
0
蛮牛币
1604
威望
0
注册时间
2015-9-9
在线时间
429 小时
最后登录
2018-8-17
QQ
发表于 2018-6-15 09:21:45 | 显示全部楼层
我就看看不说话

回复

使用道具 举报

2初来乍到
104/150
排名
16703
昨日变化
9

0

主题

26

帖子

104

积分

Rank: 2Rank: 2

UID
23464
好友
0
蛮牛币
74
威望
0
注册时间
2014-4-30
在线时间
42 小时
最后登录
2018-8-2
发表于 2018-6-15 09:29:51 | 显示全部楼层
222222222222222222222222222

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1368/1500
排名
2028
昨日变化
5

0

主题

69

帖子

1368

积分

Rank: 6Rank: 6Rank: 6

UID
78855
好友
0
蛮牛币
1707
威望
0
注册时间
2015-3-11
在线时间
675 小时
最后登录
2018-8-17
发表于 2018-6-15 09:30:37 | 显示全部楼层
太厉害了

回复

使用道具 举报

7日久生情
1780/5000
排名
1528
昨日变化
6

2

主题

188

帖子

1780

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
161991
好友
2
蛮牛币
418
威望
0
注册时间
2016-8-12
在线时间
824 小时
最后登录
2018-8-18
发表于 2018-6-15 09:30:41 | 显示全部楼层
Signed Distance Field Shadow in Unity

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1368/1500
排名
2028
昨日变化
5

0

主题

69

帖子

1368

积分

Rank: 6Rank: 6Rank: 6

UID
78855
好友
0
蛮牛币
1707
威望
0
注册时间
2015-3-11
在线时间
675 小时
最后登录
2018-8-17
发表于 2018-6-15 09:33:26 | 显示全部楼层
太厉害了

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表