osg使用整理(10):Shadow Mapping
思路很简单,将相机放到光源处,观察到的物体部分就是光照部分,没观察到的就是阴影,它被自身或其他物体遮挡住了。我们知道可以通过深度缓冲来保存面片到相机间的相对距离关系,离相机最近的物体会遮挡其他相同位置的物体面片,于是我们可以首先将相机放在光源处,这样得到的深度缓冲就保存了可以看见的面片信息,称为阴影贴图。
如上图所示,当渲染点P处的面片时,需要判断它是否处于阴影中,使用相机变换矩阵将点P变换到光源为中心的坐标空间中,得到点P的深度值,然后计算阴影贴图中点P的深度值,如果小于当前点P的深度,说明其处于阴影中。
2. osgShadow类分析
从example::osgShadow.cpp中可以发现,osgShadow包含SoftShadowMap、ShadowMap、ParallelSplitShadowMap、LightSpacePerspectiveShadowMap、StandardShadowMap、ViewDependentShadowMap等阴影贴图技术。从简单的ShadowMap类看如何实现的。
2.1 osgShadow::ShadowMap类
osgShadow::ShadowMap类继承了osgShadow::ShadowTechnique类,用于提供设置渲染阴影的shader以及uniform。
2.1.1 ShadowMap::init()函数
init函数用于阴影渲染的初始化。在ShadowTechnique::traverse(osg::NodeVisitor& nv)函数中被更新遍历时调用。
其中有几个关键点:
a. 阴影悬浮问题 (Peter Panning)
阴影相对实际物体位置的偏移现象叫做悬浮,可以通过渲染深度贴图的时候使用正面剔除解决。
// cull front faces so that only backfaces contribute to depth map
osg::ref_ptr<osg::CullFace> cull_face = new osg::CullFace;
cull_face->setMode(osg::CullFace::FRONT);
stateset->setAttribute(cull_face.get(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
b. 采样过多问题
超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会的得到不正确的深度结果,因为他不是基于真实的来自光源的深度值。可以把所有超过深度贴图的坐标深度范围是1.0,这样超过坐标永远不在阴影之中。然后把深度贴图的环绕方式设为GL_CLAMP_TO_BORDER。
首先准备了一张阴影贴图,注意格式为深度贴图,超出纹理范围为白色,分辨率默认为1024*1024。
// the shadow comparison should fail if object is outside the texture
_texture->setWrap(osg::Texture2D::WRAP_S,osg::Texture2D::CLAMP_TO_BORDER);
_texture->setWrap(osg::Texture2D::WRAP_T,osg::Texture2D::CLAMP_TO_BORDER);
_texture->setBorderColor(osg::Vec4(1.0f,1.0f,1.0f,1.0f));
然后将生成的阴影贴图附着到相机上,相机视口和纹理保持一致,渲染方式为osg::Camera::FRAME_BUFFER_OBJECT,设置相机裁剪掉模型正面。
c. shadow2DProj函数
glsl自带的深度纹理比较函数,输入参数为深度纹理采样器,纹理坐标,如果深度值小于深度纹理中的深度值则返回1否则返回0,要使用shadow2DProj必须打开深度纹理比较模式。
_texture->setShadowComparison(true);
如果我们使用了shadow2DProj的话,那么比较函数不是我们自己实现的,那么我们就没办法加上这个小的容错误差。这时候我们可以在绘制的时候使用polygonoffset来避免z-fighting的问题。
osg::ref_ptr<osg::PolygonOffset> polygon_offset = new osg::PolygonOffset;
polygon_offset->setFactor(factor);
polygon_offset->setUnits(units);
stateset->setAttribute(polygon_offset.get(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
stateset->setMode(GL_POLYGON_OFFSET_FILL, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
2.2 osgShadow::SoftShadowMap类
SoftShadowMap类提供了对软阴影的支持。当我们放大ShadowMap生成的阴影时,可以发现命案交界处有明显的锯齿,这是因为深度贴图的分辨率有限,当多个片段从同一深度值采样时,这几个片段便得到的是同一个阴影,就会产生锯齿边。可以通过PCF方案来缓解这个问题。
2.2.1 PCF (percentage-closer filtering)
PCF通过从深度贴图中多次采样 ,每一次采样的纹理坐标都稍微不同,然后平均采样结果,这样就能得到边缘柔和的阴影。
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
for(int y = -1; y <= 1; ++y)
{
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
但这种方法只有在阴影区域较小时才会起作用,否则条带伪影就很明显。为了消除这种条带伪影,就必须增加采样次数,这又会带来性能问题。
osgShadow::SoftShadowMap类采用了GPU gem2 17章所述的Efficient Soft-Edged Shadows Using Pixel Shader Branching方法,实现了阴影效果和shader性能的平衡。
2.2.2 优化后的PCF思路
a. 蒙特卡洛方法:该方法基于概率计算,其中从函数中获取许多随机样本并进行平均。这样每次计算阴影值时,阴影贴图样本位置的顺序略有不同,因此不同像素出的近似误差不同,这有效地用高频噪声取代了条带。
b. 分层或抖动采样:如果我们仔细地构建纹理坐标偏移量序列,使其仅具有一定成都的随机性,那么我们可以获得比使用完全随机偏移更好的结果。首先我们将采样区域分成许多面积相等的子区域,然后在每个子区域中随机采样。
c.性能优化:为了获得良好的阴影质量,我们可能需要采集多达64个样本,我们希望只在真正的阴影附近采样,因此需要先判断下采样位置是否在明暗交界处。我们采样少量样本,如果他们都为1或者0,说明采样点处于阴影内外,则跳过PCF过程。这些样本的偏移量最好位于最边缘,这样最容易捕获到明暗变化。
2.2.3 SoftShadowMap::initJittering函数
initJittering函数用于创建一张包含了纹理坐标扰动数据的三维纹理。其中纹理的s、t方向对应二维纹理坐标x、y,r方向则储存了上述多余的采样偏移值。8乘以8的网格共64个扰动,我们可以利用RG通道存储奇数组、BA通道存储偶数组。
// 创建偏移数据三维纹理,数据类型为8bit
osg::Image* image3D = new osg::Image;
unsigned char *data3D = new unsigned char[size * size * R * 4];
for ( unsigned int s = 0; s < size; ++s )
{
for ( unsigned int t = 0; t < size; ++t )
{
float v[4], d[4];
for ( unsigned int r = 0; r < R; ++r )
{
const int x = r % ( gridW / 2 );
const int y = ( gridH - 1 ) - ( r / (gridW / 2) );
// Generate points on a regular gridW x gridH rectangular
// grid. We multiply x by 2 because, we treat 2
// consecutive x each loop iteration. Add 0.5f to be in
// the center of the pixel. x, y belongs to [ 0.0, 1.0 ].
v[0] = float( x * 2 + 0.5f ) / gridW;
v[1] = float( y + 0.5f ) / gridH;
v[2] = float( x * 2 + 1 + 0.5f ) / gridW;
v[3] = v[1];
// Jitter positions. ( 0.5f / w ) == ( 1.0f / 2*w )
v[0] += ((float)rand() * 2.f / RAND_MAX - 1.f) * ( 0.5f / gridW );
v[1] += ((float)rand() * 2.f / RAND_MAX - 1.f) * ( 0.5f / gridH );
v[2] += ((float)rand() * 2.f / RAND_MAX - 1.f) * ( 0.5f / gridW );
v[3] += ((float)rand() * 2.f / RAND_MAX - 1.f) * ( 0.5f / gridH );
// Warp to disk; values in [-1,1]
d[0] = sqrtf( v[1] ) * cosf( 2.f * 3.1415926f * v[0] );
d[1] = sqrtf( v[1] ) * sinf( 2.f * 3.1415926f * v[0] );
d[2] = sqrtf( v[3] ) * cosf( 2.f * 3.1415926f * v[2] );
d[3] = sqrtf( v[3] ) * sinf( 2.f * 3.1415926f * v[2] );
// store d into unsigned values [0,255]
const unsigned int tmp = ( (r * size * size) + (t * size) + s ) * 4;
data3D[ tmp + 0 ] = (unsigned char)( ( 1.f + d[0] ) * 127 );
data3D[ tmp + 1 ] = (unsigned char)( ( 1.f + d[1] ) * 127 );
data3D[ tmp + 2 ] = (unsigned char)( ( 1.f + d[2] ) * 127 );
data3D[ tmp + 3 ] = (unsigned char)( ( 1.f + d[3] ) * 127 );
}
}
}