Shadow Volume
https://www.cnblogs.com/chandler00x/p/3958407.html?utm_source=tuicool&utm_medium=referral
shadow volume是长啥样的
https://blog.csdn.net/jxw167/article/details/65435329
这篇理论讲得比较浅显易懂
https://blog.csdn.net/quxing10086/article/details/80140484
这篇代码比较清晰
https://www.cnblogs.com/dragon2012/p/3806026.html
shadow map 和 shadow volume的原理对比
优点:由于它是依靠Mesh渲染的,所以不像shadowmap一样依赖深度图精度导致锯齿。
缺点:比较麻烦需要构造Volume,并且Volume会参与计算,导致DC增加。最后还要根据模版缓冲区执行一个全屏shader,可能存在像素填充过高的问题
我简单理解了下,其原理如下:
1、遍历灯光周围物体计算出每个物体相对灯光所产生的Volume,这个Volume其实是个Mesh
2、正常渲染观察空间下场景所有物件(这里不包括Volume,很多博客都说是为了得到深度图,其实是正常渲染完场景顺便得到深度图。也因为如此,由于我是先学的shadow map,所以这里一开始没搞明白到底是灯光视角还是相机视角的,因为shadow map渲染的是相机视角的深度图)。
3、进行模版处理了,因为只处理模版缓冲区,所以这里要关闭深度写和颜色写,然后关闭面的裁剪,也就是说正反面都渲染。然后我们对所有的Volume进行渲染,采取以下规则渲染到模版缓冲区,处理完之后,如果物体的像素被Volume覆盖的话,则Volume的背面深度测试会失败因为深度值比物体的大,而正面则会深度测试成功。依照下面规则,也就是值会大于0。我想了想应该只有大于0或等于0两种情况。
1、如果在渲染阴影体积的背面多边形时深度测试失败,我们会增加模板缓冲区中的值。
2、如果在渲染阴影体积的前面多边形时深度测试失败,我们会减小模板缓冲区中的值。
3、在深度测试通过,模板测试失败情况下,我们什么都不做。
4、根据模版缓冲区的结果渲染阴影,这次就要开启面的裁剪和写颜色,我们执行一个全屏的shader来绘制阴影,模版比较设置为大于0的时候才测试通过,由于模版大于0的部分是阴影,所以我们要设置混合模式让阴影颜色跟背景颜色进行混合。
Shadow Map
优点:比较简单,整个场景的阴影仅用一张深度图就能够表示了
缺点:每个模型都需要多绘制一遍去生成shadow map,shadow map表示的东西太多而它的尺寸太小,导致它精度不够容易出现锯齿。
原理比较简单,主要用来处理平行光投影,由于平行光属于全局光不存在光源位置。所以我们需要在观察视角下构造一个相机,使其旋转角度等于光照角度,并且要看到观察视角下的所有东西。构造相机相关的我理解得不是特别透彻,大概是先创建相机并旋转到灯光角度,拿到灯光相机的视锥体之后与遍历观察视角下的所有模型进行处理(这部分模型是通过观察相机的视锥体与八叉树求得的),处理主要是创建一个空的boundingbox,然后每个模型进行合并,最后拿合并完的boundingbox去构造灯光相机的视锥体,令其完全覆盖这个boundingbox。这么做是为了提高shadow map精度,因为刚好覆盖,所以提高了shadow map的利用率。
然后在灯光相机视角下,对场景进行深度图的渲染,注意这里说的深度图不是指的深度缓冲区,而是将深度信息渲染到Texture里。因为只需要简单的深度信息,所以需要单独的shader不进行光照计算,只是简单地输出像素深度。每个模型渲染完之后,就得到了shadow map
紧接着正常渲染场景,同样是遍历每个模型进行渲染,渲染的时候会将上次处理的shadow map以及灯光相机的VP矩阵传进去,顶点着色器中会根据顶点的世界坐标和VP矩阵相乘计算出一个灯光空间的裁剪坐标。将裁剪坐标传到像素着色器之后,根据裁剪坐标采样shadow map就能得到当前裁剪坐标对应像素在灯光空间的深度值,同时裁剪坐标也能算出裁剪坐标在灯光空间的深度值。将两个深度值进行比较,如果从贴图采样的结果大于裁剪坐标的深度值的话,说明有其他物体深度值更小,说明它被遮挡了,需要绘制阴影,否则绘制模型。
Shadow Map锯齿处理
PCF(Percentage Closer Filter ) :简单来说就是采样shadow map的时候多采样周围的像素,例如才样当前像素,右、下、右下角像素相加取平均,或者用珀松分布算。最后的阴影系数范围在[0,1],拿它用作阴影颜色和模型颜色的混合系数。
CSM(convolution shadow map):在camera的近处,场景中的物体的分辨率比较高,一小段面片,会对应着大量的pixel,而此时的shadow map精度没有发生变化,所以就会有大量的pixel对应着shadow map中的同一点,因而产生锯齿的情况。针对这种情况我们可以用多张shadow map来解决这个问题,可以将原来一个灯光相机改成多个,改变它们的近、远裁剪面,分别用不同的值来处理不同距离的物体,小的用来处理近处的物体提高近处shadow map的利用率,大的则用来处理远的。例如近裁减面是10,远裁剪面分别是1000,2000,3000,4000。则第一张shadow map计算的裁剪范围是[10,1000],第二张是[1000,2000],第三张是[2000,3000],第四张是[3000,4000]。
根据近裁减面和远裁剪面的值,我们可以只处理该裁剪范围内的物体,有多个相机,每台相机处理不同的物体,它们可以各自渲染到一张RenderTarget里,也可以渲染到同一张RT的不同区域,例如4台相机每台占RT的1/4,只要投影矩阵处理下就好了。最后只要渲染物体的时候根据深度找到对应的相机,然后将世界坐标和相机对应的投影矩阵,转到对应的投影坐标去采样贴图。就能得到比较精确的深度值
左边是非CSM下一张图的利用率,右图是VSM情况下,越黑表示利用率越差。
优点:图片利用率提高,能有效减少锯齿
缺点:需要设置多台相机,并且需要每台相机遍历哪些物体在它的视锥体内
VSM(variance shadow map):计算shadow map的时候R通道存深度值,G通道存深度值的平方。shadow map处理完之后进行横向和纵向模糊。最后就是用切尔雪夫不等式求方差和期望。优点是只采样一次,然后效果还不错
float ReduceLightBleeding(float min, float p_max)
{
return clamp((p_max - min) / (1.0 - min), 0.0, 1.0);
}
// 这里Moments表示shadow map算出来的深度和深度平方
float Chebyshev(vec2 Moments, float depth)
{
//One-tailed inequality valid if depth > Moments.x
float p = float(depth <= Moments.x);
//Compute variance.
float Variance = Moments.y - (Moments.x * Moments.x);
// cVSMShadowParams.xy的值是(0.0000001f, 0.9f)
float minVariance = cVSMShadowParams.x;
Variance = max(Variance, minVariance);
//Compute probabilistic upper bound.
float d = depth - Moments.x;
float p_max = Variance / (Variance + d*d);
// Prevent light bleeding
p_max = ReduceLightBleeding(cVSMShadowParams.y, p_max);
return max(p, p_max);
}
Shadow Acne && Self Shadowing
参考自:
https://blog.csdn.net/ronintao/article/details/51649664
https://www.cnblogs.com/hellobb/p/8058348.html
https://www.cnblogs.com/zsb517/p/6817373.html
由于shadow map精度有限,容易多个像素对应shadow map上一个点,就容易导致精度误差,例如屏幕多个像素是0.5,0.50001,0.5,0.50001对应贴图里的一个点是0.5,就会导致阴影和非阴影区域交替出现,导致自投影的情况出现。
解决方案就是用shadow bias,原理很简单,无非是将模型深度向灯光方向偏移变小一点,或者让贴图的值变大一点。但是bias这个值很微妙,它加小了的话没法解决shadow acne的问题,加大了的话,不仅深度不准还会导致Peter panning。
float bias = 0.005; float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
所以现在常用的shadow bias的计算方法,是基于物体斜度的,称为 slope scale based depth bias。
说白了就是当三角面与灯光夹角越小,那么shadow map中一个点对应三角面的区域也就越多,其深度变化就越大,越需要一个比较大的bias。而三角面与灯光夹角越小,说明dot(normal,lightDir)的值就越小,所以一般bias与它们的cos值成反比。简单来说公式就是 bias = factorSlope * slope + constantBias。
这里factorSlope是slope scale的系数,slope是根据cos值算出来的一个值,constantBias是基础的bias。这里factorSlope和constantBias都需要慢慢调,可以参考以下几种做法,不过貌似opengl自带了一个api叫glPolygonOffset,需要传入两个参数,第一个应该是slope scale,第二个应该是constantBias,然后slope它帮你算好了,套这两个参数计算出公式然后自动帮你偏移深度。
// 第一种,直接拿tan当成slope直接与constantBias相加
float GetShadowBias(float3 lightDir , float3 normal , float maxBias , float baseBias)
{
float cos_val = saturate(dot(lightDir, normal));
float sin_val = sqrt(1 - cos_val*cos_val); // sin(acos(L·N))
float tan_val = sin_val / cos_val; // tan(acos(L·N))
float bias = baseBias + clamp(tan_val,0 , maxBias) ;
return bias ;
}
// 第二种,将1-cos值当成slope,然后套公式
// dot product returns cosine between N and L in [-1, 1] range
// then map the value to [0, 1], invert and use as offset
float offsetMod = 1.0 - clamp(dot(N, L), 0, 1)
float offset = minOffset + maxSlopeOffset * offsetMod;
// 第三种,貌似和第一种一样
// another method to calculate offset
// gives very large offset for surfaces parallel to light rays
float offsetMod2 = tan(acos(dot(N, L)))
float offset2 = minOffset + clamp(offsetMod2, 0, maxSlopeOffset);
Peter Panning
当bias太大时,可能导致原来是阴影的地方被误认为是自投影而给忽略掉了。解决方案就是Bias不要设太大(滑稽)
Z-Fighting && Depth-Fighting
当两个物体离得很近时,如果稍微移动相机就会出现闪烁现象,很可能就是出现了Z-Fighting。也就是说两者的深度太接近,导致渲染的时候算出来有时候是A大有时候是B大。这种情况可能是因为近裁减面和远裁剪面设置得不合理导致的,通过投影矩阵推导可以知道越接近远裁剪面,深度精度越低也就越相似,解决方案就是适当把近裁减面放后面一点,以此让物体离近裁减面近一些而获得更好的精度。
法线空间变换
主要为了解决将法线从切线空间转世界空间或观察空间。Tangent可以通过CPU预处理存到顶点数据中,GPU根据T(Tangent)和N(Normal)可以通过N叉积T得到B(BiTangent)。直接传入GPU的T和N属于本地空间,如果我们将T和N转到世界空间,然后进行叉积得出来的B则是世界空间
用T、B、N三个向量直接构造出来的矩阵,我们称之为TBN矩阵,其作用就是将采样贴图出来的法线从切线空间转到我们想要的空间。最后得出来的法线我们会拿去做光照计算
我们一般会将光照计算统一在一个空间里进行计算,例如全都转到切线空间,或全转到世界空间或观察空间。假设我们用世界空间或观察空间,T可以通过直接乘以M或MV来完成转换,而N则不行,因为有可能存在矩阵缩放,这样计算出来的法线不是我们希望得到的。所以希望通过构造一个矩阵使得N乘以它可以完成转换,通过查阅资料,得到的答案是乘以M的逆矩阵的转置矩阵或者MV的逆矩阵的转置。具体看下面这篇文章,其中有一步其实不是特别理解,就是T(b)·N(b)=0,然后要将T(b)进行转置,我猜可能是为了将向量转成矩阵计算,将左边的列向量转成行向量之后,相乘结果等于0,然后就给它转置了下,方便解。
http://www.idivecat.com/archives/749
https://blog.csdn.net/qq_29523119/article/details/52776960 这篇是DX统一在世界空间进行计算
https://zhuanlan.zhihu.com/p/61995038 这是opengl统一在切线空间进行计算,将向量从世界空间转到切线空间,其实就是拿向量乘以世界空间的TBN矩阵的转置,因为TBN是正交矩阵,则TBN的逆矩阵等于它的转置矩阵,通过乘以它的逆矩阵可以将向量从世界空间转到切线空间,因为世界空间的TBN,其实是切线空间转世界空间的矩阵,反过来则是世界空间转切线空间。
我们经常会思考顶点法线和贴图法线之间的关系,我是这么理解的,首先很容易理解的一点就是顶点法线可以用来构造切线空间转其他空间的矩阵。而法线贴图的好处我觉得其实可以像理解Gouraud着色和Phong着色那样去理解顶点法线和贴图法线,Gouraud是在顶点着色器中计算颜色,然后像素着色器根据顶点颜色插值像素颜色,如果不用法线贴图的情况下,确实法线就跟这里顶点着色器的颜色是一个道理。而Phong着色是在像素着色器中采样贴图颜色进行着色,它会比Gouraud跟细致,主要原因是它每个像素都去精准地采样贴图,一个三角面上有很多像素,如果两个顶点间想表示比较极端的颜色例如红橙黄绿青蓝紫,依靠贴图可以轻松做到,而只依赖顶点信息的话,你只能两个颜色间进行插值,丢失掉了很多信息。所以法线贴图和顶点法线同理,法线贴图比顶点法线跟细致,并且也能够满足定制化的需求。