找回密码
 注册帐号

扫一扫,访问微社区

士郎 基于ShadowMap的场景静态阴影

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

7608

主题

8154

帖子

3万

积分

Rank: 16

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

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

2018-9-13 10:33:21 显示全部楼层 阅读模式

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

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

x
前言:手游中多数情况,场景的灯光信息会由场景美术预先通过烘焙的方式生成好,这样虽然带来了性能上的高效,但是无法达到一些表现的需求,例如通过lightmapmap提前烘焙好的场景阴影,当角色步入阴影当中的时候无法达到一个逐渐进入的效果,入下图
1.1.gif
(图1)烘焙好的场景阴影贴图 1.2.gif
(图2)预期的效果
有了需求,接下来就是制定解决方案,同时还要考虑移动端性能限制。Shadowmap是现在主流的阴影解决方案,包括Unity的阴影也是基于此,但是我们项目中不需要对动态物体采用这种方式产生阴影,只需要对静态的场景物体,即设置为static的产生这种阴影区域,所以最终我们采取在进入场景前,提前缓存好一张shadowmap,由动态物体接收这个阴影来达到图2的效果。

  • ShadowMap原理
  • 从产生阴影的光源(在通常项目中为Directional Light)放置一个相机,渲染一次场景,获取深度贴图。
  • 场景内物体渲染时,将自身世界坐标转入刚才的阴影相机的投影坐标得到统一坐标系下的深度值然后和对应深度贴图中保存的深度值比较,(在d3d11下)如果小于保存的深度值代表此像素处于阴影。

  • 获取ShadowMap
有了原理,我们接下来第一步就是要保存场景的ShadowMap。首先是建立一个Camera用来生成ShadowMap,这个camera必须和场景的主光源,通常为Directional Light,有一样的Forward朝向(Directional Light的话,需开启正交相机)。Camera的视窗决定了那些区域能产生阴影,通常有两种方案来决定视窗范围:
(1)FitScene
顾名思义,阴影摄像机视锥覆盖到场景内所有角色可行走范围即可,区域外由于角色走不到,不会产生阴影交互,生成也是浪费。比较适合小场景。对于只计算静态阴影,只需生成一次即可。缺点是当场景较大的时候,摄像机视锥很大导致ShadowMap纹理很大或者分辨率较低。
1.jpg
(2)FitView
阴影摄像机视锥覆盖观察摄像机视锥,即阴影相机只渲染主观察相机能看到的地方,相对FitScene,FitView的阴影区域生成利用率更高,因为处于可行走区域但是不被主观察相机渲染的地方是不会被生成到ShadowMap中的,但是主相机的参数只要一改变,对应阴影摄像机的视锥参数也会改变,所以需要实时调整阴影摄像机的视锥以匹配观察摄像机的视锥。
2.jpg
由于项目和移动端性能考虑,我们项目采用了FitScene的方式来生成ShadowMap。在场景搭建好之后,美术或者策划只要在场景内适当位置摆放好一个用来生成的ShadowMap的摄像机即可,我们这里为其编写了一些工具,可简化美术或者策划端的工作。
3.jpg
只要提供行走区域和主光源即可算出FitScene的包围盒
有了正确的阴影摄像机参数设置,我们就可以在游戏开始前对场景生成ShadowMap。这里我们采用Camera自带的方法RenderWithShader,渲染一次到rendertexture,然后即可关闭这个相机。下面给出ShadowMapCapture(阴影捕获Shader)主体。
[AppleScript] 纯文本查看 复制代码
v2f vert (appdata v)
{
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.depth = o.vertex.zw;
        return o;
}
                        
fixed4 frag (v2f i) : SV_Target
{
        float depth = i.depth.x / i.depth.y;
        return EncodeFloatRGBA((depth * 0.5 + 0.5));
}

通过将float depth = i.depth.x / i.depth.y我们可以获取到某个像素在阴影摄像机裁切空间的深度,然后通过UnityCginc中提供的EncodeFloatRGBA方法将一个Float值存储在float4值当中,在frag函数中这个float4被当做颜色最终输出。这里要注意的一点是裁剪空间里OpenGL的z是[-1, 1],而D3D是[0, 1],而EncodeFloatRGBA方法只接受[0..1)范围的输入值,所以这里我们还得考虑手机平台的OpenGL,将depth值进行一次映射。实际效果如下图。
4.jpg
选中阴影摄像机的预览图 5.jpg
通过shader获取的ShadowMap纹理
  • 应用ShadowMap
有了ShadowMap纹理之后,我们需要做的是如何让我们的角色shader支持。上面这张ShadowMap纹理中保存的是在阴影摄像机裁切空间坐标系下的Z深度,所以要进行比较,我们角色需要将自身像素的坐标转换到该坐标系下比较才有意义,好在Unity提供了很方便的坐标系转换API。下面给出C#里如何获取坐标系转换的代码。
[AppleScript] 纯文本查看 复制代码
public void SetGlobalShaderParam()
{
    //世界->阴影摄像机观察空间
    Matrix4x4 worldToView = LightCamera.worldToCameraMatrix;
    //阴影摄像机观察空间->阴影摄像机裁切空间
    Matrix4x4 projection = GL.GetGPUProjectionMatrix(LightCamera.projectionMatrix, false);
    //将裁切空间的XY坐标系[-1, 1]映射到uv坐标[0, 1]
    Matrix4x4 posToUV = new Matrix4x4();
    posToUV.SetRow(0, new Vector4(0.5f, 0, 0, 0.5f));
    posToUV.SetRow(1, new Vector4(0, 0.5f, 0, 0.5f));
    posToUV.SetRow(2, new Vector4(0, 0, 1, 0));
    posToUV.SetRow(3, new Vector4(0, 0, 0, 1));
    //最终世界坐标系到ShadowMao纹理UV坐标系的转换矩阵
    LightProjectMatrix = posToUV * projection * worldToView;
    Shader.SetGlobalMatrix("_LightProjection", LightProjectMatrix);
}

在Shader中获取到到转换坐标系后,就可以在frag函数中将像素的worldPos转换成到ShadowMap纹理UV并采样到该像素对应在ShadowMap中的深度值。然后纹理中存储的深度和该像素的深度做比较可以得出该像素是否处于阴影中,然后自己做相应的修改,这里要注意的是在d3d和移动端OpenGl的坐标系不同,会导致Z值判断不同,用UnityCG.cginc中定义的UNITY_REVERSED_Z来判断即可。下面给出ShadowMapReceiver(阴影接受Shader)中Frag中相关代码
[AppleScript] 纯文本查看 复制代码
//世界坐标->阴影摄像机裁切空间坐标
fixed4 lightClipPos = mul(_LightProjection, i.worldPos);
//shadowmap 纹理采样
fixed4 depthRGBA = tex2D(_LightDepthTex, lightClipPos.xy);
//还原深度值
float depth = DecodeFloatRGBA(depthRGBA);

lightClipPos.z = lightClipPos.z / lightClipPos.w;
//截取shadowmap的时候对depth做了范围映射,这样采取同样处理
float pixeldepth = (lightClipPos.z * 0.5 + 0.5);
//opengl d3d Z轴正负需要考虑
#if defined(UNITY_REVERSED_Z)
if (pixeldepth < depth)
{
        finalColor.rgb *= _ShadowColorAtten;
}
#else
if (pixeldepth  > depth)
{
        finalColor.rgb *= _ShadowColorAtten;
}
#endif                

最终,对于场景内静态区域的阴影,我们可以得到如下图的效果。
6.jpg
场景结构,LightMap已经提前烘焙好 1.3.gif
[size=0.9em]我们的主角球已经可以接受到正确的场景阴影信息


  • 改进
很快美术提来了新的需求,我们的场景有一片树林,但是背景的树是用贴图的方式制作的,这在截取ShadowMap纹理的时候,这片树林被当做一面墙来产生阴影了,效果如图。
1.4.gif
plane上的阴影是烘焙产生的,但是小球接受的阴影却是错误的
导致这个结果的原因是我们在用阴影摄像机RenderWithShader的时候没有对CutOut和Blend两种透明方式做处理,这里处理的方式其实也很简单。
[AppleScript] 纯文本查看 复制代码
public void RenderWithShader(Shader shader, string replacementTag)

replacementTag这个参数用来指定使用具有该标签的shader替换,比如场景里A物体的tag包含{"RenderType"="Opaque"},那么就会从传入的shader(ShadowMapCapture)中查找具有相同tag的subshader去替换渲染。那么对应非透明(Opaque),透明裁切(TransparentCutout), 透明混合(Transparent)这三个主要的标签,我们要分别写三个subshader。其中Opaque上文已经给出,对于TransparentCutout、Transparent这两种其实处理方法一致,统一采用alpha cutout的方式,这样的相邻阴影会衔接在一起,无明显过度效果,但是处理起来方便简单。下面给出代码
[AppleScript] 纯文本查看 复制代码
float depth = i.depth.x / i.depth.y;
fixed4 color = tex2D(_MainTex, i.uv);
//这里裁切透明度可以自己设置
if (color.a < 0.3)
{
        return fixed4(0, 0, 0, 0);
}
else
{
        return EncodeFloatRGBA((depth * 0.5 + 0.5));
}
同时对于这些SubShader,我们要修改如下功能

//写入深度并且在混合时取最大保证透明物体的层叠问题
BlendOp Max
ZWrite On
//避免被实际深度测试剔除
ZTest Off
//渲染背面,避免一些片面的背部朝向光源
Cull off


最终效果,这里我把球换成了Cube,这样效果比较明显一点
1.5.gif


  • 结语
以上是我结合网上的ShadowMap资料,以移动端为目标平台所制作出的一个静态阴影解决方案。
它的优点是:
  • 性能开销相对开启实时灯光接收阴影小
  • 能够实现静态物体的阴影区域进入效果
缺点也是有的:
  • 无法实现移动物体的自身阴影,因为ShadowMap只在最开始的时候截取了一张,要想实现动态阴影,必须同步一直更新ShadowMap,性能开销又变大了,不过对于角色自身的阴影,我们也有其他的投机的解决方案
  • 对于FitScene来获取ShadowMap纹理,对于大场景不适用,要么纹理大,精度高,要么压缩纹理导致阴影精度低,产生很糟糕的锯齿感。而改用FitView同样要面临同步更新ShadowMap纹理的问题。
最后,实际的效果需要结合项目自身决定,所幸我们项目正好满足上述各种限制:较小场景,同时动态物体阴影采取其他方案解决。

想一起聊技术还定期发红包的可以扫码加入微信群哦
VEE.jpg

知乎@Chenyuansheng

回复

使用道具 举报

7日久生情
2037/5000
排名
1058
昨日变化

13

主题

269

帖子

2037

积分

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

UID
99182
好友
0
蛮牛币
9825
威望
0
注册时间
2015-5-10
在线时间
707 小时
最后登录
2019-4-25
2018-9-13 11:50:15 显示全部楼层
很棒思路!
回复

使用道具 举报

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-9-14 08:23:54 显示全部楼层
谢谢楼主大大。
回复

使用道具 举报

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

0

主题

323

帖子

1365

积分

Rank: 6Rank: 6Rank: 6

UID
228538
好友
11
蛮牛币
3247
威望
0
注册时间
2017-6-24
在线时间
390 小时
最后登录
2019-4-25
2018-9-14 09:53:09 显示全部楼层
感謝分享
回复

使用道具 举报

5熟悉之中
590/1000
排名
4499
昨日变化

0

主题

91

帖子

590

积分

Rank: 5Rank: 5

UID
282652
好友
1
蛮牛币
3204
威望
0
注册时间
2018-5-24
在线时间
173 小时
最后登录
2019-4-24
2018-9-14 10:08:07 显示全部楼层
哇,厉害哦
回复

使用道具 举报

排名
24126
昨日变化

0

主题

8

帖子

44

积分

Rank: 1

UID
245587
好友
0
蛮牛币
76
威望
0
注册时间
2017-9-23
在线时间
18 小时
最后登录
2019-4-10
2018-9-14 11:11:28 显示全部楼层
unity中不是可以直接得到相机的深度纹理吗,为什么要自己计算一遍
回复 支持 反对

使用道具 举报

5熟悉之中
848/1000
排名
5174
昨日变化

1

主题

295

帖子

848

积分

Rank: 5Rank: 5

UID
258102
好友
1
蛮牛币
1100
威望
0
注册时间
2017-12-6
在线时间
274 小时
最后登录
2019-4-25
2018-9-14 16:28:27 显示全部楼层
受益匪浅
回复

使用道具 举报

排名
31428
昨日变化

0

主题

14

帖子

61

积分

Rank: 2Rank: 2

UID
169309
好友
0
蛮牛币
371
威望
0
注册时间
2016-9-19
在线时间
37 小时
最后登录
2019-4-22
2018-9-15 15:36:25 显示全部楼层
这个方法还是挺好用的。
回复 支持 反对

使用道具 举报

2初来乍到
134/150
排名
15584
昨日变化

1

主题

31

帖子

134

积分

Rank: 2Rank: 2

UID
22646
好友
0
蛮牛币
14
威望
0
注册时间
2014-4-23
在线时间
54 小时
最后登录
2019-4-19
2018-9-15 20:55:39 显示全部楼层
谢谢分享~~~~~~~~~~~~
回复

使用道具 举报

2初来乍到
114/150
排名
19336
昨日变化

0

主题

45

帖子

114

积分

Rank: 2Rank: 2

UID
94110
好友
0
蛮牛币
2
威望
0
注册时间
2015-4-22
在线时间
39 小时
最后登录
2019-4-19
2018-9-16 07:44:50 显示全部楼层
牛逼学习学习
回复

使用道具 举报

排名
39856
昨日变化

0

主题

8

帖子

17

积分

Rank: 1

UID
291020
好友
0
蛮牛币
31
威望
0
注册时间
2018-7-24
在线时间
3 小时
最后登录
2018-9-17
2018-9-17 10:28:20 显示全部楼层
谢谢大神分享,受益匪浅!
回复 支持 反对

使用道具 举报

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

13

主题

103

帖子

748

积分

Rank: 5Rank: 5

UID
284790
好友
1
蛮牛币
6513
威望
0
注册时间
2018-6-7
在线时间
280 小时
最后登录
2019-3-15
2018-9-18 20:19:34 显示全部楼层
进来凑凑
回复

使用道具 举报

排名
39856
昨日变化

0

主题

5

帖子

18

积分

Rank: 1

UID
304774
好友
0
蛮牛币
8
威望
0
注册时间
2018-11-15
在线时间
7 小时
最后登录
2019-3-1
2018-11-23 09:51:53 显示全部楼层
{:87:}{:87:}{:87:}棒棒棒
回复 支持 反对

使用道具 举报

4四处流浪
321/500
排名
8010
昨日变化

0

主题

78

帖子

321

积分

Rank: 4

UID
181921
好友
0
蛮牛币
389
威望
0
注册时间
2016-11-8
在线时间
89 小时
最后登录
2019-4-25
2018-11-29 11:27:40 显示全部楼层
这个厉害了
回复

使用道具 举报

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

本版积分规则