思路很簡單,將相機放到光源處,觀察到的物體部分就是光照部分,沒觀察到的就是陰影,它被自身或其他物體遮擋住了。我們知道可以透過深度緩衝來儲存面片到相機間的相對距離關係,離相機最近的物體會遮擋其他相同位置的物體面片,於是我們可以首先將相機放在光源處,這樣得到的深度緩衝就儲存了可以看見的面片資訊,稱為陰影貼圖。
如上圖所示,當渲染點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 );
}
}
}