Ogre2.1大量光源渲染
Ogre2.1不是采用現在大部分引擎所用的延遲渲染,而是采用一種前向渲染的改進技術,理論基本來自於Forward+,見如下。
http://www.ogre3d.org/2015/03/05/ogre-progress-report-february-2015
第一個鏈接是Forward+技術的原理,第二個是講Ogre2.x的Forward3D實現,和正規的Forward+不同之處,以及沒有默認采用延遲渲染的原因,順便也指出,一樣可以采用延遲渲染,並且還能使用Forward3D生成的數據,實現Tiled-based Deferred Shading,相比Ogre1.x中的延遲渲染例子,現ogre2.1中實現更方便與高效,大致看下Ogre2.1的渲染流程(更高效的多線程,統一Cull,SOA,SMID)就能明白。
為了鞏固對延遲渲染的理解,解釋下Ogre1.x中延遲渲染例子,對於這個例子我自認為是Ogre1.x中的比較有難度的一個例子,因為關聯的東東太多,如渲染流程以及事件在流程中的位置,合成器技術,自定義合成器渲染的實現,RTT,MRT等等,原來一直想單獨用一篇來說明Ogre1.x中的延遲渲染例子,在Ogre2.1出來后,這個想法也就沒了,簡單說下這個例子,在這不講理論,理論大家自己查找下,只講Ogre1.x中的這個例子實現過程。
Ogre1.9的延遲渲染例子
1 生成GBuffer。使用合成器第一階段DeferredShading/GBuffer,設置當前材質方案為GBuffer,截獲(10至79)渲染通道,一般來說,Ogre1.x中,10之前的通道用來渲染背景如天空盒之類的,90之后的用來渲染UI這些,正常模型不設置默認為50,在這燈的包裝模型設置渲染通道為80,所以GBuffer里沒有包含燈的模型。
在渲染時,因為當前材質方案為GBuffer,Ogre就會去查找相應的MaterialManager::Listener,在這GBufferSchemeHandler調用GBufferMaterialGenerator根據Pass屬性生成對應着色器代碼,這里有點像Ogre2.1中的高級材質系統HLMS,在這把如點位置,頂點顏色,法線,紋理坐標等寫入到GBuffer中,也就是紋理中,這里只用到二張,第一張保存顏色與暴光系數,第二張法線與深度,如果有法線貼圖,法線是經過片斷法線貼圖轉化了的,還有可能有人要問,頂點位置昨沒了,這個在下面會提到。
2 使用GBuffer信息,還原頂點位置,法線,結合燈光生成最終顏色。使用合成器第二階段DeferredShading/ShowLit,采用自定義的合成器渲染DeferredLight渲染場景全局顏色與燈光,此處可見DeferredLightRenderOperation::execute,把場景全局顏色包裝成AmbientLight,燈光包裝成DLight進行渲染。相應着色器代碼的目錄在DeferredShading/post,其中,AmbientLight與DLight都采用相同的頂點着色器vs.glsl,其中AmbientLight片斷着色器是Ambient_ps.glsl,DLight根據燈光類型設置編譯條件,LightMaterial_ps.glsl生成不同片斷。
在這感覺有必要說下如何得到頂點位置,感覺這個思路還是很贊的。注意,GBuffer里存的深度是在視圖坐標系下的,視圖坐標系也是這個思路的重點。下面是vs.glsl的代碼.

#version 150 in vec4 vertex; out vec2 oUv0; out vec3 oRay; uniform vec3 farCorner; uniform float flip; void main() { // Clean up inaccuracies vec2 Pos = sign(vertex.xy); // Image-space oUv0 = (vec2(Pos.x, -Pos.y) + 1.0) * 0.5; // This ray will be interpolated and will be the ray from the camera // to the far clip plane, per pixel oRay = farCorner * vec3(Pos, 1); gl_Position = vec4(Pos, 0, 1); gl_Position.y *= flip; }
其中farCorner是在視圖坐標系右上角頂點,當時為這句farCorner*vec3(pos,1)想了半天也沒搞清楚是啥意思,后來結合AmbientLight與方向光都是渲染一個正文形(-1,1)范圍,就如我們現在看一個黑板,如果我們知道其中右上角是1,1,左上角是-1,1等,而實際位置右上角是farCorner,那么所有的點是不是都知道了,當然這只包含xy,但是深度我們前面已經保存下來了.后面的頂點光與聚光燈的思路差不多,控制渲染時的矩陣達到這個效果。
別的延遲渲染算法總的來說,其過程與這大同小異,使原來模型N個,燈光M個,要渲染N*M成功變成N+M個了,所以燈光越多,延遲渲染的優勢越大。嗯,也要回到正題上來了,Ogre2.1為什么不使用延遲渲染了,前面鏈接里dark_sylinc有說,透明度,硬件AA,復雜材質,大量帶寬,嗯,KlayGE這個鏈接里也有說,就采用了Forward+,但是正統Forward+一是需要前向渲染深度,二是需要DX11級別的硬件來支持亂序訪問視圖(UAV)。所以dark_sylinc稍微修改了下這個技術,使之一不需要前向渲染深度,二不需要DX11的硬件,算法主要如燈光列表主要是CPU算的,dark_sylinc好像也暫時不清楚是否需要后面移植到Compute Shaders上,至於性能比Forward+與延遲渲染是好是壞還是更多和場景有關,總的來說,差不到那去,也好不到那去,好,最后讓我們來看下Ogre2.1中如何實現大量燈光渲染的。
Ogre2.1中的Forward+算法
在KlayGE那個鏈接里,我們能知道Forward+的大致用法,主要思想就是確定每塊上需要那些燈光,Forward3D就是這個算法的實現,還是老樣子,先上代碼.

Forward3D::Forward3D( uint32 width, uint32 height, uint32 numSlices, uint32 lightsPerCell, float minDistance, float maxDistance, SceneManager *sceneManager ) : mWidth( width ), mHeight( height ), mNumSlices( numSlices ), /*mWidth( 1 ), mHeight( 1 ), mNumSlices( 2 ),*/ mLightsPerCell( lightsPerCell ), mTableSize( mWidth * mHeight * mLightsPerCell ), mMinDistance( minDistance ), mMaxDistance( maxDistance ), mInvMaxDistance( 1.0f / mMaxDistance ), mVaoManager( 0 ), mSceneManager( sceneManager ), mDebugMode( false ) { uint32 sliceWidth = mWidth; uint32 sliceHeight = mHeight; mResolutionAtSlice.reserve( mNumSlices ); for( uint32 i=0; i<mNumSlices; ++i ) { mResolutionAtSlice.push_back( Resolution( sliceWidth, sliceHeight, getDepthAtSlice( i + 1 ) ) ); sliceWidth *= 2; sliceHeight *= 2; } mResolutionAtSlice.back().zEnd = std::numeric_limits<Real>::max(); const size_t p = -((1 - (1 << (mNumSlices << 1))) / 3); mLightCountInCell.resize( p * mWidth * mHeight, 0 ); }
幾個關鍵屬性,mWidth和mHeight表示第一層長度和寬度,mNumSlices表示多少層,后一層是前一層長寬各乘以2,mLightsPerCell表示每格最多存放多少燈。舉個例子,mWidth和mHeight是2,mNumSlices是4,mLightsPerCell是100,那么就是說一共四層,其中第一層長寬各2,就是4格,而第二層長寬各4,就是16格,以至類推,其中每格能放100個燈的索引(后面會說應該是99),比如第一層,一共有200個燈會照亮第一層中的第一格,但是我只能存放99個索引。另外101個不記錄。那么mTableSize就是第一層所有燈索引的長度。其中這里會常出現的一個公式,-((1 - (1 << (mNumSlices << 1))) / 3),這個就是表示當前層一共有多少個mTableSize,如上面第一層固定只有一個mTableSize,第二層有4個,第三層有16個,第四去有64個,那么在第二層一共有5個,第三層一共有21個,第四層一共有85個。
然后大致過程就是,在開始渲染通道里的模型前,生成二個列表,一個是所有光源的屬性,二個每個格子里對其有影響的光源列表。首先調用Forward3D::collectLights,除開方向光,陰影燈外的所有光源,方向光對每個像素肯定都有影響,因此不列出來,陰影光源會單獨計算。其中調用Forward3D::fillGlobalLightListBuffer生成當前攝像機下的燈光列表,記錄每個光源的位置,環境,鏡面等信息。然后針對每個光的AABB,得到在視圖模式下的最大與最小深度,比較由mNumSlices確定的深度層,分別得到最大與最少深度對應那個層級,以及由AABB得到對應所占格子數,根據層,格子數,得到格子位置,取出這個格子的第一個位置得到當前這個格子已經有多少個光源了,如果少於 mLightsPerCell-1,則定位到對應的格式里的索引位置,寫入當前光源的索引,具體代碼就不貼了,大家如果有興趣自己下載源代碼看看。
下面是一段PBS生成的片斷着色器代碼。

#version 330 core #extension GL_ARB_shading_language_420pack: require layout(std140) uniform; #define FRAG_COLOR 0 layout(location = FRAG_COLOR, index = 0) out vec4 outColour; in vec4 gl_FragCoord; // START UNIFORM DECLARATION struct ShadowReceiverData { mat4 texViewProj; vec2 shadowDepthRange; vec4 invShadowMapSize; }; struct Light { vec3 position; vec3 diffuse; vec3 specular; vec3 attenuation; vec3 spotDirection; vec3 spotParams; }; //Uniforms that change per pass layout(binding = 0) uniform PassBuffer { //Vertex shader (common to both receiver and casters) mat4 viewProj; //Vertex shader mat4 view; ShadowReceiverData shadowRcv[5]; //------------------------------------------------------------------------- //Pixel shader mat3 invViewMatCubemap; vec4 ambientUpperHemi; vec4 ambientLowerHemi; vec4 ambientHemisphereDir; float pssmSplitPoints0; float pssmSplitPoints1; float pssmSplitPoints2; Light lights[3]; //f3dData.x = minDistance; //f3dData.y = invMaxDistance; //f3dData.z = f3dNumSlicesSub1; //f3dData.w = uint cellsPerTableOnGrid0 (floatBitsToUint); vec4 f3dData; vec4 f3dGridHWW[5]; } pass; //Uniforms that change per Item/Entity, but change very infrequently struct Material { /* kD is already divided by PI to make it energy conserving. (formula is finalDiffuse = NdotL * surfaceDiffuse / PI) */ vec4 kD; //kD.w is alpha_test_threshold vec4 kS; //kS.w is roughness //Fresnel coefficient, may be per colour component (vec3) or scalar (float) //F0.w is transparency vec4 F0; vec4 normalWeights; vec4 cDetailWeights; vec4 detailOffsetScaleD[4]; vec4 detailOffsetScaleN[4]; uvec4 indices0_3; //uintBitsToFloat( indices4_7.w ) contains mNormalMapWeight. uvec4 indices4_7; }; layout(binding = 1) uniform MaterialBuf { Material m[273]; } materialArray; //Uniforms that change per Item/Entity layout(binding = 2) uniform InstanceBuffer { //.x = //The lower 9 bits contain the material's start index. //The higher 23 bits contain the world matrix start index. // //.y = //shadowConstantBias. Send the bias directly to avoid an //unnecessary indirection during the shadow mapping pass. //Must be loaded with uintBitsToFloat uvec4 worldMaterialIdx[4096]; } instance; // END UNIFORM DECLARATION in block { flat uint drawId; vec3 pos; vec3 normal; vec2 uv0; vec4 posL0; vec4 posL1; vec4 posL2; vec4 posL3; vec4 posL4; float depth; } inPs; /*layout(binding = 1) */uniform usamplerBuffer f3dGrid; /*layout(binding = 2) */uniform samplerBuffer f3dLightList; #define ROUGHNESS material.kS.w Material material; vec3 nNormal; uniform sampler2DShadow texShadowMap[5]; float getShadow( sampler2DShadow shadowMap, vec4 psPosLN, vec4 invShadowMapSize ) { float fDepth = psPosLN.z; vec2 uv = psPosLN.xy / psPosLN.w; float retVal = 0; vec2 fW; vec4 c; retVal += texture( shadowMap, vec3( uv, fDepth ) ).r; return retVal; } //Default BRDF vec3 BRDF( vec3 lightDir, vec3 viewDir, float NdotV, vec3 lightDiffuse, vec3 lightSpecular ) { vec3 halfWay= normalize( lightDir + viewDir ); float NdotL = clamp( dot( nNormal, lightDir ), 0.0, 1.0 ); float NdotH = clamp( dot( nNormal, halfWay ), 0.0, 1.0 ); float VdotH = clamp( dot( viewDir, halfWay ), 0.0, 1.0 ); float sqR = ROUGHNESS * ROUGHNESS; //Roughness/Distribution/NDF term (GGX) //Formula: // Where alpha = roughness // R = alpha^2 / [ PI * [ ( NdotH^2 * (alpha^2 - 1) ) + 1 ]^2 ] float f = ( NdotH * sqR - NdotH ) * NdotH + 1.0; float R = sqR / (f * f + 1e-6f); //Geometric/Visibility term (Smith GGX Height-Correlated) float Lambda_GGXV = NdotL * sqrt( (-NdotV * sqR + NdotV) * NdotV + sqR ); float Lambda_GGXL = NdotV * sqrt( (-NdotL * sqR + NdotL) * NdotL + sqR ); float G = 0.5 / (( Lambda_GGXV + Lambda_GGXL + 1e-6f ) * 3.141592654); //Formula: // fresnelS = lerp( (1 - V*H)^5, 1, F0 ) float fresnelS = material.F0.x + pow( 1.0 - VdotH, 5.0 ) * (1.0 - material.F0.x); //We should divide Rs by PI, but it was done inside G for performance vec3 Rs = ( fresnelS * (R * G) ) * material.kS.xyz.xyz * lightSpecular; //Diffuse BRDF (*Normalized* Disney, see course_notes_moving_frostbite_to_pbr.pdf //"Moving Frostbite to Physically Based Rendering" Sebastien Lagarde & Charles de Rousiers) float energyBias = ROUGHNESS * 0.5; float energyFactor = mix( 1.0, 1.0 / 1.51, ROUGHNESS ); float fd90 = energyBias + 2.0 * VdotH * VdotH * ROUGHNESS; float lightScatter = 1.0 + (fd90 - 1.0) * pow( 1.0 - NdotL, 5.0 ); float viewScatter = 1.0 + (fd90 - 1.0) * pow( 1.0 - NdotV, 5.0 ); float fresnelD = 1.0 - fresnelS; //We should divide Rd by PI, but it is already included in kD vec3 Rd = (lightScatter * viewScatter * fresnelD) * material.kD.xyz * lightDiffuse; return NdotL * (Rs + Rd); } void main() { uint materialId = instance.worldMaterialIdx[inPs.drawId].x & 0x1FFu; material = materialArray.m[materialId]; /// Sample detail maps and weight them against the weight map in the next foreach loop. /// 'insertpiece( SampleDiffuseMap )' must've written to diffuseCol. However if there are no /// diffuse maps, we must initialize it to some value. If there are no diffuse or detail maps, /// we must not access diffuseCol at all, but rather use material.kD directly (see piece( kD ) ). /// Blend the detail diffuse maps with the main diffuse. /// Apply the material's diffuse over the textures // Geometric normal nNormal = normalize( inPs.normal ); float fShadow = 1.0; if( inPs.depth <= pass.pssmSplitPoints0 ) fShadow = getShadow( texShadowMap[0], inPs.posL0, pass.shadowRcv[0].invShadowMapSize ); else if( inPs.depth <= pass.pssmSplitPoints1 ) fShadow = getShadow( texShadowMap[1], inPs.posL1, pass.shadowRcv[1].invShadowMapSize ); else if( inPs.depth <= pass.pssmSplitPoints2 ) fShadow = getShadow( texShadowMap[2], inPs.posL2, pass.shadowRcv[2].invShadowMapSize ); /// If there is no normal map, the first iteration must /// initialize nNormal instead of try to merge with it. /// Blend the detail normal maps with the main normal. //Everything's in Camera space vec3 viewDir = normalize( -inPs.pos ); float NdotV = clamp( dot( nNormal, viewDir ), 0.0, 1.0 ); vec3 finalColour = vec3(0); finalColour += BRDF( pass.lights[0].position, viewDir, NdotV, pass.lights[0].diffuse, pass.lights[0].specular ) * fShadow; vec3 lightDir; float fDistance; vec3 tmpColour; float spotCosAngle; //Point lights //Spot lights //spotParams[0].x = 1.0 / cos( InnerAngle ) - cos( OuterAngle ) //spotParams[0].y = cos( OuterAngle / 2 ) //spotParams[0].z = falloff lightDir = pass.lights[1].position - inPs.pos; fDistance= length( lightDir ); spotCosAngle = dot( normalize( inPs.pos - pass.lights[1].position ), pass.lights[1].spotDirection ); if( fDistance <= pass.lights[1].attenuation.x && spotCosAngle >= pass.lights[1].spotParams.y ) { lightDir *= 1.0 / fDistance; float spotAtten = clamp( (spotCosAngle - pass.lights[1].spotParams.y) * pass.lights[1].spotParams.x, 0.0, 1.0 ); spotAtten = pow( spotAtten, pass.lights[1].spotParams.z ); tmpColour = BRDF( lightDir, viewDir, NdotV, pass.lights[1].diffuse, pass.lights[1].specular ) * getShadow( texShadowMap[3], inPs.posL3, pass.shadowRcv[3].invShadowMapSize ); float atten = 1.0 / (1.0 + (pass.lights[1].attenuation.y + pass.lights[1].attenuation.z * fDistance) * fDistance ); finalColour += tmpColour * (atten * spotAtten); } lightDir = pass.lights[2].position - inPs.pos; fDistance= length( lightDir ); spotCosAngle = dot( normalize( inPs.pos - pass.lights[2].position ), pass.lights[2].spotDirection ); if( fDistance <= pass.lights[2].attenuation.x && spotCosAngle >= pass.lights[2].spotParams.y ) { lightDir *= 1.0 / fDistance; float spotAtten = clamp( (spotCosAngle - pass.lights[2].spotParams.y) * pass.lights[2].spotParams.x, 0.0, 1.0 ); spotAtten = pow( spotAtten, pass.lights[2].spotParams.z ); tmpColour = BRDF( lightDir, viewDir, NdotV, pass.lights[2].diffuse, pass.lights[2].specular ) * getShadow( texShadowMap[4], inPs.posL4, pass.shadowRcv[4].invShadowMapSize ); float atten = 1.0 / (1.0 + (pass.lights[2].attenuation.y + pass.lights[2].attenuation.z * fDistance) * fDistance ); finalColour += tmpColour * (atten * spotAtten); } float f3dMinDistance = pass.f3dData.x; float f3dInvMaxDistance = pass.f3dData.y; float f3dNumSlicesSub1 = pass.f3dData.z; uint cellsPerTableOnGrid0= floatBitsToUint( pass.f3dData.w ); // See C++'s Forward3D::getSliceAtDepth /*float fSlice = 1.0 - clamp( (-inPs.pos.z + f3dMinDistance) * f3dInvMaxDistance, 0.0, 1.0 ); fSlice = (fSlice * fSlice) * (fSlice * fSlice); fSlice = (fSlice * fSlice); fSlice = floor( (1.0 - fSlice) * f3dNumSlicesSub1 );*/ float fSlice = clamp( (-inPs.pos.z + f3dMinDistance) * f3dInvMaxDistance, 0.0, 1.0 ); fSlice = floor( fSlice * f3dNumSlicesSub1 ); uint slice = uint( fSlice ); //TODO: Profile performance: derive this mathematically or use a lookup table? uint offset = cellsPerTableOnGrid0 * (((1u << (slice << 1u)) - 1u) / 3u); float lightsPerCell = pass.f3dGridHWW[0].w; //pass.f3dGridHWW[slice].x = grid_width / renderTarget->width; //pass.f3dGridHWW[slice].y = grid_height / renderTarget->height; //pass.f3dGridHWW[slice].z = grid_width * lightsPerCell; //uint sampleOffset = 0; uint sampleOffset = offset + uint(floor( gl_FragCoord.y * pass.f3dGridHWW[slice].y ) * pass.f3dGridHWW[slice].z) + uint(floor( gl_FragCoord.x * pass.f3dGridHWW[slice].x ) * lightsPerCell); uint numLightsInGrid = texelFetch( f3dGrid, int(sampleOffset) ).x; for( uint i=0u; i<numLightsInGrid; ++i ) { //Get the light index uint idx = texelFetch( f3dGrid, int(sampleOffset + i + 1u) ).x; //Get the light vec4 posAndType = texelFetch( f3dLightList, int(idx) ); vec3 lightDiffuse = texelFetch( f3dLightList, int(idx + 1u) ).xyz; vec3 lightSpecular = texelFetch( f3dLightList, int(idx + 2u) ).xyz; vec3 attenuation = texelFetch( f3dLightList, int(idx + 3u) ).xyz; vec3 lightDir = posAndType.xyz - inPs.pos; float fDistance = length( lightDir ); if( fDistance <= attenuation.x ) { lightDir *= 1.0 / fDistance; float atten = 1.0 / (1.0 + (attenuation.y + attenuation.z * fDistance) * fDistance ); if( posAndType.w == 1.0 ) { //Point light vec3 tmpColour = BRDF( lightDir, viewDir, NdotV, lightDiffuse, lightSpecular ); finalColour += tmpColour * atten; } else { //spotParams.x = 1.0 / cos( InnerAngle ) - cos( OuterAngle ) //spotParams.y = cos( OuterAngle / 2 ) //spotParams.z = falloff //Spot light vec3 spotDirection = texelFetch( f3dLightList, int(idx + 4u) ).xyz; vec3 spotParams = texelFetch( f3dLightList, int(idx + 5u) ).xyz; float spotCosAngle = dot( normalize( inPs.pos - posAndType.xyz ), spotDirection.xyz ); float spotAtten = clamp( (spotCosAngle - spotParams.y) * spotParams.x, 0.0, 1.0 ); spotAtten = pow( spotAtten, spotParams.z ); atten *= spotAtten; if( spotCosAngle >= spotParams.y ) { vec3 tmpColour = BRDF( lightDir, viewDir, NdotV, lightDiffuse, lightSpecular ); finalColour += tmpColour * atten; } } } } vec3 reflDir = 2.0 * dot( viewDir, nNormal ) * nNormal - viewDir; float ambientWD = dot( pass.ambientHemisphereDir.xyz, nNormal ) * 0.5 + 0.5; float ambientWS = dot( pass.ambientHemisphereDir.xyz, reflDir ) * 0.5 + 0.5; vec3 envColourS = mix( pass.ambientLowerHemi.xyz, pass.ambientUpperHemi.xyz, ambientWD ); vec3 envColourD = mix( pass.ambientLowerHemi.xyz, pass.ambientUpperHemi.xyz, ambientWS ); float NdotL = clamp( dot( nNormal, reflDir ), 0.0, 1.0 ); float VdotH = clamp( dot( viewDir, normalize( reflDir + viewDir ) ), 0.0, 1.0 ); float fresnelS = material.F0.x + pow( 1.0 - VdotH, 5.0 ) * (1.0 - material.F0.x); finalColour += mix( envColourD * material.kD.xyz, envColourS * material.kS.xyz.xyz, fresnelS ); outColour.xyz = finalColour; outColour.w = 1.0; }
查看其中使用f3dGrid,f3dLightList的采樣器位置,光照思路如上生成,然后使用,根據當前像素深度得到對應層,然后得到對應格子的第一個數據,多少個光源,然后得到每個光源的屬性,算出光源影響。當然這段代碼也包含下面的陰影實現。
陰影
在Ogre2.1中,已經取消Ogre1.x存在很久的Shadow Volume陰影,可能是因為模型的Volume生成麻煩不說,性能也受影響,看下Ogre1.x中Entity的ShadowCaster類方法的實現就知道了,以及Shadow Volume最少要三次Pass.所以只保留了Shadow Mapping方法以及對應的改進技術如LiSPSM,PSSM,這些在Ogre1.9中就已經比較完善了,不過也有些不同,本文按照Ogre2.1中的代碼來說。
ConvexBody:封裝凸體模型與別的模型交互
ConvexBody用來表示一個凸體,其中每一個面用Polygon來表示(可以是三角形,四邊形等),如一個立方體,長方體,視截體都可以用6個面來表示。
其中ConvexBody::clip與plan的計算舉例來說下,把一個長方體用從中間一刀切下去,變成二個長方體,根據參數我們來確定用刀正面的長方體,還是反面的長方體,長方體是ConverBody,刀是Plan,一面是正面,一面是反面(ogre中頂點順序為逆時針是正面,否則是反面),其設計思路如下:ConvexBody表示凸體,Plane表示面A,先以凸體中的每個平面中的點與面A進行位置測試,判斷對應點是在面A的上面,下面,還是平面內,然后計算每條射線與面A相交情況,重新確定頂點。如下圖。
由P1P2組成的面切割長方體的一個面ABCD的情況,由面的紅線方法,我們知道AD在外面。
1 都在Plan的里面,如BC,這時只記錄C點。
2 由內到外與Plan相交,如CD與面相交P2,記錄P2點。
3 都在Plan的外面,如DA,不記錄
4 由外到內與Plan相交,如AB與面相交P1點,先記錄P1點,再記錄B點。
這樣ABCD變成CP2P1B,注意到,這二者還保持着逆時針的順序。還有上面把紅色箭頭替換一個方法,我們就得到的是AP1P2D了。
Ogre中的PSSM
PSSM的理論很多地方都有,理論也說的比較簡單。
http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html
簡單來說,把視截體分段,PSSM是Shadow Map的進階,如果不了解Shadow Map,請先了解Shadow Map,我以前寫過OpenGL 陰影之Shadow Mapping和Shadow Volumes ,我們知道Shadow Map容易產生鋸齒,一般我們只有提高陰影紋理的分辨率,當燈光與物體比較近時,也會產生鋸齒。
在開始說Ogre中的陰影前,先放出一段現在Ogre2.1中的陰影節點設置,對比上面的片斷着色器代碼,可以知道生成五張陰影紋理,其中三張是光源0以PSSM技術產生,第四張是光源1以Focused產生,第五張是光源2以Focused產生。

compositor_node_shadow ShadowMapDebuggingShadowNode { technique pssm num_splits 3 pssm_lambda 0.95 shadow_map 0 2048 2048 PF_D32_FLOAT light 0 split 0 shadow_map 1 1024 1024 PF_D32_FLOAT light 0 split 1 shadow_map 2 1024 1024 PF_D32_FLOAT light 0 split 2 technique focused shadow_map 4 2048 2048 PF_D32_FLOAT light 1 shadow_map 5 2048 2048 PF_D32_FLOAT light 2 shadow_map 0 1 2 4 5 { pass clear { colour_value 1 1 1 1 } pass render_scene { } } }
先看下默認陰影實現DefaultShadowCameraSetup,大致思路,如果是方向光,得到我們視截體大約中間的一個點,根據這個點與燈光方向確定陰影攝像機的位置,然后以平行投影的方式生成陰影視圖。聚光燈簡單的把透視投影的視截體FOV.而點光源只是簡單的根據攝像機中視截的一個點與燈光的位置來確定透視投影的方向。
其中針對大規模的陰影(平行光)的升級版,Ogre給出的方案是采用FocuseShadowCameraSetup,在前面的方案中,平行光只是大致根據視截體得到平行投影的攝像機位置,而在FocuseShadowCameraSetup,將精確計算平行光下什么位置的AABB能剛好包圍這個視截體。
FocuseShadowCameraSetup中的getShadowCamera最主要的思路就是用來確定上面的藍色框或是紅色框,得到視截體的八個點,然后換算到光源模型方向中,為什么了,因為我們知道AABB是與軸平行的,而最終的AABB是與光源方向平行的,如上圖,所以我們需要轉化到光源坐標軸上,就是以燈光的方向為Z軸,然后算出X軸,又反推出Y軸,這個具體過程想下視圖坐標系如何建立的就知道了,在這並不需要原點在光源上,只需要坐標軸是光源坐標軸。然后根據這八個點就可以求出來對應的AABB,就是如上框的藍框,但是我們可以看到,下面還有一段場景的ConverxBody與這個AABB相交的代碼,這次是為了去掉如上圖藍色框右邊面向燈的那條線,代替以場景的ConverxBody與之相交的新線,請看上面的ConverxBody的講解,這樣新生成的AABB,一般來說,最大值和原來的藍框一樣,而最少值大多時候只是Z值比原來藍框的大,有小部分情況XY值會縮小,這是因為藍框有燈光那部分跑到場景外面去了,然后把光源的位置放入如圖上位置,設置成平行投影,剛好把整個視截顯示下的窗口大小,這樣就精確了。
然后就是PSSMShadowCameraSetup,這個只是負責分了一下層,並不復雜,看下calculateSplitPoints就明白了,視截體本來就是個立方的梯形,分成幾層后還是立方的梯形,直接放到FocuseShadowCameraSetup,得到每層的框框就明白了,如上面的紅色框,你可以看到第二層的顯示,這樣就完成了PSSM的整個過程。上面的圖片確實經典,因為我當初看前面那個NV的PSSM的講解鏈接時,就有點沒搞清楚,NV的講解中,給出的燈光與視截體相交圖片中,其中夾角大多是大於等於90度,而最下面有一個人站在一個井旁的畫面,其中給出三張PSSM的圖,我就在想這不對啊,為啥PSSM3包含PSSM2,而PSSM2又包含了PSSM1,看到上面這圖,才想到原來是角度問題,我暈,在這種視線與光源方向相差比較小時,這種情況每張圖就有些包含關系,當然視線與光源成90時,NV的PSSM的講解鏈接最下面,就會把一個模型分成幾段不重復顯示在每個陰影圖上。
Ogre2.1中好像沒看到TSM,其中TSM只是更改投影矩陣,就能達到近的高質量,遠的質量低的效果,給個鏈接在這Trapezoidal Shadow Maps, http://www.comp.nus.edu.sg/~tants/tsm.html.
參考:
http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html