osg使用整理(10):Shadow Mapping

1. 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  );

            }
        }
    }
posted @ 2024-04-28 20:44  王小于的啦  阅读(163)  评论(0编辑  收藏  举报