Unity移动端动态阴影总结

2017年05月04日 16:40 0 点赞 0 评论 更新于 2025-11-21 21:24

一、基于Cubemap的动态软阴影

ARM公司曾利用Unity开发过两款技术Demo(Ice Cave和Chess Room),充分发挥了Cubemap的强大功能,不仅用于地面反射、冰块折射,还用于实现动态软阴影,凭借简单的技术打造出了高品质画面。以下是Ice Cave的效果展示。

反射、折射部分可参考:Reflections Based on Local Cubemaps in UnityARM Guide for Unity Developers,下面将重点介绍软阴影部分的原理。

以国际象棋屋为例,在屋子中间放置一个Reflect probe来拍摄周围环境,仅使用了Cubemap的RGB通道。实际上,周围环境的Alpha通道也代表了光是否穿透窗户或被墙壁遮挡,因此可以利用Cubemap剩余的Alpha通道来存储光与周围环境的遮挡情况,Alpha通道图如下(此处原文未给出图)。

生成Cubemap的细节可参考AssetStore中的源码:Asset Store

利用生成的Cubemap渲染阴影主要分为两步:一是将向量L(vertex - to - light)转换为Lp(校准过的vertex - to - light,用于采样Cubemap);二是进行软阴影处理。

1. L到Lp向量校准

输入参数

  • _EnviCubeMapPos:cubemap中心坐标
  • _BBoxMax:包围盒最大坐标,在生成Cubemap时自动生成
  • _BBoxMin:包围盒最小坐标,在生成Cubemap时自动生成
  • V:顶点坐标
  • L:vertex - to - light向量,已归一化

输出参数

  • Lp:校准后的vertex - to - light向量,作为UV去采样Cubemap

校准过程

先利用线和包围盒求交点,从包围盒位置到交点的向量即为Lp,然后使用Lp去采样Cubemap用于着色。另外,需要对背面进行特殊处理,以防止出现阴影穿透问题。

2. 软阴影

阴影平滑的过程十分有趣。首先,将Cubemap过滤方式选择为tri - linear filtering,然后计算vertex - to - intersection - point(顶点到交点)向量的长度,并乘以外部传入系数。

为了平滑阴影,使用texCUBElod去采样Cubemap,其中UV的XYZ来自Lp,W来自vertex - to - intersection - point(顶点到交点)的距离。从下图(此处原文未给出图)可以看到,离窗户越远处的阴影越模糊。

这种实现阴影的方法的局限性在于,比较适合室内环境、点光源位置不变且内部有移动物体的情况。

二、地面云阴影

对于地面上的云阴影,使用实时灯光照射来产生阴影显然不划算。可以直接在地面shader中混合一个运动的云图,就能达到类似的效果。

由于gif图无法显示,可查看:云阴影

我使用shaderforge创建了一个简单的版本。另外,这种方法也可用于实现地面风雪效果。

三、植物摇曳阴影

对于树、草、旗子这类位置不变但有摇曳动画的物体,可以预先将阴影烘焙到贴图中,然后将阴影图作为单独贴图或地面贴图Alpha通道传送到地面shader中,之后只需添加阴影晃动的特性,使其随植物晃动而晃动,就能营造出真实阴影的感觉。同时,要注意阴影的方向以及与植物晃动同步等细节。

具体细节可参考:手机游戏中大量植物图像的伪阴影渲染

四、结合Projector和Rendertexture的实时阴影

创建一个跟随主相机的阴影相机,将其改为正交投影,并设置单独的shadow Layer,把需要投射阴影的物体设置到shadow layer。为该阴影相机设置渲染目标到一个渲染纹理RTT_Shadow。另外,创建一个Projector,为其设置一个材质Mat_Proj,并将RTT_Shadow传递到Mat_Proj的shader中进行着色。为防止投影相机边缘出现刺刺的长线,需要设置一个阴影衰减纹理;如果需要软阴影,则需要另外进行Blur处理。

这是近年来手游中应用较为广泛的方法,网上有很多相关文章,例如:结合Projector和Rendertexture实现实时阴影ProjectorShadow(手游上的实时阴影方案)。另外,AssetStore也有不少类似插件:Fast Shadow Projector

五、角色脚下阴影面片

对于游戏中的NPC、杂兵、野怪等非关键性角色,可以直接放置一个阴影面片来模拟阴影。当然,如果地面起伏较大,可能会出现穿插问题。

六、Light Probe

具体细节可参考Unity手册,此处不再赘述:Light Probes

七、Shadow Maps

1. Standard Shadow Mapping

其基本思想是在光源位置放置一个相机(Light space Camera),绘制一遍深度得到深度图。在渲染场景时,将pixel坐标转换到Light Space计算深度,然后将其与深度图中的深度进行比较。如果比深度图中的深度大,则意味着该像素处于阴影中;否则,该像素被照亮。

阴影的锯齿主要有两类:透视导致的锯齿(Perspective alias)和投影导致的锯齿(Project alias)。

2. PCF

投影导致的锯齿是由于灯光投射方向和物体表面夹角过小时,多个pixel对应阴影图的一个texel。这可以通过提高阴影图的大小来解决,也可以通过Percentage Closer Filtering(PCF)来柔化边缘。PCF在绘制时,除了绘制当前点,还会对周围像素进行多次采样、混合,以柔化锯齿。常用的PCF方法有:使用随机采样实现soft shadow、泊松采样等。

3. PSM

透视导致的锯齿是由透视的近大远小特性引起的。于是,出现了Perspective Shadow Map(PSM),它将整个Shadow Map的计算过程转到归一化设备空间(NDC)进行计算,从而消除了近大远小的问题。下图(此处原文未给出图)展示了Standard Shadow Map和经过Perspective Shadow Map优化后的阴影,优化后的阴影明显更细致。

然而,PSM本身存在很大局限性,例如影子质量比较依赖视角方向、近处阴影与远处阴影Z分布过大。

4. LISPSM

在PSM的基础上,又出现了新的阴影技术Light Space Perspective Shadow Maps(LISPSM)。它在与灯光方向垂直的方向构建View Frustrum,然后将灯光、场景都转换到这个View Frustrum的Perspective space,再计算Shadow Map。这样,无论是点光、聚光还是平行光,都可以转换为平行光。

左图(此处原文未给出图)是Uniform(近处精度不足),中间是LISPSM(近处、远处精度都不错),右面是PSM(远处精度不足)。

LISPSM具体细节参考:https://www.cg.tuwien.ac.at/research/vr/lispsm/shadows_egsr2004_revised.pdf

5. VSM(方差阴影)

在使用PCF时,一般不能提前对Shadow Map进行模糊处理,因为这会导致PCF计算不准确。而Variance Shadow Maps(VSM)则没有这个限制。VSM存储的Shadow Map不仅包括深度,还包括深度的平方,此时可以对Shadow Map进行过滤,然后利用切比雪夫不等式计算出大于当前深度的概率上限,即阴影区的概率。

左图(此处原文未给出图)是Standard Shadow Map,右图是Variance Shadow Map。

具体细节参考:Variance Shadow MappingVSM的demosMatt's Variance Shadow Maps

6. CSM/PSSM

这是两种分别研究发表但原理几乎相同的阴影技术,Unity使用的是CSM,其中PSSM是由几个中国人(Zhang F, Sun H Q, Xu L L, et al)提出的。它们的原理如下:

a) 切分阴影图

对摄像机视锥体内沿着Z轴由近到远将阴影图切分为多张,切分规则是均匀切分和指数切分的混合,两者按照一定比率进行混合。

b) 计算cropMatrix

针对每一块分别计算一个光源投影空间内平移、缩放的矩阵cropMatrix,该矩阵可以将切分的多块移动、缩放到光源的视椎中,其形式与正交投影矩阵非常相似。

// Build a matrix for cropping light's projection
// Given vectors are in light's clip space
Matrix Light::CalculateCropMatrix(Frustum splitFrustum)
{
Matrix lightViewProjMatrix = viewMatrix * projMatrix;
// Find boundaries in light's clip space
BoundingBox cropBB = CreateAABB(splitFrustum.AABB, lightViewProjMatrix);
// Use default near - plane value
cropBB.min.z = 0.0f;
// Create the crop matrix
float scaleX, scaleY, scaleZ;
float offsetX, offsetY, offsetZ;
scaleX = 2.0f / (cropBB.max.x - cropBB.min.x);
scaleY = 2.0f / (cropBB.max.y - cropBB.min.y);
offsetX = -0.5f * (cropBB.max.x + cropBB.min.x) * scaleX;
offsetY = -0.5f * (cropBB.max.y + cropBB.min.y) * scaleY;
scaleZ = 1.0f / (cropBB.max.z - cropBB.min.z);
offsetZ = -cropBB.min.z * scaleZ;
return Matrix( scaleX,     0.0f,     0.0f,  0.0f,
0.0f,   scaleY,     0.0f,  0.0f,
0.0f,     0.0f,   scaleZ,  0.0f,
offsetX,  offsetY,  offsetZ,  1.0f);
}

c) 渲染阴影图

针对切分的每一块渲染阴影图,通常阴影图大小相同,例如都是1024 * 1024。由于近处包含的场景范围比远处小,所以近处阴影图的精度会更高。

d) 渲染场景阴影

最后进行场景阴影的渲染。

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章