游戏中人物的阴影等级分类
1.无阴影
对于一些重要性很低或者很难被注意到的,当然大多数情况下是硬件设备配置达不到要求而采用低配画质设置的,这种情况下直接不产生任何阴影,虽然这看起来会让玩家觉得不那么真实,但是为了能让低端机上能够保持一定的帧率运行,这样是不可避免的
2.脚底假阴影
fake_shadow,脚底的阴影贴图,为所有需要添加假阴影的人物,脚底添加假阴影预制体,实时跟随人物移动,开销只是一张贴图的开销,但不太适用于移动的NPC或者怪物,毕竟游戏中地形高低起伏,一张贴图可能会产生穿帮,所以为静止的NPC使用这种较为合适
3.Unity系统的实时阴影
在有光源并勾选了Cast Shadow的情况下,物体会投射阴影,勾选了Receive Shadow的物体表面会接收到投射的阴影,由于阴影是实时的,一般移动的物体使用这个效果会比较好,但同时这个开销也是不可避免的。unity自带的shadowmap阴影基于深度,需要浮点纹理的支持,但是有的移动平台不支持,而且开销很大。
shadowMap的阴影实现原理:将相机放在和光源重合的位置上,那么场景中相机看不到的地方就是光源的阴影区域,然后通过相机将这个阴影映射纹理存储,可以每次采样得到某个方向距离光源最近的点(比这个点远则说明在阴影中)
4.软阴影
何为软阴影,游戏引擎中的阴影为实时的数学计算,投射出的阴影都是有棱有角,在阴影和非阴影的边界也是区分明显,而真实世界中则不是这样,真实世界中的阴影收到间接光照等的影响,在阴影边缘形状往往不那么锋利,而且往往过渡平滑。那么如何在引擎中实现这一技术,近几年大火的实时光照追踪技术,即将一束光的第一次,第二次甚至更多反射的影响都计算上去,得到更为真实的渲染效果,不过这一过程计算复杂,开销往往也比较大。
5.特殊的阴影实现方式——投影器(Projector)
1.用来实现实时阴影
先来看看Unity自带的ShadowMap的实时阴影有什么弊端
(1)ShadowMap采集场景中所有物件的信息,进行深度排序,在用到的时候进行采样,需要浮点数的支持,相当于存了一张很大的图,开销可想而知,而且有的平台甚至可能不支持
(2)在一位前辈的知乎专栏看到shadowMap会影响到静态合批的数量和效率,具体原因分析想来也合理,shadowMap存储了场景中物件的深度信息,这在一些宏的设置时打乱了合批的顺序,使得很多物件无法被合批,batch数量增加
那么投影器怎么实现的呢
原理就是:将要投射阴影的cube设置为shadow层然后相机渲染出RT,然后用这个RT再去渲染接受阴影的物体
Projector projector = GetComponent
// 设置相机参数
Camera camera = gameObject.AddComponent
camera.cullingMask = shadowCasterLayer;
camera.orthographic = true;
camera.orthographicSize = projector.orthographicSize;
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(1.0f, 1.0f, 1.0f, 0.0f);
_renderTex = new RenderTexture(1024, 1024, 0, RenderTextureFormat.ARGB32);
camera.targetTexture = _renderTex;
camera.SetReplacementShader(Shader.Find("Unlit/ReplaceShadowRender"), null);
// 将相机渲染的图像传递给要投影的shader
projector.material.SetTexture("_ShadowTex", _renderTex);
效果如图:Cube为shadow层,Plane为非shadow层,每帧阴影都会得到更新
添加一个接受阴影的小Cube,层级为非 Shadow,可以正确投影到小Cube上,分别为小的Cube投射和不投射阴影的情况
2.用来实现软阴影
软阴影的实现如果使用实时光线追踪的方式开销很大,而一种取巧的方式就是通过投影器,投影的纹理图片我们可以自定义,选用一个合适的边缘模糊的纹理,就可以让我们的影子看起来像是软阴影,最右边为硬阴影,有棱有角,中间为软阴影,左边固定在下面的fake阴影,上面已经介绍了fake阴影和硬阴影的实现,软阴影的实现原理类似,选用合适的渐变的黑白纹理,使得投影出的阴影有个渐变过程
6.FrameDebug
1.投影器的效率
Camera.Render一共有19个事件,
(1)在UpdateDepthTexture中有四个,可以看出,第一个是用来清空颜色缓冲,z缓冲,模板缓冲,然后3个事件来渲染三个场景中物体的深度信息,即DepthJob
(2)中间13个,9个用于渲染不透明物体,1个天空盒在渲染完其他物体后渲染,3个渲染透明队列的物体为什么渲染透明物体3个Pass呢,通过改变Cube的Layer可以发现,这一步的操作实际上是我们投影器Projector再次渲染场景中的物体,现在场景中有一个Plane,一个Cube,一个Cube1,如果三个都是接受投影即layer为非shadow则透明队列会有三个消耗,如果只把cube的layer改成shadow,那么会有两个消耗,因为cube需要分别向Plane和Cube1投影,同理,如果两个cube都为shadow,那么接受投射的物体就只剩下plane,那么这里就这只有一个开销。
分析可知,使用投影器的时候,如果一个投影器投射的影子需要有很多物体接受,那么会增加很多这个开销,还是要注意权衡
(3)投影器使用一张RT来存储产生阴影的物体,然后用这个RT重新绘制接受阴影的物体,RT的尺寸关系到了我们阴影的质量,demo中使用的是1024*1024的RT,但是还是稍微有些锯齿感
7.Dynamic Shadow Projector代码分析
核心脚本有两个,ShadowTextureRenderer和DrawTargetObject两个,看到面板中的一些设置,投影的颜色,纹理的尺寸,mipMap的level,模糊的程度等等,核心思想就是控制RT的一些参数来控制投影的阴影的外观尺寸等参数,而DrawTargetObject脚本中则重点实现了阴影投射的递归遍历等操作,将一个物体及其所有子物体的材质和shader参数正确设置