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

扫一扫,访问微社区

开发者专栏

关注:2348

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

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

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

[士郎] Unity的投影阴影

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

6965

主题

7490

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
10372
威望
30
注册时间
2013-7-29
在线时间
3571 小时
最后登录
2018-11-12

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

发表于 2018-8-28 10:58:43 | 显示全部楼层 |阅读模式

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

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

x
前言
Unity引擎是自带阴影的,是效果较好的ShadowMap, 但是在用Unity开发大型手游的时候,一般不会使用Unity自带的影子,主要是效率问题,会导致帧率下降明显。为了在手机上角色也能有阴影效果,可以采用投影器阴影,兼顾效率和效果,参数调的好的话,也能有不错的效果。下面是Demo运行时候的视频。








功能实现
  • 关闭主光源的投影投射
1.jpg
如上图所示,使用投影阴影的时候,应该关闭主光源投射阴影。
  • 设置投影器
如图所示,添加一个Projector组件,然后调整Projector的GameObject的方向
2.jpg
3.jpg
  • 核心代码编写
如上图所示,编写ProjectorShadow脚本
1.首先创建一个RenderTexture
      
[AppleScript] 纯文本查看 复制代码
  // 创建render texture
        mShadowRT = new RenderTexture(mRenderTexSize, mRenderTexSize, 0, RenderTextureFormat.R8);
        mShadowRT.name = "ShadowRT";
        mShadowRT.antiAliasing = 1;   // 关闭抗锯齿
        mShadowRT.filterMode = FilterMode.Bilinear;
        mShadowRT.wrapMode = TextureWrapMode.Clamp;     // wrapmode要设置为Clamp

注意首先这个RenderTexture的格式是R8, 这个格式创建的贴图内存占用是最小的。
在运行时查看贴图
4.jpg
对于创建2048x2048的贴图,只有4M的内存。
然后antiAliasing设置为1, 也就是不开抗锯齿。
wrapMode设置为Clamp
最后运行是的参数如下图所示
对于图中的Depth Buffer, 虽然代码没有设置,但是默认是关闭的,这种投影阴影创建的RenderTexture不需要使用DepthBuffer, 所以应该关闭的。
5.jpg
2.设置Projector
     
[AppleScript] 纯文本查看 复制代码
   //projector初始化
        mProjector = GetComponent<Projector>();
        mProjector.orthographic = true;
        mProjector.orthographicSize = mProjectorSize;
        mProjector.ignoreLayers = mLayerIgnoreReceiver;
        mProjector.material.SetTexture("_ShadowTex", mShadowRT);

这里主要是把投影器设置为正投影。同时设置投影器的尺寸,并设置投影器的忽略层,如下图所示
6.jpg
投影器尺寸设置为23,忽略层是Unit, 也就是游戏中创建的所有的单位。
3. 创建投影Camera
      
[AppleScript] 纯文本查看 复制代码
 //camera初始化
        mShadowCam = gameObject.AddComponent<Camera>();
        mShadowCam.clearFlags = CameraClearFlags.Color;
        mShadowCam.backgroundColor = Color.black;
        mShadowCam.orthographic = true;
        mShadowCam.orthographicSize = mProjectorSize;
        mShadowCam.depth = -100.0f;
        mShadowCam.nearClipPlane = mProjector.nearClipPlane;
        mShadowCam.farClipPlane = mProjector.farClipPlane;
        mShadowCam.targetTexture = mShadowRT;

创建的Camera的clearFlags 设置为清理颜色
Camera的清理颜色backgroundColor 设置为黑色
Camera也应该是正投影的, 同时正投影尺寸也应该和Projector的尺寸一致
Camera的depth设置为-100, 也就是比主摄像机提前渲染
Camera的近裁剪面和远裁剪面设置的和投影器的近裁剪面和远裁剪面一致
Camera的targetTexture设置为创建的RenderTexture, 也就是说,摄像机渲染所有的对象到这张RenderTexture上。
4. 渲染方式选择
这里感觉是本文的重点了。参考好几篇文章,最后总结了2种方式,其中使用CommandBuffer的方式本人认为更适合实际项目,可以提高渲染效率。
首先看一下代码实现
[AppleScript] 纯文本查看 复制代码
private void SwitchCommandBuffer()
    {
        Shader replaceshader = Shader.Find("ProjectorShadow/ShadowCaster");

        if (!mUseCommandBuf)
        {
            mShadowCam.cullingMask = mLayerCaster;

            mShadowCam.SetReplacementShader(replaceshader, "RenderType");
        }
        else
        {
            mShadowCam.cullingMask = 0;

            mShadowCam.RemoveAllCommandBuffers();
            if (mCommandBuf != null)
            {
                mCommandBuf.Dispose();
                mCommandBuf = null;
            }
            
            mCommandBuf = new CommandBuffer();
            mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf);

            if (mReplaceMat == null)
            {
                mReplaceMat = new Material(replaceshader);
                mReplaceMat.hideFlags = HideFlags.HideAndDontSave;
            }
        }
    }
a. 对于不使用CommandBuffer的情况下,主要是下面2行代码
mShadowCam.cullingMask = mLayerCaster;mShadowCam.SetReplacementShader(replaceshader, "RenderType");
设置Camera应该渲染那些层的GameObject
同时Camera渲染可以使用哪个Shader来替换
如下图所示,Camera只渲染所有创建的Unit
7.jpg
对于Camera使用的Shader, 可以用一个普通顶点/片元shader来处理
[AppleScript] 纯文本查看 复制代码
Shader "ProjectorShadow/ShadowCaster"
{
        Properties
        {
                _ShadowColor("Main Color", COLOR) = (1, 1, 1, 1)
        }
        
        SubShader
        {
                Tags{ "RenderType" = "Opaque" "Queue" = "Geometry" }

                Pass
                {
                        ZWrite Off
                        Cull Off

                        CGPROGRAM

                        #pragma vertex vert
                        #pragma fragment frag
                        
                        struct v2f
                        {
                                float4 pos : POSITION;
                        };
                        
                        v2f vert(float4 vertex:POSITION)
                        {
                                v2f o;
                                o.pos = UnityObjectToClipPos(vertex);
                                return o;
                        }

                        float4 frag(v2f i) :SV_TARGET
                        {
                                return 1;
                        }
                        
                        ENDCG
                }
        }
}

这个Shader就是输出白色,同时关闭写入深度,不使用裁剪
b. 对于使用CommandBuffer的情况,主要是如下的代码
           
[AppleScript] 纯文本查看 复制代码
 mShadowCam.cullingMask = 0;

            mShadowCam.RemoveAllCommandBuffers();
            if (mCommandBuf != null)
            {
                mCommandBuf.Dispose();
                mCommandBuf = null;
            }
            
            mCommandBuf = new CommandBuffer();
            mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf);

            if (mReplaceMat == null)
            {
                mReplaceMat = new Material(replaceshader);
                mReplaceMat.hideFlags = HideFlags.HideAndDontSave;
            }

Camera的cullingMask 设置为0,也就是Camera不会渲染任何物体,所有的渲染走CommandBuffer
然后创建CommandBuffer, 添加到Camera的CommandBuffer列表中。
创建CommandBuffer渲染需要的Material, Material需要用到的shader就是上面的"ProjectorShadow/ShadowCaster"
在每帧刷新的时候
  
[AppleScript] 纯文本查看 复制代码
 private void FillCommandBuffer()
    {
        mCommandBuf.Clear();

        Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam);

        List<GameObject> listgo = UnitManager.Instance.UnitList;
        foreach (var go in listgo)
        {
            if (go == null)
                continue;

            Collider collider = go.GetComponentInChildren<Collider>();
            if (collider == null)
                continue;

            bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds);
            if (!bound)
                continue;

            Renderer[] renderlist = go.GetComponentsInChildren<Renderer>();
            if (renderlist.Length <= 0)
                continue;

            // 是否有可见的render
            // 有可见的则整个GameObject都渲染
            bool hasvis = false;
            foreach (var render in renderlist)
            {
                if (render == null)
                    continue;

                RenderVis rendervis = render.GetComponent<RenderVis>();
                if (rendervis == null)
                    continue;

                if (rendervis.IsVisible)
                {
                    hasvis = true;
                    break;
                }
            }

            foreach(var render in renderlist)
            {
                if (render == null)
                    continue;

                mCommandBuf.DrawRenderer(render, mReplaceMat);
            }           
        }
    }

遍历游戏中所有创建的单位,首先通过视锥体剔除,剔除投影Camera看不到的Unit, 主要是下面两行代码
[AppleScript] 纯文本查看 复制代码
Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam);

bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds);

首先计算得到投影Camera的视锥体, 然后通过函数,判断单位的Collider是否在视锥体范围内。这样就可以筛选出当前帧摄像机可以看到的Unit.
接着进行下面的判断
        
[AppleScript] 纯文本查看 复制代码
   Renderer[] renderlist = go.GetComponentsInChildren<Renderer>();
            if (renderlist.Length <= 0)
                continue;

            // 是否有可见的render
            // 有可见的则整个GameObject都渲染
            bool hasvis = false;
            foreach (var render in renderlist)
            {
                if (render == null)
                    continue;

                RenderVis rendervis = render.GetComponent<RenderVis>();
                if (rendervis == null)
                    continue;

                if (rendervis.IsVisible)
                {
                    hasvis = true;
                    break;
                }
            }

对于在视锥体内的Unit, 遍历它所有的Render, 判断Render是否可以,只有当这个Unit有一个Render可见的情况下,然后渲染这个单位(这里为什么不根据Render是否可见,单独渲染每个Render, 主要是因为我们希望渲染的Unit是完整的,不想Unit是部份被渲染出来的。要么整个渲染出来,要么就是不渲染)
那么问题来了,Unit什么时候可见,什么时候不可见,我们是怎么知道的。可以看下下面的代码片段。
  
[AppleScript] 纯文本查看 复制代码
 private bool mIsVisible = false;

    public bool IsVisible
    {
        get { return mIsVisible; }
    }

    void OnBecameVisible()
    {
        mIsVisible = true;
    }

    void OnBecameInvisible()
    {
        mIsVisible = false;
    }

每个Render下面都会挂这个脚本,当这个Render被摄像机看见,Unity引擎就会调用OnBecameVisible函数,当这个Render摄像机不可见,就会调用OnBecameInvisible函数。
目前在这个Demo中,在投影Camera使用CommandBuffer的情况下,Camera是不渲染任何物体的,只有Main Camera会渲染所有的Render, 所以就可以理解为当Visible可见的时候,这个Render就出现在屏幕上,当Visible不可见的时候,这个Render在屏幕上不可见。
总结一下,在每帧刷新的时候,首先通过投影Camera筛选出需要的投影Camera能够渲染的Unit, 然后判断这个对象是否也同时被Main Camera可见。都满足的情况下,再使用
mCommandBuf.DrawRenderer(render, mReplaceMat);函数来渲染对象到创建的RenderTexture中。
5. 投影器Shader是怎么实现的?
投影Shader其实是一个阴影接收Shader, 具体实现如下所示
               
[AppleScript] 纯文本查看 复制代码
ZWrite Off
                        ColorMask RGB
                        Blend DstColor Zero
                        Offset -1, -1

                        v2f vert(float4 vertex:POSITION)
                        {
                                v2f o;
                                o.pos = UnityObjectToClipPos(vertex);
                                o.sproj = mul(unity_Projector, vertex);
                                UNITY_TRANSFER_FOG(o,o.pos);
                                return o;
                        }

                        float4 frag(v2f i):SV_TARGET
                        {
                                half4 shadowCol = tex2Dproj(_ShadowTex, UNITY_PROJ_COORD(i.sproj));
                                half maskCol = tex2Dproj(_FalloffTex, UNITY_PROJ_COORD(i.sproj)).r;
                                half a = shadowCol.r * maskCol;
                                float c = 1.0 - _Intensity * a;

                                UNITY_APPLY_FOG_COLOR(i.fogCoord, c, fixed4(1,1,1,1));

                                return c;
                        }

在vert中,计算出投影位置 o.sproj = mul(unity_Projector, vertex);
在frag中,通过UNITY_PROJ_COORD(i.sproj)计算出投影纹理坐标。
然后混合出最终的颜色。
这里需要指出的是。如下图所示。
8.jpg
添加了一张Mask图,通过这张Mask图,可以把阴影边缘处理的比较好,阴影边缘出现会有淡入淡出的效果。
  • 运行游戏
效果图如下所示,同一视角下,切换是否使用CommandBuffer方式渲染,在同样的效果下,使用CommandBuffer的方式使用的Batch更好,性能相应的也就更好。(上图是不使用CommandBuf, 下图使用CommandBuf)
9.jpg
不使用CommandBuf渲染方式
11.jpg
使用CommandBuf渲染方式

知乎@谢刘建

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

7日久生情
1516/5000
排名
1430
昨日变化
1

14

主题

149

帖子

1516

积分

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

UID
147764
好友
0
蛮牛币
2913
威望
0
注册时间
2016-5-6
在线时间
523 小时
最后登录
2018-11-9

迈向小康

发表于 2018-8-28 11:26:10 | 显示全部楼层
为什么你这么厉害,我何时才能像你一样优秀

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1102/1500
排名
2698
昨日变化
9

0

主题

293

帖子

1102

积分

Rank: 6Rank: 6Rank: 6

UID
228538
好友
9
蛮牛币
2397
威望
0
注册时间
2017-6-24
在线时间
297 小时
最后登录
2018-11-12
发表于 2018-8-28 11:51:20 | 显示全部楼层
感谢分享,膜拜大神

回复 支持 反对

使用道具 举报

3偶尔光临
276/300
排名
9033
昨日变化
3

3

主题

82

帖子

276

积分

Rank: 3Rank: 3Rank: 3

UID
194927
好友
0
蛮牛币
138
威望
0
注册时间
2016-12-21
在线时间
67 小时
最后登录
2018-10-16
发表于 2018-8-28 11:53:49 | 显示全部楼层
厉害厉害、

回复

使用道具 举报

7日久生情
2881/5000
排名
414
昨日变化
1

2

主题

320

帖子

2881

积分

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

UID
33368
好友
2
蛮牛币
4314
威望
0
注册时间
2014-7-9
在线时间
971 小时
最后登录
2018-11-12
发表于 2018-8-28 14:32:21 | 显示全部楼层
666666666666666666
[发帖际遇]: zwxzxl 发帖时在路边捡到 2 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

5熟悉之中
800/1000
排名
2607
昨日变化
1

0

主题

62

帖子

800

积分

Rank: 5Rank: 5

UID
231194
好友
0
蛮牛币
2680
威望
0
注册时间
2017-7-10
在线时间
204 小时
最后登录
2018-11-9
发表于 2018-8-28 15:37:57 | 显示全部楼层
感谢分享

回复

使用道具 举报

7日久生情
1542/5000
排名
2521
昨日变化

12

主题

626

帖子

1542

积分

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

UID
214924
好友
3
蛮牛币
3917
威望
0
注册时间
2017-3-28
在线时间
358 小时
最后登录
2018-11-9
发表于 2018-8-28 15:42:05 | 显示全部楼层
达到清风水平,我还差出生

回复 支持 反对

使用道具 举报

4四处流浪
487/500
排名
5509
昨日变化
40

1

主题

149

帖子

487

积分

Rank: 4

UID
245227
好友
0
蛮牛币
569
威望
0
注册时间
2017-9-21
在线时间
93 小时
最后登录
2018-11-12
发表于 2018-8-28 16:05:03 | 显示全部楼层
本帖最后由 tiancaiwlk 于 2018-8-28 16:06 编辑

随着场景中被投射物体的增加, 最损耗性能的恰恰是这种投影的方式被投射物体指的是地面, 建筑等影子要投射的物体

回复 支持 反对

使用道具 举报

3偶尔光临
210/300
排名
11243
昨日变化
5

3

主题

47

帖子

210

积分

Rank: 3Rank: 3Rank: 3

UID
205266
好友
0
蛮牛币
1124
威望
0
注册时间
2017-2-6
在线时间
77 小时
最后登录
2018-11-12
发表于 2018-8-28 16:50:34 | 显示全部楼层
大神,我何时才能像你一样优秀

回复 支持 反对

使用道具 举报

3偶尔光临
210/300
排名
11243
昨日变化
5

3

主题

47

帖子

210

积分

Rank: 3Rank: 3Rank: 3

UID
205266
好友
0
蛮牛币
1124
威望
0
注册时间
2017-2-6
在线时间
77 小时
最后登录
2018-11-12
发表于 2018-8-28 16:52:50 | 显示全部楼层
说好的“Demo运行时候的视频”在哪呢

回复 支持 反对

使用道具 举报

7日久生情
2205/5000
排名
2742
昨日变化
4

1

主题

1419

帖子

2205

积分

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

UID
119154
好友
0
蛮牛币
2385
威望
0
注册时间
2015-8-21
在线时间
279 小时
最后登录
2018-11-9
发表于 2018-8-28 19:50:02 | 显示全部楼层
谢谢楼主大大。

回复

使用道具 举报

7日久生情
2360/5000
排名
447
昨日变化
1

0

主题

264

帖子

2360

积分

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

UID
8212
好友
0
蛮牛币
1460
威望
0
注册时间
2013-11-16
在线时间
560 小时
最后登录
2018-11-10
发表于 2018-8-28 20:39:34 | 显示全部楼层
666666666666666666666666666666666

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1460/1500
排名
1541
昨日变化
5

0

主题

420

帖子

1460

积分

Rank: 6Rank: 6Rank: 6

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

回复 支持 反对

使用道具 举报

4四处流浪
463/500
排名
5235
昨日变化
29

0

主题

74

帖子

463

积分

Rank: 4

UID
71644
好友
0
蛮牛币
741
威望
0
注册时间
2015-1-28
在线时间
129 小时
最后登录
2018-11-12
发表于 2018-8-29 08:55:07 | 显示全部楼层
66666666666
[发帖际遇]: ydy 在网吧通宵,花了 1 蛮牛币. 幸运榜 / 衰神榜

回复

使用道具 举报

5熟悉之中
540/1000
排名
4879
昨日变化
21

0

主题

120

帖子

540

积分

Rank: 5Rank: 5

UID
171160
好友
0
蛮牛币
1347
威望
0
注册时间
2016-9-22
在线时间
140 小时
最后登录
2018-11-12
发表于 2018-8-29 08:55:59 | 显示全部楼层
{:94:}

回复

使用道具 举报

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

本版积分规则

关闭

站长推荐 上一条 /1 下一条

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