osg使用整理(10):Shadow Mapping

王小于的啦發表於2024-04-28

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

            }
        }
    }

相關文章