
Soft shadows with VSM - Almost finished
Posted Tuesday, 17 April, 2012 - 18:56 by socdream inHi all!
I've ported and adapted Fabien Sanglard's Soft shadows with VSM from here.
But I still have some bugs in my code that I haven't had time yet to trace down but I'm willing to offer my port.
We need 3 shaders, 1 for the depth pass rendering from light point of view, the next for the blurring pass where we render a quad with the depth as a texture and the las for rendering the shadows from the camera point of view.
public static string Shadow01DepthVS = @" varying vec4 v_position; void main() { gl_Position = ftransform(); v_position = gl_Position; }"; public static string Shadow01DepthPS = @" varying vec4 v_position; void main() { float depth = v_position.z / v_position.w ; depth = depth * 0.5 + 0.5; //Don't forget to move away from unit cube ([-1,1]) to [0,1] coordinate system float moment1 = depth; float moment2 = depth * depth; // Adjusting moments (this is sort of bias per pixel) using partial derivative float dx = dFdx(depth); float dy = dFdy(depth); moment2 += 0.25*(dx*dx+dy*dy) ; gl_FragColor = vec4( moment1,moment2, 0.0, 0.0 ); }"; public static string Shadow02BlurVS = @" void main() { gl_Position = ftransform(); gl_TexCoord[0] = gl_MultiTexCoord0; }"; public static string Shadow02BlurPS = @" uniform vec2 ScaleU; uniform sampler2D textureSource; void main() { vec4 color = vec4(0.0); color += texture2D(textureSource, gl_TexCoord[0].st + vec2(-3.0 * ScaleU.x, -3.0 * ScaleU.y)) * 0.015625; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(-2.0 * ScaleU.x, -2.0 * ScaleU.y)) * 0.09375; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(-1.0 * ScaleU.x, -1.0 * ScaleU.y)) * 0.234375; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(0.0, 0.0)) * 0.3125; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(1.0 * ScaleU.x, 1.0 * ScaleU.y)) * 0.234375; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(2.0 * ScaleU.x, 2.0 * ScaleU.y)) * 0.09375; color += texture2D(textureSource, gl_TexCoord[0].st + vec2(3.0 * ScaleU.x, -3.0 * ScaleU.y)) * 0.015625; gl_FragColor = color; }"; public static string Shadow03RenderVS = @" // Used for shadow lookup varying vec4 ShadowCoord; //uniform mat4 matTex; void main() { ShadowCoord = gl_TextureMatrix[7] * gl_Vertex; //ShadowCoord = matTex * gl_Vertex; gl_Position = ftransform(); gl_FrontColor = gl_Color; }"; public static string Shadow03RenderPS = @" uniform sampler2D ShadowMap; varying vec4 ShadowCoord; vec4 ShadowCoordPostW; float chebyshevUpperBound(float distance) { // We retrive the two moments previously stored (depth and depth*depth) vec2 moments = texture2D(ShadowMap,ShadowCoordPostW.xy).rg; // Surface is fully lit. as the current fragment is before the light occluder if (distance <= moments.x) return 1.0 ; // The fragment is either in shadow or penumbra. We now use chebyshev's upperBound to check // How likely this pixel is to be lit (p_max) max(variance, 0.00002); float variance = moments.y - (moments.x * moments.x); variance = max(variance, 0.0012); float d = distance - moments.x; float p_max = variance / (variance + d*d); return p_max; } void main() { ShadowCoordPostW = ShadowCoord / ShadowCoord.w; // Moving from unit cube [-1,1] to [0,1] ShadowCoordPostW = ShadowCoordPostW * 0.5 + 0.5; float shadow = chebyshevUpperBound(ShadowCoordPostW.z); gl_FragColor = vec4(shadow) * gl_Color; //gl_FragColor = vec4(shadow, shadow, shadow, 1.0); }";
I load a cube as a test mesh to which I reffer as 'xoMesh', that's just a container class to manage vertex and index buffers.
We need to create the buffers and textures needed for rendering:
private Graphics.Shader xoShaderAuxTex7; private Graphics.Shader xoShaderShadow01Moments; private Graphics.Shader xoShaderShadow02Blur; private Graphics.Shader xoShaderShadow03ShadowMap; private float xiShadowMapCoef = 0.9f; private float xiBlurCoef = 0.5f; private uint xiFboLight; private Graphics.Texture xoDepthTexture; private Graphics.Texture xoColorTexture; private uint xiFboBlur; private Graphics.Texture xoFboBlurColorTexture; int shadowMapUniform; private void LoadShadow() { xoShaderShadow01Moments = new Graphics.Shader(Graphics.Shader.Shadow01DepthVS, Graphics.Shader.Shadow01DepthPS); xoShaderShadow02Blur = new Graphics.Shader(Graphics.Shader.Shadow02BlurVS, Graphics.Shader.Shadow02BlurPS); xoShaderShadow03ShadowMap = new Graphics.Shader(Graphics.Shader.Shadow03RenderVS, Graphics.Shader.Shadow03RenderPS); xoShaderAuxTex7 = new Graphics.Shader(Graphics.Shader.ShadowAuxTex7VS, Graphics.Shader.ShadowAuxTex7PS); //uniform shadowMapUniform = GL.GetUniformLocation(xoShaderShadow03ShadowMap.Program, "ShadowMap"); //Shadow FBO xoDepthTexture = new Graphics.Texture((int)(this.Width * xiShadowMapCoef), (int)(this.Height * xiShadowMapCoef), PixelInternalFormat.DepthComponent, OpenTK.Graphics.OpenGL.PixelFormat.DepthComponent, PixelType.UnsignedByte); xoColorTexture = new Graphics.Texture((int)(this.Width * xiShadowMapCoef), (int)(this.Height * xiShadowMapCoef), PixelInternalFormat.Rgb16f, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.Float); GL.GenFramebuffers(1, out xiFboLight); GL.BindFramebuffer(FramebufferTarget.FramebufferExt, xiFboLight); GL.FramebufferTexture(FramebufferTarget.FramebufferExt, FramebufferAttachment.DepthAttachmentExt, xoDepthTexture.TextureId, 0); GL.FramebufferTexture(FramebufferTarget.FramebufferExt, FramebufferAttachment.ColorAttachment0, xoColorTexture.TextureId, 0); FramebufferErrorCode doError = GL.CheckFramebufferStatus(FramebufferTarget.FramebufferExt); if (doError != FramebufferErrorCode.FramebufferCompleteExt) throw new Exception(); GL.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); //Blur FBO GL.GenFramebuffers(1, out xiFboBlur); GL.BindFramebuffer(FramebufferTarget.FramebufferExt, xiFboBlur); xoFboBlurColorTexture = new Graphics.Texture((int)(this.Width * xiShadowMapCoef * xiBlurCoef), (int)(this.Height * xiShadowMapCoef * xiBlurCoef), PixelInternalFormat.Rgb16f, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.Float); GL.FramebufferTexture(FramebufferTarget.FramebufferExt, FramebufferAttachment.ColorAttachment0, xoFboBlurColorTexture.TextureId, 0); doError = GL.CheckFramebufferStatus(FramebufferTarget.FramebufferExt); if (doError != FramebufferErrorCode.FramebufferCompleteExt) throw new Exception(); GL.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); }
The Shader and Texture classes are just containers to make things easier for myself.
Once, everything is created, we render the scene:
private void Render() { GL.BindFramebuffer(FramebufferTarget.FramebufferExt, xiFboLight); GL.UseProgram(xoShaderShadow01Moments.Program); GL.Viewport(0, 0, (int)(this.Width * xiShadowMapCoef), (int)(this.Height * xiShadowMapCoef)); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); //Setup matrices GL.MatrixMode(MatrixMode.Projection); Matrix4 perspective = xoLight.GetProjectionMatrix(this.Width, this.Height); GL.LoadMatrix(ref perspective); GL.MatrixMode(MatrixMode.Modelview); Matrix4 view = xoLight.ViewMatrix; GL.LoadMatrix(ref view); GL.CullFace(CullFaceMode.Front); DrawObjects(); GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); GL.CullFace(CullFaceMode.Back); //Save modelview/projection matrix into texture7 GL.MatrixMode(MatrixMode.Texture); GL.ActiveTexture(TextureUnit.Texture7); // concatenating all matrices into one. Matrix4 texView = xoLight.ViewMatrix * perspective; GL.LoadMatrix(ref texView); GL.MatrixMode(MatrixMode.Modelview); //blurShadowMap GL.BindFramebuffer(FramebufferTarget.FramebufferExt, xiFboBlur); GL.Viewport(0, 0, (int)(this.Width * xiShadowMapCoef * xiBlurCoef), (int)((float)this.Height * xiShadowMapCoef * xiBlurCoef)); GL.Arb.UseProgramObject(xoShaderShadow02Blur.Program); GL.Uniform2(GL.GetUniformLocation(xoShaderShadow02Blur.Program, "ScaleU"), 1f / ((float)this.Width * xiShadowMapCoef * xiBlurCoef), 0f); GL.Uniform1(GL.GetUniformLocation(xoShaderShadow02Blur.Program, "textureSource"), 0); GL.ActiveTexture(TextureUnit.Texture0); GL.BindTexture(TextureTarget.Texture2D, xoColorTexture.TextureId); //Preparing to draw quad GL.MatrixMode(MatrixMode.Projection); GL.LoadIdentity(); GL.Ortho(-this.Width / 2.0, this.Width / 2.0, -this.Height / 2.0, this.Height / 2.0, 1, 20); GL.MatrixMode(MatrixMode.Modelview); GL.LoadIdentity(); //Drawing quad GL.Translate(0f, 0f, -5f); GL.Begin(BeginMode.Quads); GL.TexCoord2(0f, 0f); GL.Vertex3(-this.Width / 2f, -this.Height / 2f, 0f); GL.TexCoord2(1f, 0f); GL.Vertex3(this.Width / 2f, -this.Height / 2f, 0f); GL.TexCoord2(1f, 1f); GL.Vertex3(this.Width / 2f, this.Height / 2f, 0f); GL.TexCoord2(0f, 1f); GL.Vertex3(-this.Width / 2f, this.Height / 2f, 0f); GL.End(); // Bluring vertically GL.BindFramebuffer(FramebufferTarget.FramebufferExt, xiFboLight); GL.Viewport(0, 0, (int)(this.Width * xiShadowMapCoef), (int)(this.Height * xiShadowMapCoef)); GL.Uniform2(GL.GetUniformLocation(xoShaderShadow02Blur.Program, "ScaleU"), 0.0f, 1.0f / ((float)this.Height * xiShadowMapCoef)); GL.BindTexture(TextureTarget.Texture2D, xoFboBlurColorTexture.TextureId); GL.Begin(BeginMode.Quads); GL.TexCoord2(0f, 0f); GL.Vertex3(-this.Width / 2f, -this.Height / 2f, 0f); GL.TexCoord2(1f, 0f); GL.Vertex3(this.Width / 2f, -this.Height / 2f, 0f); GL.TexCoord2(1f, 1f); GL.Vertex3(this.Width / 2f, this.Height / 2f, 0f); GL.TexCoord2(0f, 1f); GL.Vertex3(-this.Width / 2f, this.Height / 2f, 0f); GL.End(); // Now rendering from the camera POV, using the FBO to generate shadows GL.BindFramebuffer(FramebufferTarget.FramebufferExt, 0); GL.Viewport(0, 0, this.Width, this.Height); // Clear previous frame values GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); //Using the shadow shader xoShaderShadow03ShadowMap.Activate(); GL.Uniform1(shadowMapUniform, 7); GL.ActiveTexture(TextureUnit.Texture7); GL.BindTexture(TextureTarget.Texture2D, xoColorTexture.TextureId); BindTexture(xoColorTexture.TextureId, TextureUnit.Texture7, "ShadowMap", xoShaderShadow03ShadowMap.Program); GL.MatrixMode(MatrixMode.Projection); Matrix4 viewProj = xoCamera.ViewProjection; GL.LoadMatrix(ref viewProj); GL.MatrixMode(MatrixMode.Modelview); GL.LoadIdentity(); GL.CullFace(CullFaceMode.Back); DrawObjects(); }
The DrawObjets() function:
private void DrawObjects() { //Draw for (int i = 0; i < 30; i++) { for (int j = 0; j < 30; j++) { GL.PushMatrix(); GL.Translate((-15 + i) * 100f, 0f, (-15 + j) * 100f); GL.MatrixMode(MatrixMode.Texture); GL.ActiveTexture(TextureUnit.Texture7); GL.PushMatrix(); GL.Translate((-15 + i) * 100f, 0f, (-15 + j) * 100f); xoMesh.Render(); GL.PopMatrix(); GL.MatrixMode(MatrixMode.Modelview); GL.PopMatrix(); } } GL.PushMatrix(); GL.Scale(0.5f, 0.5f, 0.5f); GL.Translate(0f, 80f, 0f); GL.MatrixMode(MatrixMode.Texture); GL.ActiveTexture(TextureUnit.Texture7); GL.PushMatrix(); GL.Scale(0.5f, 0.5f, 0.5f); GL.Translate(0f, 80f, 0f); xoMesh.Render(); GL.PopMatrix(); GL.MatrixMode(MatrixMode.Modelview); GL.PopMatrix(); GL.PushMatrix(); GL.Scale(0.5f, 0.5f, 0.5f); GL.Translate(160f, 80f, 0f); GL.MatrixMode(MatrixMode.Texture); GL.ActiveTexture(TextureUnit.Texture7); GL.PushMatrix(); GL.Scale(0.5f, 0.5f, 0.5f); GL.Translate(160f, 80f, 0f); xoMesh.Render(); GL.PopMatrix(); GL.MatrixMode(MatrixMode.Modelview); GL.PopMatrix(); }
This function draws a floor made of cubes and then draws 2 cubes to cast shadows, you can see how we modify the texture matrix at the same time as the ModelView one to always get the correct coordinates.
I know this is quite bad explained but I didn't want to make this post too long. Feel free to ask for anything and correct my bugs that you will discover as soon as you get this code working.

