1
\$\begingroup\$

I'm working on a 3-pass deferred lighting system for a voxel game, however I am having problems with pixelated lighting and ambient occlusion.

The first stage renders the color, position and normal of each pixel on the screen into separate textures. This part works correctly:

Base Rendering with no lighting

The second shader calculates an ambient occlusion value for each pixel on the screen and renders that to a texture. This part doesn't work correctly and is pixelated:

Base Rendering with Occlusion Applied

Raw occlusion data:

Raw Occlusion Data

The third shader uses the color, position, normal and occlusion textures to render the game scene onto the screen. The lighting in this stage is also pixelated:

Lighting

The SSAO (2nd pass) fragment shader comes from the www.LearnOpenGL.com tutorial for Screen Space Ambient Occlusion:

out float FragColor;

layout (binding = 0) uniform sampler2D gPosition; // World space position
layout (binding = 1) uniform sampler2D gNormal; // Normalised normal values
layout (binding = 2) uniform sampler2D texNoise;

uniform vec3 samples[64]; // 64 random precalculated vectors (-0.1 to 0.1 magnitude)
uniform mat4 projection;

float kernelSize = 64;
float radius = 1.5;

in vec2 TexCoords;

const vec2 noiseScale = vec2(1600.0/4.0, 900.0/4.0);

void main()
{
    vec4 n = texture(gNormal, TexCoords);

    // The alpha value of the normal is used to determine whether to apply SSAO to this pixel
    if (int(n.a) > 0)
    {
        vec3 normal = normalize(n.rgb);
        vec3 fragPos = texture(gPosition, TexCoords).xyz;
        vec3 randomVec = normalize(texture(texNoise, TexCoords * noiseScale).xyz);

        // Some maths. I don't understand this bit, it's from www.learnopengl.com
        vec3 tangent = normalize(randomVec - normal * dot(randomVec, normal));
        vec3 bitangent = cross(normal, tangent);
        mat3 TBN = mat3(tangent, bitangent, normal);

        float occlusion = 0.0;

        // Test 64 points around the pixel
        for (int i = 0; i < kernelSize; i++)
        {
            vec3 sam = fragPos + TBN * samples[i] * radius;

            vec4 offset = projection * vec4(sam, 1.0);
            offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;

            // If the normal's are different, increase the occlusion value
            float l = length(normal - texture(gNormal, offset.xy).rgb);
            occlusion += l * 0.3;
        }

        occlusion = 1 - (occlusion / kernelSize);
        FragColor = occlusion;
    }
}

The lighting and final fragment shader:

out vec4 FragColor;

in vec2 texCoords;

layout (binding = 0) uniform sampler2D gColor; // Colour of each pixel
layout (binding = 1) uniform sampler2D gPosition; // World-space position of each pixel
layout (binding = 2) uniform sampler2D gNormal; // Normalised normal of each pixel
layout (binding = 3) uniform sampler2D gSSAO; // Red channel contains occlusion value of each pixel

// Each of these textures are 300 wide and 2 tall.
// The first row contains light positions. The second row contains light colours.

uniform sampler2D playerLightData; // Directional lights
uniform sampler2D mapLightData; // Spherical lights

uniform float worldBrightness;

// Amount of player and map lights
uniform float playerLights;
uniform float mapLights;

void main()
{
    vec4 n = texture(gNormal, texCoords);

    // BlockData: a = 4
    // ModelData: a = 2
    // SkyboxData: a = 0;

    // Don't do lighting calculations on the skybox
    if (int(n.a) > 0)
    {
        vec3 Normal = n.rgb;
        vec3 FragPos = texture(gPosition, texCoords).rgb;
        vec3 Albedo = texture(gColor, texCoords).rgb;

        vec3 lighting = Albedo * worldBrightness * texture(gSSAO, texCoords).r;

        for (int i = 0; i < playerLights; i++)
        {
            vec3 pos = texelFetch(playerLightData, ivec2(i, 0), 0).rgb;

            vec3 direction = pos - FragPos;
            float l = length(direction);

            if (l < 40)
            {
                // Direction of the light to the position
                vec3 spotDir = normalize(direction);

                // Angle of the cone of the light
                float angle = dot(spotDir, -normalize(texelFetch(playerLightData, ivec2(i, 1), 0).rgb));

                // Crop the cone
                if (angle >= 0.95)
                {
                    float fade = (angle - 0.95) * 40;
                    lighting += (40.0 - l) / 40.0 * max(dot(Normal, spotDir), 0.0) * Albedo * fade;
                }
            }
        }

        for (int i = 0; i < mapLights; i++)
        {
            // Compare this pixel's position with the light's position
            vec3 difference = texelFetch(mapLightData, ivec2(i, 0), 0).rgb - FragPos;
            float l = length(difference);

            if (l < 7.0)
            {
                lighting += (7.0 - l) / 7.0 * max(dot(Normal, normalize(difference)), 0.0) * Albedo * texelFetch(mapLightData, ivec2(i, 1), 0).rgb;
            } 
        }

        FragColor = vec4(lighting, 1.0);
    }
    else
    {
        FragColor = vec4(texture(gColor, texCoords).rgb, 1.0);
    }
}

The size of each block face in the game is 1x1 (world space size). I have tried splitting these faces up into smaller triangles, as illustrated below, however there wasn't much visible difference.

More Triangles per face

My thoughts are:

  • The resolution of angles between pixels and light sources are not high enough, causing the banding lighting effect
  • I am not tessellating the triangles on the block faces enough.

How can I increase the resolution of the lighting and SSAO data to reduce these pixelated artifacts? Thank you in advance

\$\endgroup\$
  • \$\begingroup\$ Do you have mipmapping enabled on your normal gBuffer? Since your occlusion samples deliberately jump around randomly, this can make it look to the texture function like you're trying to draw a texture from a long way away, meaning it should drop to a smaller mip level to reduce texture aliasing. \$\endgroup\$ – DMGregory Sep 20 '18 at 10:13
2
\$\begingroup\$

I used to get pixelated light when precision of gBuffer textures was low. Use GL_RGB16F or GL_RGB32F as internal format for position. Position can be reconstructed from depth texture like this, so no need to save position and waste space in gBuffer:

vec3 worldSpaceFromDepth(in float depth) {
    float z = depth * 2.0 - 1.0;

    vec4 clipSpacePosition = vec4(uv0 * 2.0 - 1.0, z, 1.0);
    vec4 direct = pc.projectionCameraMatInverse * clipSpacePosition;
    return direct.xyz / direct.w;
}

Here is my gBuffer setup:

_depthComponentTexture = new Texture(_width, _height, GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_NEAREST);        
_worldNormalSpecPower = new Texture(_width, _height, GL_RGBA16F, GL_RGBA, GL_NEAREST);
_albedoSpecularIntensity = new Texture(_width, _height, GL_RGBA8, GL_RGBA, GL_NEAREST);
_3viewNormalSSAOShaderless = new Texture(_width, _height, GL_RGBA16F, GL_RGBA, GL_NEAREST);

deferred shading

\$\endgroup\$
  • \$\begingroup\$ Thank you! Changing my position buffer's internal format from rgb16f to rgb32f fixed it instantly :) \$\endgroup\$ – Mitchell Robinson Sep 20 '18 at 11:40
  • \$\begingroup\$ Its also faster to reconstruct position from depth than having multiple 32f textures. \$\endgroup\$ – some rand Sep 20 '18 at 12:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.