06 - Fake SSAO (Falso Screen-Space Ambient Occlusion)

Empezando con los efectos que se le pueden aplicar a cualquiera de nuestros renderizados, el Ambient Occlusion es un efecto que trata de simular las aportaciones de sombras que se producen por tener algún objeto cercano. El efecto es un poco 'caro' para realizarlo en aplicaciones a tiempo real por lo que se desarrolló un método llamado Screen-Space Ambient Occlusion que simula el Ambient Occlusion pero que se puede ejecutar en tiempo real. Ahora veremos un método para falsear el SSAO, que puede resultar bastante inexacto pero puede resultar útil en alguna situación.

Para ello necesitaremos guardar en una textura la profundidad lineal de la escena completa, para ello renderizaremos toda la escena (no hace falta aplicar texturas ni iluminación para este paso) desde el punto de vista de la cámara. Para ello usaremos Framebuffers con el DepthTest habilitado y usaremos, por ejemplo, los siguientes shaders:
//Vertex Shader
public static string SSAO01DepthVS = @"
            varying vec4 v_position;

         void main()
         {
           gl_Position = ftransform();

                    //view space position
           v_position = gl_ModelViewMatrix * gl_Vertex;
         }";
        //Fragment Shader
        public static string SSAO01DepthPS = @"
            varying vec4 v_position;
            uniform float cameraFarPlane;
 
         void main()
         {
          float depth = v_position.z / cameraFarPlane;
                depth = depth * 0.5 + 0.5;   //Don't forget to move away from unit cube ([-1,1]) to [0,1] coordinate system
          
          gl_FragColor = vec4(depth, 0.0, 0.0, 0.0);
         }";

Ahora necesitaremos pasarle al Shader encargado de calcular la oclusión esta profundidad renderizando un Quad que ocupe toda la pantalla.

El shader lo que hará es:

1.- Mirar la profundidad del píxel actual.
2.- Compararla con la profundidad de los pixeles que tenga alrededor.
3.- Decidir el factor de oclusión a aplicar.

Para ello se han preparado unos parámetros que tendremos que rellenar:
//fragment increment(1.0/width, 1.0/height)
            GL.Uniform2(GL.GetUniformLocation(xoShaderSSAO01Occlusion.Program, "texIncr"), 1f / (float)this.Width, 1f / (float)this.Height);

            //linear zbuffer: uniform sampler2D tex0;
            BindTexture(xoAOColorTexture.TextureId, TextureUnit.Texture0, "tex0", xoShaderSSAO01Occlusion.Program);

            //samples to take
            GL.Uniform1(GL.GetUniformLocation(xoShaderSSAO01Occlusion.Program, "iterations"), 3);

            //increment between samples
            GL.Uniform1(GL.GetUniformLocation(xoShaderSSAO01Occlusion.Program, "samIncr"), 1f);

            //sqrt(max occlusion)
            GL.Uniform1(GL.GetUniformLocation(xoShaderSSAO01Occlusion.Program, "maxOcc"), (float)System.Math.Sqrt(0.9));

En texIncr insertaremos el incremento en las coordenadas de texturas para poder navegar por ellas.
En tex0 introduciremos la textura de profundidad.
En iterations el número de veces que queremos que haga el sampleado de píxeles (cuanto más alto este número, mas se notará el efecto pero tardaremos más en realizar el efecto y bajarán los FPS).
En samIncr introduciremos el incremento de espacio entre iteraciones.
Y en maxOcc introduciremos la occlusion máxima que sufrirá un píxel, siendo 1.0 el máximo, pero aplicándole la raiz cuadrada para ahorrarle una operación al shader.

Los shaders para este proceso son:

//Vertex Shader
public static string SSAO02OcclusionVS = @"#version 120
            void main()
            {
                gl_Position =  gl_ModelViewProjectionMatrix * gl_Vertex;
                gl_TexCoord[0] = gl_MultiTexCoord0;
            }";
//Fragment Shader
        public static string SSAO01OcclusionPS = @"#version 120
            //linear zbuffer
            uniform sampler2D tex0;
            //fragment increment(1.0/width, 1.0/height)
            uniform vec2 texIncr;
            //iterations to do
            uniform int iterations;
            //increment between samples
            uniform float samIncr;
            //sqrt(max occlusion)
            uniform float maxOcc;

            //samples to take
            const vec2 samples[8] = vec2[8](
                vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(-1.0, 0.0), vec2(0.0, -1.0),
                vec2(1.0, 1.0), vec2(-1.0, 1.0), vec2(1.0, -1.0), vec2(-1.0, -1.0)
            ); 

            void main()
            {
                float zbu = texture2D(tex0, gl_TexCoord[0].xy).x;

                int hits = 0;

                for(int i=0; i < 8; i++)
                {
                    for(int j=0; j < iterations; j++)
                    {
                        float tempDepth = texture2D(tex0, gl_TexCoord[0].xy + (samples[i] * (float(j + 1) * samIncr) * texIncr)).x;
                    
                        if(zbu < tempDepth)
                        {
                            hits += (iterations - j);
                            break;
                        }
                    }
                }
                
                float occlusion = maxOcc * float(hits) / float(iterations * 8);
                occlusion = 1 - (occlusion * occlusion);
                gl_FragColor = vec4(occlusion, occlusion, occlusion, 1.0);
            }";

Al estar realizando las iteraciones, lo que se hace es seguir una linea imaginaria, si el primer punto cumple la condición, los siguientes también la cumplirán, porque detrás de una pared no nos importa que pueda haber, por eso está ahí el break.

Tan sólo quedaría renderizar la escena normalmente y luego aplicar un renderizado multiplicando por la textura que acabamos de crear.

El siguiente paso para conseguir un efecto más realista podría ser considerar las normales de los puntos a la hora de calcular la oclusión, pero vamos a dejar este efecto con la sencillez actual.

No hay comentarios:

Publicar un comentario