代码部分为Unity实现屏幕空间的实时阴影
学习教程来自:【技术美术百人计划】图形 4.3 实时阴影介绍
笔记
1. 基于图片的实时阴影技术
主流方法之一,把阴影生成为一张图片
1.1 平面投影阴影
将阴影投影在一个平面上。
缺点:必须是平面,产生阴影的物体必须介于光和平面之间
为了解决上一个方法的缺点之一(只能在平面上产生阴影)。
步骤简述:为物体多设置一个相机产生阴影纹理,与被阴影覆盖的表面的纹理进行混合得到阴影效果,在Unity中使用Projector组件实现
1.2 阴影映射(Shadow Map)
概念:从光源的位置和角度获取的深度图
核心思想:对比Shadow Map和摄像机视角的深度图,片元在Shadow Map中的值小于后者时,产生阴影
1.3 屏幕空间阴影映射
Unity中的阴影映射实现(即屏幕空间的阴影映射)
步骤:
- 屏幕空间的深度贴图
光源方向的Shadow Map
屏幕空间下进行对1、2的结果进行计算得到屏幕空间的阴影纹理
- 绘制3中的结果
2. 阴影映射的优化
2.1 自阴影问题
也叫Z-Fighting、阴影瑕疵、阴影粉刺(Surface Acne),由于阴影贴图分辨率(其分辨率低,但是相机得到的深度图分辨率高)、离散采样、数值精度等问题产生的错误的自阴影
解决办法:
- 深度偏移(Depth Bias):设置一个差值的阈值,减少阴影的产生。太大会导致Peter Panning(阴影与投影者脱节)
- 法线偏移(Normal Bias):上一条中的方向为视角方向,本方法在法线方向上偏移
补充:偏移单位为纹素(1/分辨率),只在阴影深度测试时使用,不影响其他效果2.2 走样问题
由于采样产生2.2.1 透视走样
Shadow Map本身大小均匀,但透视投影完成后采样变得不均匀,由此产生了走样(距离观察者近的元素产生走样)
解决:
- 在Shadow Map生成时进行透视投影,以保持均匀性的一致
- 级联阴影映射(Unity的解决办法):划分视锥体,得到相同大小的Shadow Map(近处的质量更高)
我猜这也是为什么上边1.3中的Shadow Map有4个
2.2.2 重采样
采样贴图时产生的误差
解决:滤波(PCF滤波),对滤波核的每一个采样点,对比中间的值后划分为blocked和visible这2种状态,输出shadow=visible/(visible+blocked)。实现方式有很多种(不同的采样个数、不同的滤波函数)
作业
1. 总结实时阴影系统的优化方案
内容来自以上笔记:
方法名称 | 方法 | 解决的问题 |
---|---|---|
深度偏移 | 使用一个偏移值来避免深度比较时产生的误差 | 自阴影 |
法线偏移 | 沿法线方向进行偏移 | 自阴影 |
透视投影 | 在Shadow Map生成时进行透视投影,以保持均匀性的一致 | 透视走样 |
级联阴影映射 | 从近到远划分视锥体,得到相同大小的Shadow Map | 透视走样 |
PCF滤波 | 对阴影贴图滤波,得到shadow值 | 重采样 |
2. 自己实现阴影系统
Tips:可以把模型的背面渲染出来作为阴影的一个Pass,来优化和避免一些问题 //TODO:
做这个是四处看代码,左抄点,又抄点,混一起成了。
参考来源:
Unity的实时阴影-ShadowMap实现原理:粘了不好使(盲猜shadowmap的矩阵转换不对),但学到大概知道要这么2个shader,一个脚本
Unity基础6 Shadow Map 阴影实现:对着上边的代码看了一下每一步大概的实现
Unity实时阴影实现——Shadow Mapping:然后看到这篇,比较接近能直接来拿粘贴的程度了,但还差点,大概是在update里调用一下函数,先调哪个后调那个不太确定。
上一条作者的github:完结,里边有一个老一点版本的shadowmap实现(对应PPT,1个shadowMap的Shader,1个屏幕空间深度的Shader,1个比较这2个深度图并计算阴影的Shader),现在对着上边的文章改成在接收阴影的材质shader中比较深度计算阴影,完成实现(2个shader,1个计算shadowMap,1个计算阴影)
总结一下实现过程,好像和上边PPT讲的不太一样:
- 从光源方向创建相机,渲染深度图的得到shadowMap
- 保存一个变换矩阵_gWorldToShadow,将阴影接收者的世界空间坐标转换到shadowMap对应的空间中(和PPT中不同,这里在shadowMap对应的空间下做collector计算,而不是将shadowMap转换到屏幕空间)
- 转换后的坐标,xy值作为UV采样shadowmap得到sampleDepth,z作为深度depth(这些值都经过了一些处理达到0-1),这里的depth并不是渲染深度得到的,而是一个位置信息,如果大于shadowMap采样的值,证明被挡住了,应该有阴影产生
- 用sampleDepth和depth计算得到阴影(sampleDepth可以for循环多采样几次计算模糊)
比较关键的地方就是把接收者的顶点位置转换到shadowMap对应的空间下(xy值作为UV采样shadowmap得到sampleDepth,z作为深度depth)
v2f vert (appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos (v.vertex);
float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
o.shadowCoord = mul(_gWorldToShadow, worldPos);//转换到shadowMap对应的空间
return o;
}
fixed4 frag (v2f i) : COLOR0
{
// shadow
i.shadowCoord.xy = i.shadowCoord.xy/i.shadowCoord.w;
float2 uv = i.shadowCoord.xy;
uv = uv*0.5 + 0.5; //(-1, 1)-->(0, 1)
float depth = i.shadowCoord.z / i.shadowCoord.w;
#if defined (SHADER_TARGET_GLSL)
depth = depth*0.5 + 0.5; //(-1, 1)-->(0, 1)
#elif defined (UNITY_REVERSED_Z)
depth = 1 - depth; //(1, 0)-->(0, 1)
#endif
// sample depth texture
// 模糊前
//float4 col = tex2D(_gShadowMapTexture, uv);
//float sampleDepth = DecodeFloatRGBA(col);
//float shadow = sampleDepth < depth ? _gShadowStrength : 1;
// 模糊后
float shadow = PCFSample(depth, uv);
return shadow;
}
上图:
刚开始有个小问题:之前建模的钢铁侠面罩模型只有一个面,没有封口,就是背面是透的,结果来到这里正面面对阳光的时候,竟然没有阴影,开了framedebug,原来是cull front去掉了正面。如图
注释掉好了,看来这个就是开头说的直接用背面作为渲染阴影的Pass了,这样做的好处猜测应该是,反正shadowMap空间下正面和背面一定会相互遮挡,不如只考虑一个面,而背面的复杂度应该又比正面低一点,所以这样是效率比较高的
最后是边缘模糊PCF Soft Shadow(Percentage Closer Filtering),代码也来自上边知乎大佬的帖子,原理就是对ShadowMap围绕着中心点采样了9次,每次都和中心点的depth比较后累加,再除9
float PCFSample(float depth, float2 uv)
{
float shadow = 0.0;
for (int x = -1; x <= 1; ++x)
{
for (int y = -1; y <= 1; ++y)
{
float4 col = tex2D(_gShadowMapTexture, uv + float2(x, y) * _gShadowMapTexture_TexelSize.xy);
float sampleDepth = DecodeFloatRGBA(col);
shadow += sampleDepth < depth ? _gShadowStrength : 1;//每一个采样点都与深度值相比较,累加
}
}
return shadow /= 9;
}
最后,这些代码也上传git了,感兴趣的可以拉下来看看(本篇内容在/Scene/4300)
git地址