GAMES202:GAMES202-高质量实时渲染_哔哩哔哩_bilibili
一、基于图片的实时阴影技术
主流方法之一,把阴影生成为一张图片
平面投影阴影
根据光方向,把物体每个顶点投影在一个平面上。
缺点:必须是平面,产生阴影的物体必须介于光和平面之间
为了解决上一个方法的缺点
- 为每个阴影投影体设置一个相机。
- 把阴影投影体渲染到阴影纹理中。
- 渲染阴影接收者,并且与阴影纹理进行混合。
投影阴影-Unity中的实现
Projector组件:可以设置它的参数生成一个视锥体。
Projector会使用提供给它的材质,绘制所有在它视锥体内的物体。
使用RenderTexture生成阴影映射图,把阴影投影体绘制到阴影纹理。
给Project设置物体和阴影映射图混合的材质。
在方块上挂上投影组件,设置参数,生成一个视锥体,最后会给视锥体的物体再加上一层阴影的渲染Pass。
阴影映射(ShadowMapping)
概念:从光源的位置和角度获取的深度图
核心思想:对比Shadow Map和摄像机视角的深度图,片元在Shadow Map中的值小于后者时,产生阴影
流程概述
1.在灯光位置放置摄像机,生成这个摄像机的深度图(Shadow Map)。
根据左图假如我们知道绿色的点的位置,它没有被遮挡,在右侧图中我们可以推出大概在绿色点的那个位置如图所示。但是左侧的红色点处于阴影内部,在右侧图中是被遮挡的,所以肯定是不可见的。这就是阴影映射的核心思想。
渲染阴影的时序图
首先从光源的位置渲染深度图,这张图我们称作阴影映射。完成这一步后我们从摄像机的视角, 渲染整个场景的物体,每次渲染都要和阴影映射进行一次深度测试, 比如说红色的点位置,它的深度是摄像机视角的深度,所以要转换成阴影映射的坐标系,保证这个计算中坐标系一致, 然后就比较两个的深度值。当摄像机视角中某个片元的深度值>阴影映射的深度值其实就是场景物体本身的深度时,说明在灯光视角下,看不到着色点,这时将着色点设为阴影,反之就是没有阴影。
Unity中的屏幕空间阴影映射
步骤:
- 首先得到屏幕空间的深度图(摄像机视角下的深度信息), 在延迟渲染中已经存在,在前向渲染中需要把 首先得到屏幕空间的深度图(摄像机视角下的深度信息),在延迟渲染中已经存在,在前向渲染中需要把场景渲染一遍,得到深度图 场景渲染一遍,得到深度图
- 然后将摄像机与光源重合(光源空间) 下通过那个特有的pass通道(ShadowCasterPass) 渲染出阴影映射 然后将摄像机与光源重合(光源空间)下通过那个特有的Pass通道(ShadowCasterPass)渲染出阴影映射纹理(其实也是一张深度图)
- 将屏幕空间下的深度图变换到光源空间,与阴影映射纹理进行比较,通过的就会渲染出来。若前者深度更 将屏幕空间下的深度图变换到光源空间,与阴影映射纹理进行比较,通过的就会渲染出来.若前者深度更大(深度越大越不可见),则说明该区域虽然可见,处于此光源的阴影中。 大(深度越大越不可见),则说明该区域虽然可见,处于此光源的阴影中.
- 通过比较后得到一张包含了阴影信息的屏幕空间阴影图,通过这张图对影进行采样,就可以得到最后的 通过比较后得到一张包含了阴影信息的屏幕空间阴影图,通过这张图对影进行采样,就可以得到最后的阴影效果了。
在Frame Debug中的渲染流程截图:
下面分别时ShadowMap、Collect Shadows、最终效果:
以下为精要的代码使用了 使用CGINC文件
// Upgrade NOTE: replaced '_LightMatrix0' with 'unity_WorldToLight'
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/attenal"
{
Properties
{
_Diffuse ("Diffuse",Color)=(1,1,1,1)
_Specular("Specular",Color)=(1,1,1,1)
_Gloss ("Gloss",Range(1,255))=10
}
SubShader
{
Tags { "LightMode"="ShaderCaster" }
LOD 100
Pass
{
Tags{"LightMode"="ForwardBase"}
CGPROGRAM
//#pragma multi_compile_fwdbase可以保证我们在Shader中使用光照衰减等
//光照变量可以被正确赋值。这是不可缺少的
#pragma multi_compile_fwdbase
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float3 NdirWS : TEXCOORD0;
float3 vertexWS:TEXCOORD1;
float4 pos : SV_POSITION;
//作用是声明一个用于阴影纹理采样的坐标。
//这个宏的参数需要是下一个可用的插值寄存器的索引值
SHADOW_COORDS(2)
};
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.NdirWS = UnityObjectToWorldNormal(v.normal);
o.vertexWS = mul(unity_ObjectToWorld, v.vertex);
//用于在顶点着色器中计算上一步中声明的阴影纹理坐标
TRANSFER_SHADOW(o);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
//得到平行光的方向
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
//计算了环境光
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
//lambert
float3 LdirWS = normalize(_WorldSpaceLightPos0.xyz);
float3 NdirWS = normalize(i.NdirWS);
float3 diffuse =_LightColor0.rgb* _Diffuse.rgb*max(0,dot(NdirWS,LdirWS));
//phone
float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.vertexWS.xyz);
float3 hDir = normalize(LdirWS + vDir);
float3 specular =_LightColor0.rgb*_Specular*pow(max(0,dot(hDir,NdirWS)),_Gloss);
//UNITY_LIGHT_ATTENUATION 是Unity 内置的用于计算光照衰减和阴影的宏
//第一个参数是atten,第二个参数是结构体v2f ,第三个参数是世界空间的坐标
UNITY_LIGHT_ATTENUATION(atten,i,i.vertexWS);
return float4((specular+diffuse)*atten,1.0);
}
ENDCG
}
Pass{
//通常来说,Addtional Pass的光照处理和Base Pass的处理方式是一样的,因此我们只需要把
//Base Pass的顶点和片元着色器代码复制到Additional Pass中,稍微修改一下即可。
Tags{"LightMode"="ForwardAdd"}
//开启和设置了混合模式
//希望Additional Pass 计算得到的光照结果与之前的光照结果进行叠加。
//没有使用Blend命令的话,Additional Pass会直接覆盖掉之前的光照结果
//我们也可以选择其他Blend命令,如Blend SrcAlpha One
Blend One One
CGPROGRAM
//这个指令保证我们再Additional Pass中访问正确的光照变量
#pragma multi_compile_fwdadd
#pragma vertex vertadd
#pragma fragment fragadd
#include "Lighting.cginc"
#include "AutoLight.cginc"
fixed4 _Diffuse;
fixed4 _Specular;
float _Gloss;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f{
float4 pos : SV_POSITION;
float3 worldNormal : TEXCOORD0;
float3 worldPos : TEXCOORD1;
};
v2f vertadd(a2v v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
return o;
}
fixed4 fragadd(v2f i):SV_Target{
fixed3 worldNormal = normalize(i.worldNormal);
//如果是平行光._WorldSpaceLightPos0.xyz世界空间下光源的位置
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else//是点光或者是聚光
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb*max(0,dot(worldNormal,worldLightDir));
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz-i.worldPos.xyz);
fixed3 halfDir = normalize(worldLightDir+viewDir);
fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(worldNormal,halfDir)),_Gloss);
//处理不同的光源衰减
//如果是平行光
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
float3 lightCoord = mul (unity_WorldToLight,float4(i.worldPos,1)).xyz;
fixed atten = tex2D(_LightTexture0,dot(lightCoord,lightCoord).rr).UNITY_ATTEN_CHANNEL;
#endif
return fixed4((diffuse+specular)*atten,1.0);
}
ENDCG
}
}
Fallback "Specular"
}
阴影映射(ShadowMapping)的问题与优化
问题1:自阴影/自遮挡
如上图,由于阴影映射的分辨率有限,离散的采样点以及数值上的偏差可能造成不正确的自阴影,也被成为Z-Fight或者阴影粉刺(Surface Acne)。
Games202中描述
- 从light往场景看(左边太阳的线)时,比如沿着某一个像素往过看,看到的位置(红色斜线)就是像素所代表的深度,(假设任何一个像素是一个常值的深度)。也就是说,在shadowmap看来,场景离散成了图中红色斜线形成的结果。
- 第二个pass渲染的时候,从camera出发(右边连着眼睛的蓝色虚线),常规操作:连向light (左边蓝色虚线),这段虚线长度就是它的深度
问题:在shadowmap记录的对应的深度是橙色的部分(更浅),而实际上从相机看向这个像素,它的深度应该是被橙色部分遮挡住的点(两条蓝色虚线的交点)→这就是自遮挡情况
- 光垂直的,从上往下照时候,不存在问题
- 光非常偏的时候,问题最大(例如:塞尔达中夕阳西下时)
解决方法
深度偏移(Depth Bias)
设置一个差值的阈值,减少阴影的产生。太大会导致Peter Panning(阴影与投影者脱节)
出现原因
- 由于shadow map的精度肯定是有限的,其记录的深度值必然是离散化的,而Plane本身是连续的,这就造成平面上一段连续的不同的深度,记录在Shadow map.上是一一个相同的深度值。
- 认为shadowmap上的深度,明显比实际的深度小的情况下,橙色的这一段障碍物就不算了。
- bias可以不是一个常数,可调整,如果垂直打光,可以非常小,夹角很大的情况,可以更长一点。(夕阳下阴影会出现这种情况)
- 当Bias设置过大时,还会导致下边这种问题:Peter panning(学术界叫作detached shadow)
优化方案:
增加ShadowMap的分辨率将上图中的区间间隔再划分一次,让两个点对应的阴影映射值在不同的区域,从而消除自阴影。但是这样很多时候并不现实,对于视野开阔的游戏来说(比如说开放大世界游戏),它们的可视范围是很大的,阴影映射分辨率是跟不上场景规模的扩大倍数,而且很多平台对纹理的尺寸都是有一定限制的。
解决方案:
在上图中红色的一点假设是我们采样的阴影映射值,那么对应的红线部分采样的值按理来说都是一样的,蓝色这一点是摄像机视角下物体表面采样的深度值,但是实际上蓝点部分的深度是大于红点部分的深度的,但是我们却把这部分算在阴影计算中了,阴影瑕疵就是这样产生的,那么如果把红点阴影映射部分加上一个深度偏移,下半部分就和物体表面一样。但是对于上半部分,如果加上一个偏移,那么就会产生Peter Panning的问题,所以深度偏移是很难适配到整个场景的。我们只能尽量微调来适应场景。
法线偏移
- 深度偏移会使得该像素向光源方向靠近,其次还有法线偏移的概念。
- 法线偏移是沿着表面的法线方向向外偏移。
- 偏移单位是阴影映射的纹素。也就是说当你的阴影映射的分辨率是512时,一个单位的深度偏移就是1/512,如果这个时候修改阴影映射的分辨率为1024,那么bias就会变为1/1024。
- 在Shadow Receive计算阶段,逐像素进行。
- bias是在阴影深度测试的时候使用,不会影响真实场景中物体的法线。
Unity中实现自阴影的优化
问题2:走样
走样的表现
走样的出现原因以及解决办法
初始采样:渲染shadowmap时
最严重的问题:透视走样
解决方案-级联阴影映射(Cascaded Shadow Map)
它的想法就是把摄像机视角看到的视锥体分割成多个子视锥体,每一部分视锥体都对应一-张
阴影映射,同时它每个阴影映射的分辨率大小是一样的,在右图中可以看到在Untiy中对应的
参数。
Two Cascaded表示2层,最大可以支持10个子视锥体。同时它设置成2层的时候,前33.3%的
是算作近的视锥体,后66.7%的是算作后面的视锥体。因为距离越近,物体显示的范围一般
会越大,需要的质量高;距离越远的显示范围会很小,需要的质量也就越低,所以这样是可
以充分利用资源的。另外,还有Shadow Distance参数,它代表阴影范围不再是近平面到远
平面,而是近平面到设置的值这段距离,所以它的级联阴影也就会在这个范围内产生阴影。
显然它减小了阴影的开销。
Showmak Mode:包含Shadowmask和Distance Shadowmask的选择。
Shadows:包含硬阴影和软阴影的选择。
Shadow Resolution:Shadow Map的分辨率选项,低、中、高、非常高。
Shadow Projection:这个选项会影响级联带的形状。一般是默认“Stable Fit”,在这个模式下,根据到相机的距离选择频段。另一个选项是“Close Fit”,根据相机的深度选择频段,这样可以更有效地使用阴影纹理,但是这个选项会导致改变相机位置的同时会产生阴影锯齿游泳的情况。
Shadow Distance:阴影距离。即光源相机到物体的距离。
Shadow Near Plane Offset:阴影近平面的偏移。
Shadow Cascads:阴影的级联数选项。目前有1、2、4选项,1就是没有开启级联阴影。
Cascade splits:显示每个级联阴影的距离范围占比。
重采样:从相机位置对shadowmap进行重采样时
阴影映射是一张动态生成的纹理。
滤波(Filter)
- 图像处理中,通过滤波强调一些特征或者去除图像中一些不需要的部分(比如高斯模糊) ;
- 滤波是一个邻域操作算子,利用给定像素周围的像素的值决定此像素的最终的输出值(比如卷积核)。
阴影的滤波
使用一部分阴影映射采样点来计算某个指定View采样点的最终阴影结果的方法。
PCF滤波(Percent-Closer Filter)
所以通过周围像素来过滤决定当前阴影的颜色,我们称之为PCF滤波。这种算法不是唯一,可以实现很多种采样思想,不同的实现方式主要体现在以下两方面:
采样数K:规则滤波,3*3或5*5
采用Poisson Disk的形式来分布一定数量的采样点。
滤波核函数:高斯函数作为滤波函数,也可以用其它。
下图就是Unity中用3*3高斯核做的一个滤波采样:
PCF滤波的缺点:因为滤波的卷积运算总会涉及到多次采样,所以非常影响性能
作业
1.总结实时阴影的优化方案
方法名称 | 方法 | 解决的问题 | 出现原因 |
深度偏移 | 设置一个差值的阈值,减少阴影的产生 | 自阴影/自遮挡 | 阴影映射的分辨率有限,离散的采样点以及数值上的偏差 |
法线偏移 | 沿着表面的法线方向向外偏移 | 自阴影/自遮挡 | |
透视投影 | 在Shadow Map生成时进行透视投影,以保持均匀性的一致 | 透视走样 | 阴影映射在世界空间均匀分配。视锥体内物体经过透视投影后,近大远小,近平面和远平面的像素一样;靠近观察者的元素所用到采样点明显变少。 |
级联阴影映射 | 从近到远划分视锥体,得到相同大小的Shadow Map | 透视走样 | |
PCF滤波 | 对阴影贴图滤波,得到shadow值 | 重采样 | 阴影映射在世界空间均匀分配。视锥体内物体经过透视投影后,近大远小,近平面和远平面的像素一样;靠近观察者的元素所用到采样点明显变少。 |