级联阴影贴图(CSM)
作者:互联网
文章目录
级联阴影贴图(CSM)
摊牌
- 级联阴影贴图,如果按照OpegnGl的尿性给他起个别名,我大概率会给他Mutil shadow mapping(多重阴影贴图),这样他的意思就更显而易见了——使用多张阴影贴图实现阴影的一种技术
- 在阴影贴图那一篇中说,升级阴影贴图的算法就可以实现CSM,那么为什么要升级?怎么升级?
- 阴影贴图的缺点:阴影边缘的锯齿严重。原因是阴影贴图的分辨率低,在对阴影贴图采样时,多个不同的顶点对同一个像素采样,导致生成锯齿。为了解决这种问题,我们使用多张阴影贴图,离相机近的地方使用精细的阴影贴图,离相机远的地方使用粗糙的阴影贴图,这样不仅优化了阴影效果,还保证了渲染效率
- 因此,级联阴影的关键就是生成和使用不同精细度的阴影贴图
实现
- 主要说明一下生成不同精细度阴影贴图的过程
- 首先对摄像机坐标系下的视锥体分割,分割为不同深度的分段
- 在光源的视角下,生成每个视锥体分段的“包围盒”,这个包围盒是生成阴影贴图时正交投影的重要参考
摄像机空间中视锥体分段
- 分割视锥体时,采用PSSM的方法,Parallel-Split Shadow Maps,平行分片阴影贴图法
- 我们先从理论上分析影响锯齿产生的因素,然后再从减少锯齿产生的角度去拆分视锥体
- 上图中ds是单位像素的阴影贴图的边长
- dp是单位像素的阴影贴图投射在画面上的阴影大小
- dp/ds可以理解为阴影的锯齿误差
- 为了使整个画面的阴影看起来质量一致,不因到相机的距离而发生明显质量变化, 应该让dp/ds 是一个常数
- 由dp/dy = n/z,得出dp = ndy/z
- φ和θ分别表示曲面法线与屏幕和阴影贴图平面之间的角度。由dz/cosθ = dy/cosφ,得出dy = dzcosφ/cosθ
- 所以dp = ndzcosφ/zcosθ,dp/ds = ndzcosφ/zdscosθ
- 最后如果用对数拆分方案,可以推导出(详细过程省略)
Z i = n ( f / n ) i / N Z_i = n(f/n)^{i/N} Zi=n(f/n)i/N
其中Z是视锥体分段处的值,对应上图中的Ci,n是近平面距离,f是远平面距离,N是视锥体分割的个数,一般是1~4个 - 如果使用均匀拆分方案
Z i = n + ( f − n ) ( i / N ) Z_i = n + (f - n)(i / N) Zi=n+(f−n)(i/N) - 而PSSM方法是他们的折中,用系数λ来加权平均,
Z i = λ n ( f / n ) i / N + ( 1 − λ ) [ n + ( f − n ) ( i / N ) ] Z_i = λn(f/n)^{i/N} + (1 - λ)[ n + (f - n)(i / N) ] Zi=λn(f/n)i/N+(1−λ)[n+(f−n)(i/N)]
float nd = camera->near;
float fd = camera->far;
float lambda = 0.75;
float ratio = fd / nd;
frustums[0].near(nd);
for(int i = 1 ; i < 分段个数 ; i++)
{
float si = i / (float)分段个数;
float t_near = lambda * (nd * powf(ratio, si)) + (1 - lambda) * (nd + (fd - nd) * si);
float far = near * 1.005f;//略微增加重合,避免断裂
frustums[i].near(near);
frustums[i-1].far(far);
}
frustums[分段个数-1].far(fd);
光源空间中正交投影
- 光视锥体,如第一个图中倾斜的矩形,这一步就来计算他
- 光视锥体就像一个包围盒。但是,仅仅包裹分割后的摄像机视锥体是不够的。如果在光源和分割后的摄像机视锥体之间存在可以投射阴影的遮挡物,我们应该扩展光视锥体的大小,将遮挡物包裹在光视锥体中。比如,光源和分割后的摄像机视锥体之间有一只小鸟,如果不扩展光视锥体将小鸟渲染到阴影贴图中,我们就不能看到小鸟的影子
详细过程
- 先计算出摄像机视锥体分块在世界空间中的坐标
glm::vec3 viewPos = camera->Position;
glm::vec3 viewDir = camera->Front;
glm::vec3 up(0.0f, 1.0f, 0.0f);
glm::vec3 right = glm::cross(viewDir, up);
for(int i = 0 ; i < 分块个数 ; i++)
{
Frustum& frustum = m_frustums[i];
glm::vec3 fc = viewPos + viewDir * frustum.far();
glm::vec3 nc = viewPos + viewDir * frustum.near();
right = glm::normalize(right);
up = glm::normalize(glm::cross(right, viewDir));
// 计算当前分片的近平面和远平面宽高的一半
float near_height = tan(frustum.fov() / 2.0f) * frustum.near();
float near_width = near_height * frustum.ratio();
float far_height = tan(frustum.fov() / 2.0f) * frustum.far();
float far_width = far_height * frustum.ratio();
//记录视锥8个顶点
frustum.m_points[0] = nc - up * near_height - right * near_width;
frustum.m_points[1] = nc + up * near_height - right * near_width;
frustum.m_points[2] = nc + up * near_height + right * near_width;
frustum.m_points[3] = nc - up * near_height + right * near_width;
frustum.m_points[4] = fc - up * far_height - right * far_width;
frustum.m_points[5] = fc + up * far_height - right * far_width;
frustum.m_points[6] = fc + up * far_height + right * far_width;
frustum.m_points[7] = fc - up * far_height + right * far_width;
}
- 利用分块的各顶点坐标,计算摄像机视锥体分段的“包围盒”,从而计算出分块对应的正交投影矩阵。注意:这个计算出的投影矩阵是光空间中的投影矩阵,是用于渲染阴影贴图时使用的
glm::mat4 lightProjMat;
for(int i = 0 ; i < 分块个数 ; i++)
{
//1. 找出光空间中八个顶点的最大最小z值
Frustum& frustum = m_frustums[i];
glm::vec3 max(-1000.0f, -1000.0f, 0.0f);
glm::vec3 min(1000.0f, 1000.0f, 0.0f);
glm::vec4 transf = lightViewMat * glm::vec4(frustum.m_points[0], 1.0f);
min.z = transf.z;
max.z = transf.z;
for(int j = 1 ; j < 8 ; j++)
{
transf = lightViewMat * glm::vec4(frustum.m_points[j], 1.0f);
if(transf.z > max.z) { max.z = transf.z; }
if(transf.z < min.z) { min.z = transf.z; }
}
//1.1 扩展光视锥体的大小,使其包括所有的遮挡物
for(int j=0; j<场景中物体包围球个数; j++)
{
transf = lightViewMat * vec4f(objBSphereCenter[j], 1.0f);
if(transf.z + objBSphereRadius[j] > max.z)
{
max.z = transf.z + objBSphereRadius[j];
}
}
//2. 设光空间的正交投影矩阵为ortho,他的x,y∈[-1,1],z∈[-tmax.z,-tmin.z]
//使用x,y∈[-1,1],是因为每个分块的投影矩阵都可以使用单位x,y缩放平移后获得
//使用z∈[-tmax.z,-tmin.z],是因为摄像机空间指向负Z方向,而glm::ortho传入的是近平面和远平面指向正Z方向
glm::mat4 ortho = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, -tmax.z, -tmin.z);
//2.1 在光空间中,找出视锥体切片各顶点的x、y的标准设备坐标范围。因为我们设的投影矩阵的x、y都是标准设备坐标,我们需要求出x、y的变化范围,以便对ortho进行缩放平移
glm::mat4 lightVP = ortho * lightViewMat;
for(int j = 0 ; j < 8 ; j++)
{
transf = lightVP * glm::vec4(frustum.m_points[j], 1.0f);
transf.x /= transf.w;
transf.y /= transf.w;
if(transf.x > max.x) { max.x = transf.x; }
if(transf.x < min.x) { min.x = transf.x; }
if(transf.y > max.y) { max.y = transf.y; }
if(transf.y < min.y) { min.y = transf.y; }
}
//2.2 根据正交投影矩阵的公式,设置缩放平移量(计算过程在后面)
glm::vec2 scale(2.0f / (max.x - min.x), 2.0f / (max.y - min.y));
glm::vec2 offset(-0.5f * (max.x + min.x) * scale.x, -0.5f * (max.y + min.y) * scale.y);
glm::mat4 crop = glm::mat4(1.0);
//2.3 设置缩放平移的变换矩阵
crop[0][0] = scale.x;
crop[1][1] = scale.y;
crop[0][3] = offset.x;
crop[1][3] = offset.y;
crop = glm::transpose(crop);//注意glm按列储存,实际矩阵要转置
//2.4 计算出光空间中的正交投影矩阵
lightProjMat = crop * ortho;
//保存光空间的投影矩阵
projection_matrices[i] = lightProjMat;
//保存世界坐标到光空间变换的矩阵
crop_matrices[i] = lightProjMat * lightViewMat;
}
- 正交投影矩阵公式为:
[ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 0 1 ] \left[ \begin{matrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{matrix} \right] ⎣⎢⎢⎡r−l20000t−b20000f−n−20−r−lr+l−t−bt+b−f−nf+n1⎦⎥⎥⎤ - 根据已有条件,设缩放平移的变换矩阵为:
[ S x 0 0 O x 0 S y 0 O y 0 0 1 0 0 0 0 1 ] \left[ \begin{matrix} S_x & 0 & 0 & O_x \\ 0 & S_y & 0 & O_y \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{matrix} \right] ⎣⎢⎢⎡Sx0000Sy000010OxOy01⎦⎥⎥⎤ - 因此:
S x = 2 r − l = 2.0 m a x . x − m i n . x S_x = \frac{2}{r-l} = \frac{2.0}{max.x - min.x} Sx=r−l2=max.x−min.x2.0
S y = 2 t − b = 2.0 m a x . y − m i n . y S_y = \frac{2}{t-b} = \frac{2.0}{max.y - min.y} Sy=t−b2=max.y−min.y2.0
由 r − l = 2 S x , 得 : O x = − r + l 2 S x = − 0.5 ( m a x . x + m i n . x ) S x 由 r-l = \frac{2}{S_x},得:O_x = -\frac{r+l}{2}S_x = -0.5(max.x + min.x)S_x 由r−l=Sx2,得:Ox=−2r+lSx=−0.5(max.x+min.x)Sx
由 t − b = 2 S y , 得 : O y = − t + b 2 S y = − 0.5 ( m a x . y + m i n . y ) S y 由 t-b = \frac{2}{S_y},得:O_y = -\frac{t+b}{2}S_y = -0.5(max.y + min.y)S_y 由t−b=Sy2,得:Oy=−2t+bSy=−0.5(max.y+min.y)Sy
- 计算出摄像机视锥体分块的远平面在摄像机空间中投影后的位置,并把他变换到[0.0,1.0]。这么做的目的是为了在片段着色器中判断某个片段属于哪个分块
for(int i = 0 ; i < 分块个数 ; i++)
{
far_bounds[i] =0.5f*((-1.0f * frustums[i].far() * projMat[2][2] + projMat[3][2])/
frustums[i].far())+0.5f;
}
- 与阴影贴图一样,在摄像机空间中,投影矩阵使用的是透视投影:
p r o j M a t = [ 2 n r − l 0 r + l r − l 0 0 2 n t − b t + b t − b 0 0 0 − f + n f − n − 2 f n f − n 0 0 − 1 0 ] projMat = \left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & -\frac{f+n}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{matrix} \right] projMat=⎣⎢⎢⎡r−l2n0000t−b2n00r−lr+lt−bt+b−f−nf+n−100f−n−2fn0⎦⎥⎥⎤
[ X C Y C Z C W C ] = p r o j M a t ∗ [ X E Y E Z E W E ] \left[ \begin{matrix} X_C \\ Y_C \\ Z_C \\ W_C \end{matrix} \right] = projMat * \left[ \begin{matrix} X_E \\ Y_E \\ Z_E \\ W_E \end{matrix} \right] ⎣⎢⎢⎡XCYCZCWC⎦⎥⎥⎤=projMat∗⎣⎢⎢⎡XEYEZEWE⎦⎥⎥⎤
把 − f + n f − n 用 A 替 换 , − 2 f n f − n 用 B 替 换 , 得 : Z C = A Z E + B W E 把 -\frac{f+n}{f-n}用A替换,\frac{-2fn}{f-n}用B替换,得:Z_C = AZ_E + BW_E 把−f−nf+n用A替换,f−n−2fn用B替换,得:ZC=AZE+BWE
再 变 换 成 标 准 设 备 坐 标 : Z N = Z C W C = A Z E + B W E W C 再变换成标准设备坐标:Z_N = \frac {Z_C} {W_C} = \frac {AZ_E + BW_E}{W_C} 再变换成标准设备坐标:ZN=WCZC=WCAZE+BWE
又 因 为 W E = 1 , W C = − Z E , 所 以 Z N = A Z E + B − Z E 又因为W_E = 1,W_C = -Z_E,所以Z_N = \frac{AZ_E+B}{-Z_E} 又因为WE=1,WC=−ZE,所以ZN=−ZEAZE+B
由 于 标 准 设 备 坐 标 的 范 围 是 [ − 1.0 , 1.0 ] , 所 以 再 把 Z N ∗ 0.5 + 0.5 由于标准设备坐标的范围是[-1.0,1.0],所以再把Z_N * 0.5 + 0.5 由于标准设备坐标的范围是[−1.0,1.0],所以再把ZN∗0.5+0.5 - 需要注意的是:在实际应用时,如果给摄像机视锥体的近远平面设置的都是正值,那么在这里使用近远平面的值时,需要将他乘以-1。因为这一步是在摄像机空间执行的,在摄像机空间中摄像机指向-Z轴方向,所以要将上式中的ZE的乘以-1
最后
- 通过上面的铺垫,就可以渲染出不同分段的阴影贴图,并在最后的片段着色器中判断该使用哪个阴影贴图,最后渲染出阴影
- 下图是摄像机视锥体没有分块,相当于阴影贴图的效果,锯齿严重
- 下图是有两个分段,近处比较好,稍微远一点就出现了明显锯齿
- 下图是四个分段,被优化的效果看得见
标签:贴图,CSM,级联,frac,glm,max,阴影,transf 来源: https://blog.csdn.net/m0_57980287/article/details/118554696