找回密码
 注册帐号

扫一扫,访问微社区

士郎 Unity的投影阴影

24
回复
2783
查看
[ 复制链接 ]
排名
1
昨日变化

7608

主题

8154

帖子

3万

积分

Rank: 16

UID
1231
好友
186
蛮牛币
10030
威望
30
注册时间
2013-7-29
在线时间
3927 小时
最后登录
2019-4-25

活力之星原创精华达人突出贡献奖财富之证游戏蛮牛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渲染方式

知乎@谢刘建
回复

使用道具 举报

7日久生情
1581/5000
排名
1433
昨日变化

14

主题

152

帖子

1581

积分

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

UID
147764
好友
0
蛮牛币
9120
威望
0
注册时间
2016-5-6
在线时间
553 小时
最后登录
2019-4-25
2018-8-28 11:26:10 显示全部楼层
为什么你这么厉害,我何时才能像你一样优秀
回复 支持 反对

使用道具 举报

6蛮牛粉丝
1365/1500
排名
2129
昨日变化

0

主题

323

帖子

1365

积分

Rank: 6Rank: 6Rank: 6

UID
228538
好友
11
蛮牛币
3247
威望
0
注册时间
2017-6-24
在线时间
390 小时
最后登录
2019-4-25
2018-8-28 11:51:20 显示全部楼层
感谢分享,膜拜大神
回复 支持 反对

使用道具 举报

4四处流浪
304/500
排名
8822
昨日变化

3

主题

86

帖子

304

积分

Rank: 4

UID
194927
好友
0
蛮牛币
101
威望
0
注册时间
2016-12-21
在线时间
79 小时
最后登录
2019-4-24
2018-8-28 11:53:49 显示全部楼层
厉害厉害、
回复

使用道具 举报

7日久生情
3160/5000
排名
352
昨日变化

2

主题

325

帖子

3160

积分

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

UID
33368
好友
2
蛮牛币
4978
威望
0
注册时间
2014-7-9
在线时间
1061 小时
最后登录
2019-4-25
2018-8-28 14:32:21 显示全部楼层
666666666666666666
回复 支持 反对

使用道具 举报

5熟悉之中
876/1000
排名
2503
昨日变化

0

主题

66

帖子

876

积分

Rank: 5Rank: 5

UID
231194
好友
0
蛮牛币
2843
威望
0
注册时间
2017-7-10
在线时间
226 小时
最后登录
2019-3-12
2018-8-28 15:37:57 显示全部楼层
感谢分享
回复

使用道具 举报

7日久生情
2006/5000
排名
1897
昨日变化

41

主题

737

帖子

2006

积分

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

UID
214924
好友
4
蛮牛币
17455
威望
0
注册时间
2017-3-28
在线时间
512 小时
最后登录
2019-4-25
2018-8-28 15:42:05 显示全部楼层
达到清风水平,我还差出生
回复 支持 反对

使用道具 举报

5熟悉之中
746/1000
排名
4111
昨日变化

1

主题

235

帖子

746

积分

Rank: 5Rank: 5

UID
245227
好友
0
蛮牛币
961
威望
0
注册时间
2017-9-21
在线时间
150 小时
最后登录
2019-4-25
2018-8-28 16:05:03 显示全部楼层
本帖最后由 tiancaiwlk 于 2018-8-28 16:06 编辑

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

使用道具 举报

3偶尔光临
211/300
排名
11658
昨日变化

3

主题

47

帖子

211

积分

Rank: 3Rank: 3Rank: 3

UID
205266
好友
0
蛮牛币
1138
威望
0
注册时间
2017-2-6
在线时间
77 小时
最后登录
2018-12-21
2018-8-28 16:50:34 显示全部楼层
大神,我何时才能像你一样优秀
回复 支持 反对

使用道具 举报

3偶尔光临
211/300
排名
11658
昨日变化

3

主题

47

帖子

211

积分

Rank: 3Rank: 3Rank: 3

UID
205266
好友
0
蛮牛币
1138
威望
0
注册时间
2017-2-6
在线时间
77 小时
最后登录
2018-12-21
2018-8-28 16:52:50 显示全部楼层
说好的“Demo运行时候的视频”在哪呢
回复 支持 反对

使用道具 举报

7日久生情
2695/5000
排名
2230
昨日变化

1

主题

1721

帖子

2695

积分

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

UID
119154
好友
0
蛮牛币
2778
威望
0
注册时间
2015-8-21
在线时间
343 小时
最后登录
2019-4-23
2018-8-28 19:50:02 显示全部楼层
谢谢楼主大大。
回复

使用道具 举报

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

0

主题

282

帖子

2517

积分

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

UID
8212
好友
0
蛮牛币
1401
威望
0
注册时间
2013-11-16
在线时间
597 小时
最后登录
2019-4-16
2018-8-28 20:39:34 显示全部楼层
666666666666666666666666666666666
回复 支持 反对

使用道具 举报

7日久生情
1845/5000
排名
1193
昨日变化

0

主题

542

帖子

1845

积分

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

UID
87577
好友
0
蛮牛币
6535
威望
0
注册时间
2015-3-31
在线时间
325 小时
最后登录
2019-4-25
2018-8-29 08:34:31 显示全部楼层
too good too strong!
回复 支持 反对

使用道具 举报

5熟悉之中
748/1000
排名
3601
昨日变化

0

主题

96

帖子

748

积分

Rank: 5Rank: 5

UID
71644
好友
0
蛮牛币
1264
威望
0
注册时间
2015-1-28
在线时间
236 小时
最后登录
2019-4-25
2018-8-29 08:55:07 显示全部楼层
66666666666
回复

使用道具 举报

5熟悉之中
564/1000
排名
4882
昨日变化

0

主题

119

帖子

564

积分

Rank: 5Rank: 5

UID
171160
好友
0
蛮牛币
1493
威望
0
注册时间
2016-9-22
在线时间
147 小时
最后登录
2018-11-23
2018-8-29 08:55:59 显示全部楼层
{:94:}
回复

使用道具 举报

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

本版积分规则