结合在Unity中的具体Shader实现光照模型。同时加入了Unity阴影的实现原理,ShadowCascade的实现原理。

 

1. 光照模型

1.1 Lambert漫反射:

公式:入射光线的颜色和强度 * 材质的漫反射系数 * max(0,dot(法线方向,光照方向))

缺点:在光照无法照到的地方,模型外观是全黑的,没有任何明暗变化。在背光面,max(0,法线向量和光照方向的点乘结果),映射到同一个值,即0处。

fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 lightDirction = normalize(_WorldSpaceLightPos0.xyz);
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0,dot(worldNormal, lightDirction));
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}

1.2 Half-Lambert漫反射:

公式:入射光线的颜色和强度 * 材质的漫反射系数 * (dot(法线方向, 光照方向)* 0.5 + 0.5)

解决了上面的问题,在背光面也可以有明暗变化。把法线向量和光照方向的点乘结果范围从[-1, 1]映射到[0,1]范围,使不同的点乘结果映射到不同的值上。

fixed4 frag (v2f i) : SV_Target
{
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDirection = normalize(_WorldSpaceLightPos0.xyz);
fixed halfLambert = dot(worldNormal, worldLightDirection) * 0.5 + 0.5;
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * halfLambert;
fixed3 color = ambient + diffuse;
return fixed4(color, 1.0);
}

1.3 Gouraud高洛德着色:

逐顶点光照

 

1.4 Phong高光反射:

逐像素光照

公式:入射光线的颜色和强度 * 材质的高光反射系数 * pow(max(0,dot(视角方向 ,反射方向)),gloss高光区域大小)

//计算高光反射
//获得反射方向,由于Cg的reflect函数的入射方向要求是由光源方向指向与反射光线的交点处,
//所以需要对worldLightDirection取反再传给reflect函数
//_WorldSpaceLightPos0表示世界空间中光源的位置,也就是由原点指向光源位置
//reflect(i , n) i,入射方向;n,法线方向
fixed3 reflectDir = normalize(reflect(-worldLightDirection, worldNormal));
//mul(unity_ObjectToWorld, v.pos)把顶点位置从模型空间转换到世界空间中,再和_WorldSpaceCameraPos相减,即可得到世界空间下的视角方向
//fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.pos).xyz);
fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0,dot(reflectDir, viewDir)), _Gloss);

1.5 Blinn-Phong高光反射计算:

引入了一个新的矢量h,它是通过对视角方向和光照方向相加后再归一化得到的。

公式:入射光线的颜色和强度 * 材质的高光反射系数 *

pow(max(0,dot(法线方向 ,h矢量方向)),gloss高光区域大小)

fixed4 frag(v2f i) : SV_Target {
// Get ambient term
fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;

fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);

// Compute diffuse term
fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir));

// Get the view direction in world space
fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// Get the half direction in world space
fixed3 halfDir = normalize(worldLightDir + viewDir);
// Compute specular term
fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss);

return fixed4(ambient + diffuse + specular, 1.0);
}

2. 阴影实现原理

首先把摄像机的位置放在与光源重合的位置,那么场景中该光源的阴影区域就是那些摄像机看不到的地方。在前向渲染中,如果场景中最重要的平行光开启了阴影,Unity就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,记录从光源出发得到从该光源处观察到的深度纹理。

Unity选择使用了一个额外的Pass来专门更新光源的阴影映射纹理,这个Pass就是LightMode标签被设置为ShadowCaster的Pass。这个Pass渲染目标不是帧缓存,而是阴影映射纹理(或深度纹理)。

 

2.1 传统的阴影映射纹理技术

在正常渲染的Pass中把顶点位置从模型空间转换到光源空间下,以得到它在光源空间中的三维位置信息。然后,使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置的深度信息。如果该顶点的深度值(通常由z分量得到)大于阴影映射纹理中该位置的深度值,那么说明光源无法照到,该点在阴影中。

 

2.2 屏幕空间的阴影映射技术

在Unity5中,Unity采用不同于传统的阴影映射纹理技术,即屏幕空间的阴影映射技术(Screenspace Shadow map)。需要注意,屏幕空间的阴影映射需要显卡支持MRT(Multiple Render Target,多重渲染目标,GPU允许我们把场景同时渲染到多个渲染目标纹理中),需要OpenGL ES 3.0,Metal支持。

  1. 当使用了屏幕空间的阴影映射技术时,Unity首先会调用LightMode为ShadowCaster的Pass来得到光源的阴影映射纹理以及摄像机的深度纹理。
  2. 然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影映射纹理。
  3. 把每一个像素根据它在摄像机的深度纹理值得到在世界空间的坐标,再把它的坐标从世界空间转换到光源空间中,和光源的ShadowMap中的深度值对比,如果大于ShadowMap中的深度值,那么说明光源无法照到,处于该光源的阴影中。这样,屏幕空间的阴影映射纹理就包含了屏幕空间中所有有阴影的区域。

2.2.1 物体接收来自其他物体的阴影

如果我们想要一个物体接收来自其他物体的阴影,只需要在Shader中把位置从模型空间转换到屏幕空间中,然后使用这个位置对屏幕空间的阴影映射纹理进行采样,把采样结果和最后的光照结果相乘来产生阴影结果。

 

2.2.2 物体向其他物体投射阴影

如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时,可以得到该物体的相关信息。

 

3. 实时阴影

3.1 ShadowCascade实现原理

在实时渲染中,存在这样的一个渲染优化手段,也就是“LOD”,level of details,也就是细节等级的意思,总体来说也就是根据我们观察相机距离被观察物体的距离来决定优化被观察物体的渲染细节。距离相机越近,使用更精致的模型。

CascadeShadowMap其实就是来源于LOD的思想,总体的原理:将我们相机视椎体根据与相机空间的原点的远近距离来划分为near,middle,far几个层级(也就是所谓的层级Cascade),并将这几个部分的物体渲染到相应的ShadowMap。

CascasdeShadowMap主要是用于主光源平行光的阴影投射。

Shadow Cascade,就是远处的阴影用分辨率比较小的贴图,近处的阴影用分辨率比较大的贴图,提升了近处阴影的质量,但增加了性能开销。

3D图形学(3):光照模型实现、阴影实现原理及ShadowCascade的实现原理_抗锯齿

 

CascadeShadowMap(层级阴影贴图)的计算过程:

因为平行光的投影变换矩阵是正交投影矩阵(OrthoProjMatrix)

平行光渲染ShadowMap的变换公式:objectPos*WorldMatrix*LightViewMatrix*OrthoProjMatrix

objectPos(物体的顶点模型空间位置),WorldMatrix(世界变换矩阵),LightViewMatrix(光源相机空间的变换矩阵)都是已经知道的。

那么下面我们求的就是观察相机的视椎体的Near,Middle,Far三个层级在平行光源的相机空间形成的正交投影矩阵OrthoProjMatrix(注意理解平行光的相机空间)。

1. 根据在相机空间下坐标点距离原点的远近,划分视截体为三个层级,其中箭头为平行光方向。如下所示:

3D图形学(3):光照模型实现、阴影实现原理及ShadowCascade的实现原理_抗锯齿_02

2. 计算观察相机的视椎体三个层级(Near,Middle,Far),在平行光源空间形成的AABB包围盒,从而得到相应的正交投影变换矩阵。

3D图形学(3):光照模型实现、阴影实现原理及ShadowCascade的实现原理_ShadowCascade_03

注意:

这里有三个层级,理论上有对应的三张ShadowMap,但是合成了一张ShadowMap,也就是说如果ShadowMap的分辨率是1024X1024,则合成的ShadowMap分辨率为3072X1024。

3.2 软阴影

软阴影是通过对阴影图进行多次采样实现的

因为多个片段经常会对应同一个阴影纹理像素,单次采样会产生严重的锯齿问题

另外软阴影还可以产生比较柔和的边界,看起来比较自然

2×2线性插值

3D图形学(3):光照模型实现、阴影实现原理及ShadowCascade的实现原理_光照模型_04

如上图,实线分割的四个区域表示阴影纹理中的四个像素,ABCD为其中心点

当一个片段映射到ABCD虚线包含的区域时,这四个像素都应该参与阴影计算,然后通过线性插值得出最终结果

更大的采样范围

2x2线性插值虽然可以产生平滑过渡的阴影,但没有完全解决锯齿纹理,效果并不理想

这是因为使用的阴影图本身就是栅格化的

处理这个问题的办法是采用更大的采样范围比如3x3、4x4、5x5

 

软阴影算法的步骤如下:

1、在阴影贴图中搜索遮挡物的邻域。

2、投射阴影的样本记录为遮挡物。

3、将遮挡物中的中心样本的平均深度差用作第二个pass中的采样半径,并且在该半径内取多个标准PCF样本并取平均值。

4、为了隐藏有限数量的样本失真,采样图案以从屏幕位置产生的伪随机角度进行旋转。

 

4. 抗锯齿

       抗锯齿(英语:anti-aliasing,简称AA),也译为边缘柔化、消除混叠、抗图像折叠有损,反走样等。它是一种消除显示器输出的画面中图物边缘出现凹凸锯齿的技术,那些凹凸的锯齿通常因为高分辨率的信号以低分辨率表示或无法准确运算出3D图形坐标定位时所导致的图形混叠(aliasing)而产生的,抗锯齿技术能有效地解决这些问题。

4.1 超级采样抗锯齿(SSAA)

       超级采样抗锯齿(Super-Sampling Anti-Aliasing,简称SSAA)是比较早期的抗锯齿方法,比较消耗资源,但简单直接。这种抗锯齿方法先把图像映射到缓存并把它放大,再用超级采样把放大后的图像像素进行采样,一般选取2个或4个邻近像素,把这些采样混合起来后,生成的最终像素,令每个像素拥有邻近像素的特征,像素与像素之间的过渡色彩,就变得近似,令图形的边缘色彩过渡趋于平滑。再把最终像素还原回原来大小的图像,并保存到帧缓存也就是显存中,替代原图像存储起来,最后输出到显示器,显示出一帧画面。这样就等于把一幅模糊的大图,通过细腻化后再缩小成清晰的小图。如果每帧都进行抗锯齿处理,游戏或视频中的所有画面都带有抗锯齿效果。 超级采样抗锯齿中使用的采样法一般有两种:

  • OGSS,顺序栅格超级采样(Ordered Grid Super-Sampling,简称OGSS),采样时选取2个邻近像素。
  • RGSS,旋转栅格超级采样(Rotated Grid Super-Sampling,简称RGSS),采样时选取4个邻近像素。

4.2 多重采样抗锯齿(MSAA)

多重采样抗锯齿(Multi Sampling Anti-Aliasing,简称MSAA),是一种特殊的超级采样抗锯齿(SSAA)。MSAA首先来自于OpenGL。具体是MSAA只对Z缓存(Z-Buffer)和模板缓存(Stencil Buffer)中的数据进行超级采样抗锯齿的处理。可以简单理解为只对多边形的边缘进行抗锯齿处理。这样的话,相比SSAA对画面中所有数据进行处理,MSAA对资源的消耗需求大大减弱,不过在画质上可能稍有不如SSAA。

4.3 覆盖采样抗锯齿(CSAA)

覆盖采样抗锯齿(Coverage Sampling Anti-Aliasing,简称CSAA)是NVIDIA在G80及其衍生产品首次推向实用化的AA技术,也是目前NVIDIA GeForce 8/9/G200系列独享的AA技术。CSAA就是在MSAA基础上更进一步的节省显存使用量及带宽,简单说CSAA就是将边缘多边形里需要取样的子像素坐标覆盖掉,把原像素坐标强制安置在硬件和驱动程序预先算好的坐标中。这就好比取样标准统一的MSAA,能够最高效率的执行边缘取样,效能提升非常的显著。比方说16xCSAA取样性能下降幅度仅比4xMSAA略高一点,处理效果却几乎和8xMSAA一样。8xCSAA有着4xMSAA的处理效果,性能消耗却和2xMSAA相同。

4.4 高分辨率抗锯齿(HRAA)

高分辨率抗锯齿方法(High Resolution Anti-Aliasing,简称HRAA),也称Quincunx方法,也出自NVIDIA公司。“Quincunx”意思是5个物体的排列方式,其中4个在正方形角上,第五个在正方形中心,也就是梅花形,很像六边模型上的五点图案模式。此方法中,采样模式是五点梅花状,其中四个样本在像素单元的角上,最后一个在中心。

4.5 可编程过滤抗锯齿(CFAA)

可编程过滤抗锯齿(Custom Filter Anti-Aliasing,简称CFAA)技术起源于AMD-ATI的R600家庭。简单地说CFAA就是扩大取样面积的MSAA,比方说之前的MSAA是严格选取物体边缘像素进行缩放的,而CFAA则可以通过驱动和谐灵活地选择对影响锯齿效果较大的像素进行缩放,以较少的性能牺牲换取平滑效果。显卡资源占用也比较小。

4.6 形态抗锯齿(MLAA)

形态抗锯齿(Morphological Anti-Aliasing,简称MLAA),是AMD推出的完全基于CPU处理的抗锯齿解决方案。与MSAA不同, MLAA将跨越边缘像素的前景和背景色进行混合,用第2种颜色来填充该像素,从而更有效地改进图像边缘的变现效果。

4.7 快速近似抗锯齿(FXAA)常用

快速近似抗锯齿(Fast Approximate Anti-Aliasing,简称FXAA) ,是传统MSAA(多重采样抗锯齿)效果的一种高性能近似。它是一种单程像素着色器,和MLAA一样运行于目标游戏渲染管线的后期处理阶段,但不像后者那样使用DirectCompute,而只是单纯的后期处理着色器,不依赖于任何GPU计算API。正因为如此,FXAA技术对显卡没有特殊要求,完全兼容NVIDIA、AMD的不同显卡(MLAA仅支持A卡)和DirectX 9.0、DirectX 10、DirectX 11。

4.8 时间性抗锯齿(TXAA)

时间性抗锯齿(Temporal Anti-Aliasing,简称TXAA),将 MSAA、时间滤波以及后期处理相结合,用于呈现更高的视觉保真度。与CG电影中所采用的技术类似,TXAA集MSAA的强大功能与复杂的解析滤镜于一身,可呈现出更加平滑的图像效果。此外,TXAA还能够对帧之间的整个场景进行抖动采样,以减少闪烁情形,闪烁情形在技术上又称作时间性锯齿。目前,TXAA有两种模式:TXAA 2X和TXAA 4X。TXAA 2X可提供堪比8X MSAA的视觉保真度,然而所需性能却与2X MSAA相类似;TXAA 4X的图像保真度胜过8XMSAA,所需性能仅仅与4X MSAA相当。

4.9 多帧采样抗锯齿(MFAA)

多帧采样抗锯齿(Multi-Frame Sampled Anti-Aliasing,MFAA)是 NVIDIA公司根据MSAA改进出的一种抗锯齿技术。目前仅搭载 Maxwell 架构GPU的显卡才能使用。可以将MFAA理解为MSAA的优化版,能够在得到几乎相同效果的同时提升性能上的表现。MFAA与MSAA最大的差别就在于在同样开启4倍效果的时候MSAA是真正的针对每个边缘像素周围的4个像素进行采样,MFAA则是仅仅只是采用交错的方式采样边缘某个像素周围的两个像素。

5. 透明渲染

Alpha混合(Alpha Blending)方法。这个方法比较常见,其实就是按照Alpha混合向量的值来混合源像素和目标像素。当在屏幕上绘制某个物体时,与每个像素相关联的值有RGB颜色和Z缓冲深度值,以及另外一个成分alpha分量,这个alpha值也可以根据需要生成并存储,它描述的是给定像素的对象片段的不透明度的值。 alpha为1.0表示对象不透明,完全覆盖像素所在区域; 0.0表示像素完全透明。为了使对象透明,在现有场景的上方,以小于1的透明度进行绘制即可。每个像素将从渲染管线接收到一个RGBA结果,并将这个值和原始像素颜色相混合。

 

6. 透明排序

要将透明对象正确地渲染到场景中,通常需要对物体进行排序。下面分别介绍两种比较基本的透明排序方法(深度缓存和油画家算法)和两种高级别的透明排序算法(加权平均值算法和深度剥离)。

6.1 深度缓存(Z-Buffer)

Z-Buffer也称深度缓冲。在计算机图形学中,深度缓冲是在三维图形中处理图像深度坐标的过程,这个过程通常在硬件中完成,它也可以在软件中完成,它是可见性问题的一个解决方法。可见性问题是确定渲染场景中哪部分可见、哪部分不可见的问题。

Z-buffer的限制是每像素只存储一个对象。如果一些透明对象与同一个像素重叠,那么单独的Z-buffer就不能存储并且稍后再解析出所有可见对象的效果。这个问题是通过改变加速器架构来解决的,比如用A-buffer。A-buffer具有“深度像素(deep pixels)”,其可以在单个像素中存储一系列呈现在所有对象之后被解析为单个像素颜色的多个片段。但需注意,Z-buffer是市场的主流选择。

6.2 画家算法(Painter's Algorithm)

画家算法也称优先填充算法,效率虽然较低,但还是可以有效处理透明排序的问题。其基本思想是按照画家在绘制一幅画作时,首先绘制距离较远的场景,然后用绘制距离较近的场景覆盖较远的部分的思想。画家算法首先将场景中的多边形根据深度进行排序,然后按照顺序进行描绘。这种方法通常会将不可见的部分覆盖,这样就可以解决可见性问题。

6.3 加权平均值算法(Weighted Average)

使用简单的透明混合公式来实现无序透明渲染的算法,它通过扩展透明混合公式,来实现无序透明物件的渲染,从而得到一定程度上逼真的结果。

6.4 深度剥离算法(Depth Peeling)

深度剥离是一种对深度值进行排序的技术。它的原理比较直观,标准的深度检测使场景中的Z值最小的点输出到屏幕上,就是离我们最近的顶点。但还有离我们第二近的顶点,第三近的顶点存在。要想显示它们,可以用多遍渲染的方法。第一遍渲染时,按照正常方式处理,这样就得到了离我们最近的表面中的每个顶点的z值。在第二遍渲染时,把现在每个顶点的深度值和刚才的那个深度值进行比较,凡是小于等于第一遍得到的z值,把它们剥离,后面的过程依次类推即可。