阴影映射(Shadow Map)的研究(四)
上一篇文章粗略地介绍了要实现OpenGL ES 2.0的阴影映射所需的知识难点,现在简略地说明一下:1、FBO;2、着色器;3、float的分拆以及组合。上篇文章虽然说已经成功地移植了来自Java编写的Android下阴影映射的效果,但这边采用的很大程度上是OpenGL原生代码编写的内容,接下来的目标是采用自Qt 5起就逐渐采用的Qt对OpenGL的封装类,用面向对象的思维来处理OpenGL对象,这样让代码更加优雅。
1、FBO
首先说一下FBO。在Qt中有QOpenGLFramebufferObject这个类可以实现。这个类自Qt 5.0开始启用,事实上主要对OpenGL的FramebufferObject进行了面向对象的封装。只需要指定size以及可选指定FBO的格式,就可以启用了。在渲染目标至FBO的时候,只需要在最开始指定
bool QOpenGLFramebufferObject::bind( )
即可,当将FBO的内容作为纹理的渲染到默认的Framebuffer的时候,只需要指定
bool QOpenGLFramebufferObject::bindDefault( )
以及
GLuint QOpenGLFramebufferObject::texture( ) const
即可。使用起来非常方便。
2、着色器
接下来说一下着色器,要写基于OpenGL ES 2.0的程序,着色器是不可避免要写了。在书写OpenGL ES2.0的着色器时,要注意顶点着色器与片断着色器的差异性。目前在渲染阴影映射的时候,牵涉到2 pass的渲染,也就是说要对正常的场景渲染两遍。第一遍是在以光源为视角对整个场景进行渲染,产生深度图;第二遍是以正常摄像机为视角进行渲染,在其中引入第一遍产生的深度图,根据深度图中的深度信息以及该片断在光照视角下所产生的深度信息作比较,来最终得出该片断是否被遮挡。通常是深度的渲染信息大于改片断在光源视角下的深度信息,即表示该片断被遮挡(这里牵涉到深度缓存的相关知识,详见我之前写的文章《深度缓存(Z缓存)的研究》和《阴影映射(Shadow
Map)的研究(二)》)。一旦确定被遮挡,那么使用较为深的颜色表示阴影。原理虽然简单,但是需要注意的一些陷阱还是很多的。下面是渲染深度图的顶点着色器以及片断着色器代码:
// Depth.vert #ifdef GL_ES precision highp float; #endif attribute vec3 position; uniform mat4 modelMatrix; uniform mat4 viewProjectionMatrix; varying vec4 projectedPosition; void main( void ) { vec3 finalPosition = position; projectedPosition = viewProjectionMatrix * modelMatrix * vec4( finalPosition, 1.0 ); gl_Position = projectedPosition; } // Depth.frag // 注意:这个着色器的是由 // http://www.codeproject.com/Articles/822380/Shadow-Mapping-with-Android-OpenGL-ES // 改编过来的。 #ifdef GL_ES precision highp float; #endif varying vec4 projectedPosition; vec4 pack( float depth ) { const vec4 bitSh = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 ); const vec4 bitMsk = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 ); vec4 comp = fract( depth * bitSh ); comp -= comp.xxyz * bitMsk; return comp; } void main( void ) { float normalizedZ = projectedPosition.z / projectedPosition.w; normalizedZ = ( normalizedZ + 1.0 ) / 2.0; gl_FragColor = pack( normalizedZ ); }
这里在片断着色器中,也可以采用gl_FragCoord.z来表示片断的深度(详见我的文章《有关GLSL中的gl_FragCoord》),但是在后来的测试中,我发现这样的精度会受到影响,而且久久得不出想要的阴影效果,于是我放弃了记录gl_FragCoord.z的方案,而是自己计算深度(注意:这里计算的方法和gl_FragCoord.z默认产生的方法不一样,具体来说,是透视除法计算的时机不同)。pack(
)函数采用的是普遍的一种将float打包成vec4的一种方案,下面的片断着色器也有解包的步骤。
下面是第二遍渲染的着色器代码:
// Common.vert // 属性变量 attribute vec3 position; attribute vec3 normal; attribute vec2 texCoord; uniform mat4 modelMatrix; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; uniform mat3 modelViewNormalMatrix; uniform mat4 lightViewProjectionMatrix; // 转换到varying中的 varying vec3 viewSpacePosition; varying vec2 v_texCoord; varying vec3 v_normal; varying vec4 v_shadowCoord; void main( void ) { viewSpacePosition = vec3( viewMatrix * modelMatrix * vec4( position, 1.0 ) ); v_texCoord = texCoord; v_normal = modelViewNormalMatrix * normal; v_shadowCoord = lightViewProjectionMatrix * modelMatrix * vec4( position, 1.0 ); gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 ); } // Common.frag uniform sampler2D texture; uniform sampler2D shadowTexture; uniform vec3 lightPosition; uniform mat4 viewMatrix; varying vec3 viewSpacePosition; varying vec2 v_texCoord; varying vec3 v_normal; varying vec4 v_shadowCoord; float unpack (vec4 colour) { const vec4 bitShifts = vec4(1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0, 1); return dot(colour , bitShifts); } float shadowSimple( ) { vec4 shadowMapPosition = v_shadowCoord / v_shadowCoord.w; shadowMapPosition = (shadowMapPosition + 1.0) /2.0; vec4 packedZValue = texture2D(shadowTexture, shadowMapPosition.st); float distanceFromLight = unpack(packedZValue); //add bias to reduce shadow acne (error margin) float bias = 0.0005; //1.0 = not in shadow (fragment is closer to light than the value stored in shadow map) //0.0 = in shadow return float(distanceFromLight > shadowMapPosition.z - bias); } void main( ) { vec3 viewSpaceLightPosition = vec3( viewMatrix * vec4( lightPosition, 1.0 ) ); vec3 lightVector = viewSpaceLightPosition - viewSpacePosition; lightVector = normalize( lightVector ); float NdotL = dot( v_normal, lightVector ); float diffuse = max( 0.0, NdotL ); float ambient = 0.3; float shadow = 1.0; if ( v_shadowCoord.w > 0.0 ) { shadow = shadowSimple( ); shadow = shadow * 0.8 + 0.2; } vec4 textureColor = texture2D( texture, v_texCoord ); gl_FragColor = ( textureColor * ( diffuse + ambient ) * shadow ); }
这里的顶点着色器,核心的内容是将“光源的投影矩阵 * 摄像机的视图矩阵* 模型矩阵 * 模型的顶点数据”作为阴影的坐标加以储存起来,在片断处理阶段,它主要用来为深度图采样用。同样的unpack( )函数和上述提到的pack( )函数是完全相反的操作,这一对函数可以将float数据以几乎不失真的代价还原出来。
3、效果
我将第一版程序经过多次重构使用Qt的方式重写了一遍,使用QOpenGLWidget作为渲染窗体,至少能够在Linux、Windows和Mac上顺利地运行,至于能否在移动平台上运行,还没有确定,不过接下来的一次重构,将阴影映射使用Qt Quick重写,就能够在移动平台上顺利地运行了。
操作方法:鼠标左键旋转中间的立方体,右键显示深度图。下面是深度图:
程序源代码:这里