Ogre2.1 灯光与阴影
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