本文将从阴影产生的原理去讲解和剖析阴影过程,分享常用的几种阴影产生的方式,以及阴影的几种锯齿解决方法及优化方案。
本章一将做一些阴影效果分享和原理讲解并实现最基本的阴影。
本文分以下几个章节
(一)阴影原理和最基本的阴影实现
(二)阴影的抗锯齿优化
(三)多种阴影的生成方式
一.效果分享
相信大家从这两幅图的阴影效果就可以看出来,图一的阴影是最基本的阴影,图二的阴影是稍微处理过后的阴影 效果就比图一好很多 几乎看不到锯齿效果了。关于为什么会有锯齿以及解决锯齿的多种方案,后续都会一一提出来,下面我们来讲解基本阴影原理。
二.阴影产生的过程和原理
如下图所示,AB是地面,BC是遮挡物,在太阳D的照射下产生了阴影AB,其中光照方向为CD方向。其中我们的眼睛摄像机在位置E处,视觉方向为点到E的方向。
从该图可以分析到阴影的原理很简单,也就是要从太阳光的方向看被墙BC挡住的地方就是有阴影的,换句话说也就是在太阳空间视角下点F到D的距离大于B-C点到D 的距离,那么判定是否处于阴影范围就是通过在太阳空间下对比两者的深度。下面来说下实现的具体步骤:
1.应用阶段在D点生成一个ShadowCamea,选择正交相机,使其方向为太阳光照的方向。需要每帧计算使其方向始终处于光照方向以便动态更新。(我们通常说的光照方向是点到太阳的方向,这儿按图方向描述)
2.使用ShadowCamera渲染场景生成一个生成该camera空间下的深度图,这里要注意ShadowCamera渲染的纹理图大小和相机的可视范围大小,原则上 shadow渲染范围越小,肯定深度信息就越精确,就越不容易产生锯齿,反正如果渲染范围大了,肯定要用阴影抗锯齿算法处理。
3.在E处 也就是我们的maincamera上先正常渲染一次场景,生成场景的深度和法线贴图,可以自己实现也可以用Unity的方式,unity中camera.depthTextureMode =DepthTextureMode.Depth/DepthNormal;在GPU中就可以访问_CameraDepthTexture/_CameraDepthNormalsTexture贴图去取的当前像素点对应的深度和法线。
4.最后计算也是最复杂的一步,需要再渲染一遍场景进行深度对比产生阴影,在GPU中只能取到每一个点的世界空间坐标和MainCamera的View空间坐标。这里采用一个ShadowCamera.worldToCameraMatrix矩阵,把世界空间该点转换到ShadowCamera空间下的View坐标。在ShadowCamera的View空间下的坐标点Z值就是一个深度值,这里要注意下该深度z值范围在0-farclip范围内,也就是摄像机的可见范围。然后再从上面产生的_CameraDepthTexture贴进行采用获得该点对应uv坐标处的深度采用depth,注意该depth范围是0-1.进行深度对比的时候需要把Z和depth映射到同一个范围,一般用z除以farclip就可以得到0-1空间内的深度值。然后进行对比float ooc=newdepth>olddepth;就可以产生阴影系数了,那么occ的值是0或者1,然后再乘以原颜色值就可以把阴影部分表示出来了。
OK!下面我们来具体实现这个过程,有很多细节需要注意,可能初学的同学们不一定清楚。
三.阴影的实现过程
第一步把阴影相机放在光照方向后,用阴影相机渲染深度图,C#方面代码就省掉了 ,主要是贴Shader内容
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 view : TEXCOORD1;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
float4 view = mul(UNITY_MATRIX_MV, v.vertex);
o.view.w = -view.z*_ProjectionParams.w;//w是远裁剪的倒数 =1/farclip
o.view.xyz = normalize(mul(UNITY_MATRIX_MV, float4(v.normal, 0)).xyz);
float4 wpos= mul(_Object2World, v.vertex);
return o;
}
float4 frag(v2f i) : COLOR
{
float4 ret;
ret.zw = EncodeNormal(normalize(i.view.xyz));
ret.xy = EncodeDepth(i.view.w);
//ret = tex2D(_MainTex, i.uv);
return ret;
}
下面两张是主摄像渲染的场景深度和法线贴图,深度图大家可以看到越近的地方越黑越接近0,越远的地方越亮,颜色值越接近1.。法线图大家可以看到地面是绿色,它代表了RGB颜色的G分量,也就是空间y轴方向。朝摄像机的方向房屋墙壁是红色,它代表了 这个方向是x轴方向 对应RGB颜色的R分量。
以下这张图是通过阴影相机渲染的法线和深度图,也就是RGB分量存法线,A分量存储深度值。那么存储后,我们后续就可以直接通过访问深度贴图获取某一个点的深度和法线值。
OK!准备工作做好后,我们开始进行正式的阴影计算。
v2f vert (appdata_full v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
float4 wpos = mul(_Object2World,v.vertex);//这个世界坐标是主相机渲染的
o.pos = wpos;
o.normal.xyz = mul(v.normal, (float3x3)_World2Object).xyz;//更精准
float4 shadow_pos = mul(_ShadowView,wpos)+float4(o.normal.xyz*0.07f,0);//这里要对方向方向进行一点点偏移用来抗锯齿
o.shadowvpos = shadow_pos;//把渲染点的世界空间转换到Shadowcamera的View空间,矩阵自己在CPU阶段计算好了 传过来就行了
o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float4 shadowproj = mul(_ShadowProj,i.shadowvpos);//将View空间的坐标转换到proj空间
shadowproj/=shadowproj.w;//归一化
float2 depuv=shadowproj.xy*0.5+0.5;//坐标映射后 可以直接对应上步操作的深度渲染图的uv坐标取深度。
float4 col = tex2D(_ShadowDepth, depuv);
float olddepth = DecodeDepth(col.xy);//直接把阴影相机渲染的深度取出来。
float newdepth = i.shadowvpos.z / invShadowViewport.w;//invShadowViewport.w是相机farclip的大小,直接从CPU阶段传过来
//通常情况下View空间的z值就深度,把他除以farclip后就得到了0-1之间的深度了,这样我们就可以和原深度图进行对比了。
float scale=1;//这里假设了一个阴影系数
if(newdepth>olddepth)
{
scale=0;//通过深度对比,当该点被遮挡后 阴影系数为0,不被遮挡系数为1
}
return scale;
}
OK,我们直接把阴影系数给输出来,可看到如下图:大家可以看到第二张黑色和白色部分明显得可以区分了阴影。但是第一张有很多锯齿而第二张的阴影效果基本形成,为什么会形成这个现象呢?
原来在生成深度图后,深度渲染的区域过大,导致每个深度图像素可能表示了几个点的深度,那么久导致了不精确,从VS到PS阶段,中间的很多点的深度数值会采用插值来产生,于是产生了黑白交替的轮廓,正常情况下有两种解决方案,其一是如上代码所述在把顶点空间转换到View空间后 对其进行法线方向的一点点偏移来抗锯齿执行如下:
+float4(o.normal.xyz*0.07f,0)
偏移该坐标后,得到的阴影效果就能从图一过度到图二。另外的锯齿解决方法放到下一章去讲。
那么下一步我们之间把阴影系数和颜色值相乘 然后把颜色直接输出:
float4 color = tex2D(_MainTex, i.uv.xy);
return scale*color; //用颜色和阴影系数相乘,直接返回颜色,此时没有计算光照。
大家可以看到这张图用两个问题,一个是还没有光照,另一个是阴影太黑了显得突兀,那么我们下一步就是把光照计算加进去,传统光照模型halflamber和Phong模型,关于这两个公式大家可以百度下就知道了。对于阴影突兀的办法我们可以设定阴影系数为0.3和1,有遮挡的地方阴影系数设为0.3,这样显示就不会是全黑的了,会稍微淡化一点。但是在正常游戏中可能还会用BlendOne的方式去计算阴影叠加,那种方法这儿暂时不讨论了,这里讨论比较简单的实现方法。
float scale=1;//代码接上段
if(newdepth>olddepth)
{
scale=0.3f;//柔和阴影
}
//下面是光照计算了
float3 normal =normalize(i.normal);
float3 lightDir=normalize(-MainDir.xyz);//光照方向
float3 reflectDir=normalize(reflect(lightDir,normal));//反射方向
float3 viewDir= normalize(_WorldSpaceCameraPos.xyz-i.pos.xyz);//视线方向
//traditional光照模型
float3 diffuse=saturate(dot(normal,lightDir));
float3 halflambert=dot(normal,lightDir)*0.5+0.5;
float3 BlinnPhong=pow(max(0,dot(normal,normalize(lightDir+viewDir))), _Gross)*5;
/
float4 color = tex2D(_MainTex, i.uv.xy);
color.xyz = pow(color.xyz, 2.2f);//伽马矫正
float4 ambient = (normal.y*0.5+0.5f)*AmbientColor;
return color *_Color*scale*MainColor*float4(BlinnPhong + halflambert + ambient,1);//传统光照模型
OK,通过上面的代码就可以实现效果稍微能接受一点点的阴影了,在以前机器性能不好或者技术不好的情况下,这个阴影就已经用到上线的项目了。
如下图 第一张是经过光照计算 但是阴影系数设定为0和1,第二张吧阴影系数设为了0.3和1。
OK,本章就讲这么多内容,实现了最基本的阴影,但是大家可以看到阴影的锯齿非常严重,下一张我们将讲阴影的锯齿优化。