Unity3D的全局光照和阴影:下篇(unity3D中的球谐光照和SH球谐函数、unity实时阴影抗锯齿解决方案)

一、探针基于球谐函数的全局光照

球谐光照是基于预计算辐射度传输理论实现的一种实时渲染技术。预计算辐射度传输技术能够重现在区域面光源照射下的全局照明效果。这种技术通过在运行前对场景中光线的相互作用进行预计算,计算每个场景中每个物体表面点的光照信息,然后用球谐函数对这些预计算的光照信息数据进行编码,在运行时读取数据进行解码,重现光照效果。

球谐光照使用新的光照方程来代替传统的光照方程,并将这些新方程中的相关信息使用球谐基函数投影到频域,存储成一系列的系数。在运行渲染过程中,利用这些预先存储的系数信息对原始的光照方程进行还原,并对待渲染的场景进行着色计算。这个计算过程是对无限积分进行有限近似的过程。

简单说,球谐光照的基本步骤就是把真实环境中连续的光照方程离散化,得到离散的光照方程,然后对这些离散的光照方程进行分解,分解后进行球谐变换,得到球谐系数,在运行时根据球谐系数重新还原光照方程。

关于球谐函数的数学公式推导,后面有空会补上这块内容。

总结下球谐光照操作过程:

为了求得原始光照计算函数light(s)的积分结果,选用若干组基函数Bi(s)和这些基函数对应的加权系数ci,将个组基函数和加权系数相乘,然后累加乘积值,这个累计值就近似等于原始函数light(s)z的积分结果。Bi(s)就是球谐函数,而ci则未知待求得。采样若干个自变量si,得到每一个自变量si对应的原始计算函数和基函数乘积,再累加可得到ci。最后便可以求得原始光照计算函数积分近似计算结果。

二、Unity3D中的球谐光照

Unity3D内置shader库中定义了一些工具函数,可以计算球谐光照。使用了3阶的伴随勒让得多多项式作为基涵,即l的最大取值为2。直角坐标系下3阶球谐函数的系数如下所示,共9个,其中r=根号下x方+y方+z方。


最终的光照函数就是上表中每一项Y和基函数系数c的叠加,又因为球谐光照所讨论的球面空间是在一个单位球空间中的,所以上表的r项的值为1,故而在考察的单位球球面上的位置点(x,y,z)组成的向量也是一个单位化的向量。

在重建光照过程中,因为使用的光照颜色是RGB颜色,每一个分量都需要和这9个系数进行运算操作,并且因为每个光颜色分量使用的波长不同,所以对应的c也不同。因此需要27个数字去做这些操作,这27个数字存储在7个float4变量中,这7个float4变量再文件中定义,由引擎在程序运行时传递进来。

2.1 球谐光照要用到的系数

 // SH lighting environment
    half4 unity_SHAr;
    half4 unity_SHAg;
    half4 unity_SHAb;
    half4 unity_SHBr;
    half4 unity_SHBg;
    half4 unity_SHBb;
    half4 unity_SHC;

上述代码中,unity_SHAr的3个分量对应于表中l=1的各项Y的红色光分量对应的c的乘积,最后一个分量对应于l=0时Y常数值与对应的c的乘积,unity_SHAg对应于绿光分量;untiy_SHAb对应于蓝光分量。

2.2 SHEvalLinearL0L1函数

// normal should be normalized, w=1.0
//传递进来的normal参数是一个单位化的half4类型的向量,其w分量为=1.0
//即单位球面的一点,又可视为从单位球圆心到这一点的连线的向量
half3 SHEvalLinearL0L1 (half4 normal)
{
    half3 x;
 
    // Linear (L1) + constant (L0) polynomial terms
	//因为normal.w的值为1,所以unity_SHAr最后一项与normal.w的乘积和传进来的球面上的点的位置没有关系,这一项是重建光照效果时,l=0时的多项式Y(m,l)的值
    x.r = dot(unity_SHAr,normal);
    x.g = dot(unity_SHAg,normal);
    x.b = dot(unity_SHAb,normal);
 
    return x;
}

实质上就是把每个待计算的球面上的某个点传递给分别执行表中l=1时的各项多项式进行计算操作,得到各项的乘积之后,再叠加起来返回。这也是在此使用''各分量先各自相乘最后相加“的点积函数的原因。

2.3 SHEvalLinearL2函数

函数功能是计算l=2时的各个对应值。

// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
    half3 x1, x2;
    // 4 of the quadratic (L2) polynomials
    half4 vB = normal.xyzz * normal.yzzx;
    x1.r = dot(unity_SHBr,vB);
    x1.g = dot(unity_SHBg,vB);
    x1.b = dot(unity_SHBb,vB);
 
    // Final (5th) quadratic (L2) polynomial
    half vC = normal.x*normal.x - normal.y*normal.y;
    x2 = unity_SHC.rgb * vC;
 
    return x1 + x2;
}

上面代码中half4 vB=normal.xyzz*normal.yzzx,就是构造表中l=2时左数前4项的多项式中的xy、yz、z方、xz。变量half vC=normal.x*normal.x-normal.y*normal.y就对应表中l=2右数第一项中的x方-y方。有了计算l=0,1,2时的光照结果的函数,便可以把他们组合起来,提供3阶的球谐光照计算,函数ShadeSH9就用于实现该功能。

2.4 ShaderSH9 函数

// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
    // Linear + constant polynomial terms
	//计算l=0,1时的光照结果
    half3 res = SHEvalLinearL0L1 (normal);
 
    // Quadratic polynomials
	//计算l=2时的光照结果并累加上一个计算结果
    res += SHEvalLinearL2 (normal);
	//光照结果是在线性空间中定义的,如果启用了伽马空间则要换算
#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif
 
    return res;
}

函数ShaderSH9就是把SHEvalLinearL0L1和SHEvalLinearL2两个函数的计算结果叠加起来,然后根据伽马空间的宏是否启用决定是否要把计算结果从线性颜色空间中,变换到伽马空间中。

ShadeSH3Order函数则是只保留l=2时的重建光照计算部分的ShaderSH9函数,在本版本中该函数不在使用,如下。

2.5 ShadeSH3Order函数

// OBSOLETE: for backwards compatibility with 5.0
half3 ShadeSH3Order(half4 normal)
{
    // Quadratic polynomials
    half3 res = SHEvalLinearL2 (normal);
 
#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif
 
    return res;
}

2.6 ShaderSH12Order函数

// normal should be normalized, w=1.0
half3 ShadeSH12Order (half4 normal)
{
    // Linear + constant polynomial terms
    half3 res = SHEvalLinearL0L1 (normal);
 
#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif
 
    return res;
}

2.7 unity_ProbeVolumeSH变量

#if UNITY_LIGHT_PROBE_PROXY_VOLUME
    UNITY_DECLARE_TEX3D_FLOAT(unity_ProbeVolumeSH);

2.8 宏UNITY_DECLARE_TEX3D_FLOAT

#if defined(UNITY_COMPILER_HLSLCC) && !defined(SHADER_API_GLCORE) // GL Core doesn't have the _half mangling, the rest of them do.
    #define UNITY_DECLARE_TEX3D_FLOAT(tex) Texture3D_float tex; SamplerState sampler##tex
    #define UNITY_DECLARE_TEX3D_HALF(tex) Texture3D_half tex; SamplerState sampler##tex
#else
    #define UNITY_DECLARE_TEX3D_FLOAT(tex) Texture3D tex; SamplerState sampler##tex
    #define UNITY_DECLARE_TEX3D_HALF(tex) Texture3D tex; SamplerState sampler##tex
#endif

如果着色器编译器使用HLSLcc编译器,并且不是把Cg/HLSL代码转换为GLSL代码, UNITY_DECLARE_TEX3D_FLOAT(unity_ProbeVolumeSH)的含义是声明一个Texture3D_float类型的纹理变量unity_ProbeVolumeSH,同时声明一个SamplerState类型的采样器状态变量,其名字为samplerunity_ProbeVolumeSH;如果使用其他着色器编译器,无论是FLOAT还是HALF就统一不加类型大小后缀,unity_ProbeVolumeSH统一声明成Texture3D类型。

Cg/HLSL代码中声明诶Texture3D类型的纹理是一种异于一般二维的称为立体纹理的纹理。立体纹理是传统二维纹理在逻辑上的扩展。二维纹理是一张简单的位图片,用来给三维模型提供表面颜色值;而一个三维纹理可以认为是由多张二维纹理堆叠而成的,用于描述三维空间数据的图片。立体纹理通过三维纹理坐标进行访问。立体纹理可以有一系列细节级别,每一级都较上一级缩小1/2。

Unity还提供了对立体纹理进行采样操作的宏,根据使用不同着色器编译和目标平台这个采样操作宏对应着不同的实现。

2.9 UNITY_SAMPLE_TEX3D_SAMPLER宏版本 1

//这个宏使用了Direct3D 11版本的语法
 #define UNITY_SAMPLE_TEX3D_SAMPLER(tex,samplertex,coord) tex.Sample (sampler##samplertex,coord)

2.10 UNITY_SAMPLE_TEX3D_SAMPLER宏版本 2

  //这个宏使用了Direct 3D 9版本的语法
#define UNITY_SAMPLE_TEX3D_SAMPLER(tex,samplertex,coord) tex3D (tex,coord)

2.11 和光照探针代理体相关的着色器变量

CBUFFER_START(UnityProbeVolume)
        // x = Disabled(0)/Enabled(1)
        // y = Computation are done in global space(0) or local space(1)
        // z = Texel size on U texture coordinate
        float4 unity_ProbeVolumeParams;
 
        float4x4 unity_ProbeVolumeWorldToObject;
        float3 unity_ProbeVolumeSizeInv;
        float3 unity_ProbeVolumeMin;
    CBUFFER_END
#endif

unity_ProbeVolumeParams变量的4个分量函数:x分量为1时表示启用本光照体代理体,为0时表示不启用;y分量为0时表示在世界空间中进行计算,为1时表示在代理体的模型空间中计算;z分量则表示体积纹理的宽度方向上纹素的大小。假如该方向上纹素的个数为64,则该值为1/64。

unity_ProbeVolumeWorldToObject定义了从世界空间转换到光探针代理体局部空间的变换矩阵。unity_ProbeVolumeSizeInv是该光探针代理体的长宽高的倒数。unity_ProbeVolumeMin是该光探针代理体左下角的x、y、z坐标。

2.12 SHEvalLinearL0L1_SampleProbeVolume函数

#if UNITY_LIGHT_PROBE_PROXY_VOLUME
 
// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1_SampleProbeVolume (half4 normal, float3 worldPos)
{
	//根据当前空间的取值,判断是在局部空间还是在全局空间中处理光照探针
    const float transformToLocal = unity_ProbeVolumeParams.y;
	//取得纹理U坐标方向上的纹素的大小,假如U方向的纹素个数为64,则纹素大小为0.015625
    const float texelSizeX = unity_ProbeVolumeParams.z;
	//吧球谐函数的3阶系数以及探针遮盖信息打包到一个纹素中
    //The SH coefficients textures and probe occlusion are packed into 1 atlas.
    //-------------------------
    //| ShR | ShG | ShB | Occ |
    //-------------------------
 
	//如果在局部空间中处理,就要把待处理点乘一个从世界坐标到局部空间上的矩阵转换回去
    float3 position = (transformToLocal == 1.0f) ? mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz : worldPos;
	//根据当前传递进来的位置点的坐标,求得当前位置点相对于光探针代理体的最左下角位置点的偏移,然后各种除以光探针代理体的长宽高,得到归一化的纹理映射坐标
    float3 texCoord = (position - unity_ProbeVolumeMin.xyz) * unity_ProbeVolumeSizeInv.xyz;
    texCoord.x = texCoord.x * 0.25f;
 
    // We need to compute proper X coordinate to sample.
    // Clamp the coordinate otherwize we'll have leaking between RGB coefficients
	
    float texCoordX = clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX);
 
    // sampler state comes from SHr (all SH textures share the same sampler)
	//依次取得对红、绿、蓝颜色分量的球谐函数系数
    texCoord.x = texCoordX;
    half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);
 
    texCoord.x = texCoordX + 0.25f;
    half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);
 
    texCoord.x = texCoordX + 0.5f;
    half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);
 
    // Linear + constant polynomial terms
    half3 x1;
    x1.r = dot(SHAr, normal);
    x1.g = dot(SHAg, normal);
    x1.b = dot(SHAb, normal);
 
    return x1;
}
#endif

在预计算阶段,光探针把空间中某一点的球谐函数系数编码进一个立体纹理中,在运行时再从立体纹理中采样得到系数,然后根据该点的法线值重建出光照效果。
SHEvalLinearL0L1_SampleProbeVolume函数就是实现该功能。该函数从纹理中采样得到的颜色信息,其数值就是当l=1时和l=0时的各项Y与对应的C的乘积。

SHEvalLinearL0L1_SampleProbeVolume函数把对纹理进行采样时使用的纹理映射坐标按R、G、B分段压缩到长度为0.25的一个段中,在对颜色取样时就需要重新计算出各R、G、B分量对应的纹理映射坐标,上面代码已经注释了如何计算,流程如下图:


三、引擎中的渲染阴影的功能

Unity3D支持一个游戏对象所表征的物体投射阴影到其他在它附近的物体表面上,或者它自身的表面上。在画面上显示阴影效果,可以有效提升场景的纵深感和真实感。

3.1 在光源空间中确定产生阴影的区域

确定产生阴影的区域方法是把光源想象成一个摄像机(暂称光源相机),光源相机的位置就是光源位置,光源相机的朝向就是光线发出的方向。在要绘制的场景中的物体前,用光源相机对场景执行一次取景操作。这个取景操作不做任何绘制处理,仅把在光源相机所在角度所有可视的片元深度信息存储在一个帧缓冲区(frame buffer)中,称为阴影贴图(shadowmap)。在真正渲染时,把每一个待输出的片元再次放到光源相机的角度下去计算深度值。如果这次计算的深度值比深度图中国的深度值要离光源相机远,就表示它落在某个阴影区域了。Unity采用的算法原理其实就是这一种。

3.2 在屏幕空间中确定产生阴影区域

除了在光源空间中就能确定阴影覆盖范围外,还可以基于屏幕空间确定产生阴影的区域,有下面步骤:

1. 从当前摄像机位置处进行取景操作,这个取景操作不做任何绘制处理,仅将当前摄像机所在角度下的所有可视片元的深度信息存储在一个深度纹理(第一纹理)中,如果是延迟渲染,深度纹理原本就已经存在了,就在G-Buffer中。如果是前向渲染,就需要做一遍取景操作生成此深度纹理。

2. 利用光源相机对场景执行一次取景操作。这次取景操作也是不做任何绘制处理,仅将在光源相机所在的角度下的所有可视片元的深度信息存储在深度纹理中。

3. 在屏幕空间中,当前摄像机做一次阴影收集计算,这个搜集过程的流程是:当前最终要绘制的每一个片元,根据它在深度纹理中对应的深度值D1,反求得该片元在世界空间中的坐标。接着把这个坐标从世界空间转换到光源空间中得到阴影坐标,用该阴影坐标在深度纹理中进行采样,得到深度值D2,如果D1>D2,表示本片元光源无法照射到,所以是在阴影内。把这些“某片元是否在阴影内,如果在的话该处的阴影有多浓”等信息存储进一个纹理中,便形成最终的屏幕空间阴影贴图。

最后再渲染场景物体计算其阴影时,只需要在片元着色器中按照当前处理的片元在屏幕空间中的位置,对第三步生成的屏幕空间阴影进行贴图采样即可。

3.3 如何启用阴影

在unity中产生阴影需要游戏对象的多个组件共同配合指定。首先在每一个可渲染gameobject上的MeshRenderer组件上有没有Cast Shadow和Receive Shadow两个属性。除此之外,对每一个光源上的Light组件,也需要指定它的相关阴影属性去控制光源投射阴影,如下表:


3.4 透视走样和层叠式阴影贴图

当使用阴影贴图时,通常会有透视走样(perspective aliasing)的问题,透视走样是指阴影越靠近摄像机,其边缘的锯齿化(走样)现象就越严重,这是因为阴影贴图的分辨率是固定的,而透视投影的效果是近大远小,同样大小的一个阴影所对应的阴影贴图中纹素的大小也是固定的。而在渲染时,当阴影越靠近摄像机,就越容易出现多个片元从阴影贴图中的同一个纹素进行采样的情况,这几个片元得到的同一个阴影值,因而产生锯齿边。

使用更高分辨率的阴影贴图可以降低边缘锯齿,但是这样会在渲染时占用更多的内存和带宽。层叠式阴影贴图(cascaded shadow map,CSM)可以较好兼顾精度和性能的问题。这种技术与使用单个阴影贴图的区别主要在于:将摄像机的视截体(view frustum)按一定比例分成若干层级(cascade),每个层级对应一个子视截体,每一层级单独计算相关的阴影贴图,这样在渲染大场景的阴影时,就可以避免使用单张阴影贴图的各种缺点。Unity3D引擎使用层叠式阴影贴图技术实现了大部分的阴影。

层叠式阴影贴图原理如下图所示:


要使用层叠式阴影贴图技术,首先要为每一个层次对应的子视截体构建一个投影矩阵。在构建投影矩阵时,必须要使得生成的阴影贴图中,不在当前视野范围内的无关区域尽可能少(或者说生成阴影贴图的有效分辨率要尽可能高)。也就是说,要计算出一个和当前层级所对应的子视截体尽可能重合的投影矩阵。这个投影矩阵一般使用正交投影矩阵,该投影矩阵由一个能包住子视截体的,且与光源空间坐标系轴对齐的轴对齐包围盒所对应生成:


因为在渲染时,摄像机的位置朝向等属性会即时改变,所以它对应的视截体,以及每个层级对应的子视截体也会不断变换,子视截体的轴对齐包围盒也要跟着对应变化。这样可能导致出现先后两帧中轴对齐包围盒发生突变,进而导致生成的阴影贴图的有效分辨率可能在这连线的两帧中也发生突变,产生阴影抖动(flickering)问题。解决这个问题的方法之一是把使用轴对齐包围盒改为使用包围球,因为包围球随着子视截体的变化而发生大小变化的程度相对轴对齐包围盒而言要小得多,如下图所示:


更好的解决方案是一种称为“渐进式变换视截体”的方法,该方法在改善阴影抖动问题的同时,还能在一定程度上改善因为光栅化带来的抖动的问题。

Unity引擎在Quality Settings对话框中提供了和层叠式阴影设置相关的选项,如下图所示,可以指定把视截体分为多少个层级,每个层级各占的比例是多少。


版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/105716007

最新文章