2

I'm really struggling to fix an issue with my SSAO shader and could desperately use some help. Basically the shader seems to work on some objects, but looks really bad on others. From the below you can see the sphere looks correct, but the cube seems to be doing occlusion on normals that it shouldn't. Here is a screenshot:

Notice edges of cube

I am basing my shaders off of this tutorial: http://devmaster.net/posts/3095/shader-effects-screen-space-ambient-occlusion

In my render chain I render 2 render targets which get used in some post process effects later on. One of them stores the position and depth. The other stores the normals. Both targets are float textures.

Here is a screenshot of each of the steps.

Normals, Depth, Position

This is my position/depth shader:

Vertex shader:

varying vec4 vPosition;
.....
vPosition = mvPosition;

Frag Shader:

uniform float linearDepth; //Cam far - cam near
varying vec4 vPosition;
...
float ld = length(vPosition) / linearDepth;
gl_FragColor = vec4(vPosition.x, vPosition.y, vPosition.z, ld);

This is the normal shader:

Vertex shader:

varying vec3 vNormal;
...
vec3 transformedNormal = normalMatrix * objectNormal;
vNormal = transformedNormal;

Fragment shader:

gl_FragColor = vec4( normalize( vNormal ).xyz, 1.0);

Here is my SSAO fragment shader:

uniform sampler2D tDiffuse;    // The original scene texture
uniform sampler2D tPositions;    // View space position data
uniform sampler2D tNormals;    // View space normal vectors
uniform sampler2D tNoise;    // Normalmap to randomize the sampling kernel
uniform vec2 texelSize;

/// Occluder bias to minimize self-occlusion.
uniform float occluderBias;

/// Specifies the size of the sampling radius.
uniform float samplingRadius;

uniform float onlyAO;

/// <summary>
/// Ambient occlusion attenuation values.
/// These parameters control the amount of AO calculated based on distance
/// to the occluders. You need to play with them to find the right balance.
///
/// .x = constant attenuation. This is useful for removing self occlusion. When
///         set to zero or a low value, you will start to notice edges or wireframes
///         being shown. Typically use a value between 1.0 and 3.0.
///
///    .y = linear attenuation. This provides a linear distance falloff.
/// .z = quadratic attenuation. Smoother falloff, but is not used in this shader.
/// <summary>
uniform vec2 attenuation;


/// <summary>
/// Varying variables.
/// <summary>
varying vec2 vUv;

/// <summary>
/// Sample the ambient occlusion at the following UV coordinate.
/// <summary>
/// <param name=srcPosition>3D position of the source pixel being tested.</param>
/// <param name=srcNormal>Normal of the source pixel being tested.</param>
/// <param name=uv>UV coordinate to sample/test for ambient occlusion.</param>
/// <returns>Ambient occlusion amount.</returns>
float SamplePixels (vec3 srcPosition, vec3 srcNormal, vec2 uv)
{
    // Get the 3D position of the destination pixel
    vec3 dstPosition = texture2D(tPositions, uv).xyz;

    // Calculate ambient occlusion amount between these two points
    // It is simular to diffuse lighting. Objects directly above the fragment cast
    // the hardest shadow and objects closer to the horizon have minimal effect.
    vec3 positionVec = dstPosition - srcPosition;
    float intensity = max(dot(normalize(positionVec), srcNormal) - occluderBias, 0.0);

    // Attenuate the occlusion, similar to how you attenuate a light source.
    // The further the distance between points, the less effect AO has on the fragment.
    float dist = length(positionVec);
    float attenuation = 1.0 / (attenuation.x + (attenuation.y * dist));

    return intensity * attenuation;
}


/// <summary>
/// Fragment shader entry.
/// <summary>
void main ()
{
    // Get position and normal vector for this fragment
    vec3 srcPosition = texture2D(tPositions, vUv).xyz;
    vec3 srcNormal = texture2D(tNormals, vUv).xyz;
    vec2 randVec = normalize(texture2D(tNoise, vUv).xy * 2.0 - 1.0);
    float srcDepth = texture2D(tPositions, vUv).w;

    // The following variable specifies how many pixels we skip over after each
    // iteration in the ambient occlusion loop. We can't sample every pixel within
    // the sphere of influence because that's too slow. We only need to sample
    // some random pixels nearby to apprxomate the solution.
    //
    // Pixels far off in the distance will not sample as many pixels as those close up.
    float kernelRadius = samplingRadius * (1.0 - srcDepth);

    // Sample neighbouring pixels
    vec2 kernel[4];
    kernel[0] = vec2(0.0, 1.0);        // top
    kernel[1] = vec2(1.0, 0.0);        // right
    kernel[2] = vec2(0.0, -1.0);    // bottom
    kernel[3] = vec2(-1.0, 0.0);    // left

    const float Sin45 = 0.707107;    // 45 degrees = sin(PI / 4)

    // Sample from 16 pixels, which should be enough to appromixate a result. You can
    // sample from more pixels, but it comes at the cost of performance.
    float occlusion = 0.0;
    for (int i = 0; i < 4; ++i)
    {
        vec2 k1 = reflect(kernel[i], randVec);
        vec2 k2 = vec2(k1.x * Sin45 - k1.y * Sin45,
                       k1.x * Sin45 + k1.y * Sin45);
        k1 *= texelSize;
        k2 *= texelSize;

        occlusion += SamplePixels(srcPosition, srcNormal, vUv + k1 * kernelRadius);
        occlusion += SamplePixels(srcPosition, srcNormal, vUv + k2 * kernelRadius * 0.75);
        occlusion += SamplePixels(srcPosition, srcNormal, vUv + k1 * kernelRadius * 0.5);
        occlusion += SamplePixels(srcPosition, srcNormal, vUv + k2 * kernelRadius * 0.25);
    }

    // Average and clamp ambient occlusion
    occlusion /= 16.0;
    occlusion = clamp(occlusion, 0.0, 1.0);

// Blend the two 
vec3 colour =  texture2D(tDiffuse, vUv).xyz;
//colour = clamp(colour - occlusion, 0.0, 1.0);

occlusion = 1.0 - occlusion;

//gl_FragColor.xyz = pow(colour, vec3(1.0 / 2.2));
if ( onlyAO == 1.0 )
    gl_FragColor.xyz = vec3( occlusion, occlusion, occlusion );
else if ( onlyAO == 2.0 )
    gl_FragColor.xyz = vec3( srcNormal.x, srcNormal.y, srcNormal.z );
else if ( onlyAO == 3.0 )
    gl_FragColor.xyz = vec3( srcDepth, srcDepth, srcDepth );
else
{
    //// Blend the two 
    //colour = clamp(colour - occlusion, 0.0, 1.0); 

    //// Apply gamma correction 
    //gl_FragColor.xyz = pow(colour, vec3(1.0 / 2.2)); 
    //gl_FragColor.w = 1.0;

    gl_FragColor.xyz = colour * occlusion;
}

gl_FragColor.w = 1.0;

}
genpfault
  • 51,148
  • 11
  • 85
  • 139
Mat
  • 961
  • 12
  • 28
  • Your normals buffer doesn't look okay where the cube is. There should be only three colors on the cube, one for each side, greenish for up, redish for right side and blue-ish for front side. Fix that. – Dragan Okanovic Aug 27 '13 at 10:47
  • Thanks Abstract Algorith. I thought it was probably the normals. I've been scouring the net for the right way to do it but I can't find anything that seems to work. Would you happen to know what the shader might be? I based the normal shader off the THREE normal shader. I.e vNormal = normalize( normalMatrix * normal ); Except I used the transformedNormal as the objects might have morphs or skin. – Mat Aug 27 '13 at 10:59
  • 1
    It looks okay. Maybe `normalMatrix` is wrong? Try multiplying normal with MVMatrix that has no scaling and no translation. Also, there is option in Three.js - THREE.FlatShading mode for `shading` param of the material, maybe that could help a bit. – Dragan Okanovic Aug 27 '13 at 11:04
  • Hi Abstract Algorithm; after some testing it does indeed seem to be the shading. When I use Flat shading it looks much better. Do you know how I can overcome this though? I have a scene with multiple objects that require smooth normals. I can't just apply the normal material without having to re-generate the normals each time. I.e. the material.shading = THREE.FlatShading won't work unless I re-generate the normals each pass (the scene switches from phong to normal shader) – Mat Aug 27 '13 at 12:15
  • There must be a way around not to re-gen normals each time. I am unsure how it could be done with Three.js. Also, use fewer passes, you can store normals+depth in one pass, for example. For re-gen, @WestLangley might be able to help. – Dragan Okanovic Aug 27 '13 at 12:36
  • Thanks Again. I'm gonna have to find a way to work around the shading issue. Do you want to answer the post with the Flat shading recommendation? – Mat Aug 27 '13 at 13:10
  • Hey mat, sorry this is off-topic but as I was looking into SSAO myself and atm using the built-in effect but not that happy with it, I was hoping if you could share some inside stuff from three.js. As i cannot contact you via SO, maybe you can send me an email with "hallo" to guygood at gmx dot de? I would answer there or maybe you are active in the three.js irc chat? Anyway, thanks in advance – GuyGood Aug 28 '13 at 07:36
  • Sure thing. Will send you an email – Mat Sep 03 '13 at 10:58

1 Answers1

2

It looks like normals aren't perpendicular to the surface of the cube, that might be the issue coming from shading param of the material.

Try setting THREE.FlatShading param for shading option for material.

Dragan Okanovic
  • 7,509
  • 3
  • 32
  • 48