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:

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:

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:

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;

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.

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

• 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. – DMGregory Sep 20 '18 at 10:13

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);


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