socdream's picture

Soft shadows with VSM - Almost finished

Hi 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.

Inline Images
Example VSM OpenTK