4
\$\begingroup\$

EDIT

I have discovered that it does not seem to be the lighting calculation but the culling code because when i draw the lights without the culling it works perfectly.

I have been attempting to implement tile based deferred light culling using this as a guide. My current issue is that the lights don't seem to be drawing correctly. I have tested that the lights per tile are correct and they are and the light indices for each tile seem to be correct too. When I draw the scene I get allot of the tiles affected by point lights flickering every frame. I also dont think that the point lights are being rendered in there correct positions.

Here is what I am testing at the moment.

I construct the point light array to be sent to the compute shader.

for (int y = 32; y < 32; y++) {
    for (int x = 0; x < 32; x++) {
        pointLights[x + y * 32] = new PointLight(new Vector3f(x, y, 0);
    }
}

All of theses point lights are sent to the uniform array and the scene is drawn.

As you can see I am using 1024 lights but what I see is only one light affecting my 32 * 32 plane right in the center and when I test what tiles are affected by the light it is correct. These lit tiles also flicker every other frame.

So where did I go wrong Is it in my light culling code, my lighting functions, or something else completely.

Here is my complete compute shader

    #version 430

#define MAX_POINT_LIGHTS 1024
#define MAX_LIGHTS_PER_TILE 40
#define WORK_GROUP_SIZE 16

struct Attenuation
{
    float constant;
    float linear;
    float exponent;
};

struct Light
{
    vec3 color;
    float intensity;
};

struct DirectionalLight
{
    Light light;
    vec3 direction;
};

struct PointLight
{
    Light light;
    Attenuation atten;
    vec3 position;
    float radius;
};

vec4 calcLight(Light light, vec3 direction, vec3 normal, vec3 fragPos, vec3 specular, vec3 camPos)
{
    float diffuseFactor = dot(normal, -direction);

    vec4 diffuseColor = vec4(0.0, 0.0, 0.0, 0.0);
    vec4 specularColor = vec4(0.0, 0.0, 0.0, 0.0);

    if (diffuseFactor > 0)
    {
        // might need to be 0.0----------| |
        diffuseColor = vec4(light.color, 1.0) * light.intensity * diffuseFactor;

        vec3 directionToEye = normalize(camPos - fragPos);
        vec3 halfDirection = normalize(directionToEye - direction);

        float specularFactor = dot(halfDirection, normal);
        // maybe not 32?
        specularFactor = pow(specularFactor, 32);

        if (specularFactor > 0)
        {
            // maybe 0.0 again
            specularColor = vec4(light.color, 1.0) * vec4(specular, 1.0) * specularFactor;
        }
    }

    return diffuseColor + specularColor;
}

vec4 calcDirectionalLight(DirectionalLight dirLight, vec3 pos, vec3 normal, vec3 specular, vec3 camPos)
{
    return calcLight(dirLight.light, dirLight.direction, normal, pos, specular, camPos);
}

vec4 calcPointLight(PointLight pointLight, vec3 pos, vec3 normal, vec3 specular, vec3 camPos)
{
    vec3 lightDirection = pos - pointLight.position;
    float distanceToPoint = length(lightDirection);

    lightDirection = normalize(lightDirection);

    vec4 color = calcLight(pointLight.light, lightDirection, normal, pos, specular, camPos);

    //float atten = pointLight.atten.constant + pointLight.atten.linear * distanceToPoint + pointLight.atten.exponent * distanceToPoint * distanceToPoint;
    float atten = 1.0 / (pointLight.atten.constant + pointLight.atten.linear * distanceToPoint + pointLight.atten.exponent * (distanceToPoint * distanceToPoint));

    return color * atten;
}

layout (binding = 1, rgba32f) uniform image2D writeonly finalImage;
layout (binding = 2, rgba32f) uniform readonly image2D geometryPosition;
layout (binding = 3, rgba32f) uniform readonly image2D geometryDiffuse;
layout (binding = 4, rgba32f) uniform readonly image2D geometryNormal;

uniform int numActiveLights;
uniform PointLight pointLights[MAX_POINT_LIGHTS];
uniform Light ambientLight;
uniform DirectionalLight directionalLight;

uniform vec3 cameraPosition;
uniform vec2 resolution;

uniform mat4 projection;
uniform mat4 view;

shared uint minDepth;
shared uint maxDepth;
shared uint pointLightCount;
shared uint pointLightIndex[MAX_POINT_LIGHTS];

layout (local_size_x = WORK_GROUP_SIZE, local_size_y = WORK_GROUP_SIZE) in;

void main()
{
    minDepth = uint(0xFFFFFFFF);
    maxDepth = 0;
    pointLightCount = 0;
    barrier();

    ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);

    vec3 position = imageLoad(geometryPosition, pixel).xyz;
    float d = position.z;
    uint depth = uint(d * uint(0xFFFFFFFF));

    atomicMin(minDepth, depth);
    atomicMax(maxDepth, depth);

    barrier();

    float minDepthZ = float(minDepth / float(0xFFFFFFFF));
    float maxDepthZ = float(maxDepth / float(0xFFFFFFFF));

    vec2 tileScale = vec2(resolution * (1.0 / float(2 * WORK_GROUP_SIZE)));
    vec2 tileBias = tileScale - vec2(gl_WorkGroupID.xy);

    vec4 col1 = vec4(-projection[0][0] * tileScale.x, projection[0][1], tileBias.x, projection[0][3]);
    vec4 col2 = vec4(projection[1][0], -projection[1][1] * tileScale.y, tileBias.y, projection[1][3]);
    vec4 col4 = vec4(projection[3][0], projection[3][1], -1.0, projection[3][3]);

    //vec4 col1 = vec4(-projection[0][0] * tileScale.x, 0.0f, tileBias.x, 0.0f);
    //vec4 col2 = vec4(0.0f, -projection[1][1] * tileScale.y, tileBias.y, 0.0f);
    //vec4 col4 = vec4(0.0, 0.0, 1.0, 0.0);

    vec4 frustumPlanes[6];
    frustumPlanes[0] = col4 + col1;
    frustumPlanes[1] = col4 - col1;
    frustumPlanes[2] = col4 - col2;
    frustumPlanes[3] = col4 + col2;
    frustumPlanes[4] = vec4(0.0, 0.0, -1.0, -minDepthZ);
    frustumPlanes[5] = vec4(0.0, 0.0, -1.0, maxDepth);

    for (int i = 0; i < 4; i++)
    {
        frustumPlanes[i] *= 1.0 / length(frustumPlanes[i].xyz);
    }

    uint threadCount = WORK_GROUP_SIZE * WORK_GROUP_SIZE;
    uint passCount = (numActiveLights + threadCount - 1) / threadCount;

    for (int passIt = 0; passIt < passCount; ++passIt)
    {
        uint lightIndex = passIt * threadCount + gl_LocalInvocationIndex;
        lightIndex = min(lightIndex, numActiveLights);

        PointLight p = pointLights[lightIndex];
        vec4 pos = view * vec4(p.position, 1.0);
        float rad = p.radius;

        if (pointLightCount < MAX_LIGHTS_PER_TILE)
        {
            bool inFrustum = true;
            for (int i = 3; i >= 0 && inFrustum; i--)
            {
                float dist = dot(frustumPlanes[i], pos);
                inFrustum = (-rad <= dist);
            }

            if (inFrustum)
            {
                uint id = atomicAdd(pointLightCount, 1);
                pointLightIndex[id] = lightIndex;
            }
        }
    }

    barrier();

    vec3 normal = imageLoad(geometryNormal, pixel).xyz;
    vec4 diffuse = imageLoad(geometryDiffuse, pixel);
    vec3 specular = vec3(0.1, 0.1, 0.1);

    vec4 color = diffuse;
    color *= vec4((ambientLight.color * ambientLight.intensity), 1.0);
    color += calcDirectionalLight(directionalLight, position, normal, specular, cameraPosition);
    for (int i = 0; i < pointLightCount; i++)
    {
        color += calcPointLight(pointLights[pointLightIndex[i]], position, normal, specular, cameraPosition);
        //color = vec4(1.0f / pointLightCount);
    }

    barrier();

    imageStore(finalImage, pixel, color);
}
\$\endgroup\$
1
\$\begingroup\$

I had the same problem (flickering lights/visible tile boundaries due to incorrect culling), and the error was in frustum construction code. My 'engine' uses right-handed coordinate system (X - right, Y - forward, Z - up), while Direct3D demos use left-handed (Z-forward, Y-up), and my frustum planes had inverted normals. I simply added the minus sign and lighting is now working properly (no black flickering jaggies):

    // this creates the standard Hessian-normal-form plane equation from three points, 
    // except it is simplified for the case where the first point is the origin
    inline float4 CreatePlaneEquation( float4 b, float4 c )
    {
        float4 n;
        // normalize(cross( b.xyz-a.xyz, c.xyz-a.xyz )), except we know "a" is the origin
        n.xyz = -normalize(cross( b.xyz, c.xyz ));  //<= NOTE: added minus sign!
        // -(n dot a), except we know "a" is the origin
        n.w = 0;
        return n;
    }

And here's my tiled deferred shad code (HLSL Compute Shader). NOTE: AMD's Forward+ demo uses another algorithm to extract frustum planes.

// Implementation of tiled deferred shading.

// Input: G-Buffers, depth buffer and list of lights.
// Output: fully composited and lit non-MSAA HDR texture.

// References:
// https://github.com/vonture/DeferredRenderer/blob/master/doc/GDC11_DX11inBF3_Public.pdf

#include "__shader_globals_common.h"
#include "__tiled_shading_common.h"
#include "_transform.h"
#include "_gbuffer.h"
#include "_lighting.h"
#include "_screen_shader.h"
#include "_PS.h"

//-----------------------------------------------------------------------------
// Light culling constants: Parameters for the light culling shader.
//-----------------------------------------------------------------------------

#define MAX_LIGHTS_PER_TILE (1024)

// Tile dimensions determine the tile size for light binning and associated trade-offs.
#define TILE_SIZE_X (32)
#define TILE_SIZE_Y (32)

// number of threads in a thread group (each thread corresponds to one pixel)
#define THREADS_PER_TILE    (TILE_SIZE_X*TILE_SIZE_Y)

//-----------------------------------------------------------------------------------------
// Textures and Buffers
//-----------------------------------------------------------------------------------------

// all potentially visible point lights in the scene
StructuredBuffer< DeferredLight >   b_pointLights;

// output HDR target
RWTexture2D< float4 >   t_litTarget;

//-----------------------------------------------------------------------------------------
//  Group Shared Memory (aka local data share, or LDS)
//-----------------------------------------------------------------------------------------

// Min and Max depth for this tile for depth bounds testing:
groupshared uint gs_TileMinDepthInt;
groupshared uint gs_TileMaxDepthInt;

// Light list for the tile:
groupshared uint gs_TileLightIndices[MAX_LIGHTS_PER_TILE];
groupshared uint gs_TileNumLights;  // number of lights in this tile

//-----------------------------------------------------------------------------------------
//  Helper Functions
//-----------------------------------------------------------------------------------------

// this creates the standard Hessian-normal-form plane equation from three points, 
// except it is simplified for the case where the first point is the origin
inline float4 CreatePlaneEquation( float4 b, float4 c )
{
    float4 n;

    // normalize(cross( b.xyz-a.xyz, c.xyz-a.xyz )), except we know "a" is the origin
    // my view space is RIGHT handed, hence, the minus
    n.xyz = -normalize(cross( b.xyz, c.xyz ));

    // -(n dot a), except we know "a" is the origin
    n.w = 0;

    return n;
}

// point-plane distance, simplified for the case where 
// the plane passes through the origin (frustum side plane in view space)
inline float GetSignedDistanceFromPlane( float3 p, float4 eqn )
{
    // dot( eqn.xyz, p.xyz ) + eqn.w, except we know eqn.w is zero (see CreatePlaneEquation above).
    return dot( eqn.xyz, p.xyz );
}

void BuildFrustumPlanes(
    in uint2 groupId,   // 'Coordinates' of the thread group (x,y in range [0..TilesX-1, 0..TilesY-1]).
    in float nearClip, in float farClip,
    out float4 frustumPlanes[6] // plane equations for the four sides of the frustum, with the positive half-space outside the frustum
    )
{
    const uint numHorizontalTiles = g_tileCount.x;
    const uint numVerticalTiles = g_tileCount.y;

    // window width evenly divisible by tile width
    const float WW = (float) (TILE_SIZE_X * numHorizontalTiles);//<= NOTE: cast is important!
    // window height evenly divisible by tile height
    const float HH = (float) (TILE_SIZE_Y * numVerticalTiles);  //<= NOTE: must be float!

    // tile rectangle in viewport space
    const uint px_min = TILE_SIZE_X * groupId.x;    // [0 .. ScreenWidth - TILE_SIZE_X]
    const uint py_min = TILE_SIZE_Y * groupId.y;    // [0 .. ScreenHeight - TILE_SIZE_Y]
    const uint px_max = TILE_SIZE_X * (groupId.x + 1);
    const uint py_max = TILE_SIZE_Y * (groupId.y + 1);

    // scale to clip space
    const float px_left     = px_min / WW;
    const float px_right    = px_max / WW;
    const float py_top      = (HH - py_min) / HH;
    const float py_bottom   = (HH - py_max) / HH;

    // four corners of the tile in clip space, clockwise from top-left
    float4 frustumCornersCS[4];
    frustumCornersCS[0] = float4( px_left*2.f-1.f,  py_top*2.f-1.f,     1.f,    1.f );
    frustumCornersCS[1] = float4( px_right*2.f-1.f, py_top*2.f-1.f,     1.f,    1.f );
    frustumCornersCS[2] = float4( px_right*2.f-1.f, py_bottom*2.f-1.f,  1.f,    1.f );
    frustumCornersCS[3] = float4( px_left*2.f-1.f,  py_bottom*2.f-1.f,  1.f,    1.f );

    // four corners of the tile in view space
    float4 frustumCornersVS[4];
    frustumCornersVS[0] = ProjectPoint( g_inverseProjectionMatrix2, frustumCornersCS[0] );
    frustumCornersVS[1] = ProjectPoint( g_inverseProjectionMatrix2, frustumCornersCS[1] );
    frustumCornersVS[2] = ProjectPoint( g_inverseProjectionMatrix2, frustumCornersCS[2] );
    frustumCornersVS[3] = ProjectPoint( g_inverseProjectionMatrix2, frustumCornersCS[3] );

    // create plane equations for the four sides of the frustum,
    // with the positive half-space outside the frustum
    [unroll]
    for( uint i = 0; i < 4; i++ ) {
        frustumPlanes[i] = CreatePlaneEquation( frustumCornersVS[i], frustumCornersVS[(i+1)&3] );
    }

    // Near/far clipping planes in view space
    frustumPlanes[4] = float4( 0.0f,-1.0f, 0.0f, nearClip );
    frustumPlanes[5] = float4( 0.0f,+1.0f, 0.0f, -farClip );
}

// NOTE: expects plane equations for the sides of the frustum, with the positive half-space outside the frustum
inline bool FrustumIntersectsSphere( in float4 frustumPlanes[6], in float3 sphereCenter, in float sphereRadius )
{
    bool intersectsFrustum = true;
    [unroll]
    for( uint i = 0; i < 6 /*&& intersectsFrustum*/; ++i )
    {
        // plane normals point outside the frustum
        float dist = dot( frustumPlanes[i], float4( sphereCenter, 1.0f ) );
        // If it is on totally the positive half space of one plane we can reject it.
        intersectsFrustum = intersectsFrustum && ( dist <= sphereRadius );
    }
    return intersectsFrustum;
}

//-----------------------------------------------------------------------------------------
// Light culling shader
//-----------------------------------------------------------------------------------------

// called with Dispatch( groupsX, groupsY, 1 ) where groupsX = ceil( screen_width/TILE_SIZE_X ), etc.

[numthreads( NUM_THREADS_X, NUM_THREADS_Y, 1 )]
void ComputeShaderTileCS(
    uint3 groupId           : SV_GroupID,       // 'Coordinates' of the thread group.
    uint groupIndex         : SV_GroupIndex,    // The "flattened" index of a compute shader thread within a thread group.
    uint3 groupThreadId     : SV_GroupThreadID, // 'Relative coordinates' of a thread inside a thread group.
    uint3 dispatchThreadId  : SV_DispatchThreadID   // 'Absolute coordinates' of a thread as derived from the Dispatch() call.
    )
{
    // NOTE: This is currently necessary rather than just using SV_GroupIndex to work around a compiler bug on Fermi.
    groupIndex = groupThreadId.y * TILE_SIZE_X + groupThreadId.x;
    // flattened index of this thread within thread group
    const uint localThreadIndex = groupIndex;

    // texel coordinates of this pixel in the [0, textureWidth - 1] x [0, textureHeight - 1] range
    const uint2 globalCoords = dispatchThreadId.xy;


    // Compute screen/clip-space position.
    float2 screenSize;
    DepthTexture.GetDimensions( screenSize.x, screenSize.y );

    // NOTE: Mind Direct3D 11 viewport transform and pixel center!
    const float2 texCoords = (float2(globalCoords) + .5f) / screenSize;

    // here goes ya clip-space position:
    const float2 screenPosition = TexCoordsToClipPos( texCoords );

    float viewPosition_x = screenPosition.x * g_ProjParams2.x;
    float viewPosition_y = screenPosition.y * g_ProjParams2.y;
    float hardwareDepth = DepthTexture.Load( int3( globalCoords.x, globalCoords.y, 0 ) );
    // recover view-space depth
    float3 viewSpacePosition = float3( viewPosition_x, 1, viewPosition_y ) * HardwareDepthToInverseW( hardwareDepth );


    // Work out depth bounds for our samples
    float minZSample = g_DepthClipPlanes.y; // far plane initially
    float maxZSample = g_DepthClipPlanes.x; // near plane initially

    float viewSpaceDepth = viewSpacePosition.y;

    // Avoid shading skybox/background or otherwise invalid pixels
    bool validPixel = viewSpaceDepth >= g_DepthClipPlanes.x && viewSpaceDepth <  g_DepthClipPlanes.y;
    //[flatten] if (validPixel)
    {
        minZSample = min( minZSample, viewSpaceDepth );
        maxZSample = max( maxZSample, viewSpaceDepth );
    }

    // Initialize per-tile variables - shared memory light list and depth bounds.
    if( localThreadIndex == 0 )
    {
        gs_TileNumLights = 0;
        gs_TileMinDepthInt = 0x7F7FFFFF;    // largest normal single precision float (3.4028235e+38)
        gs_TileMaxDepthInt = 0;
    }

    // Block execution of all threads in a group
    // until all group shared accesses have been completed
    // and all threads in the group have reached this call.
    GroupMemoryBarrierWithGroupSync();
    {
        // Calculate min and max depth in the threadgroup/tile.
        // Atomics still aren't that great though, since they effectively serialize the memory access among all threads.
        // To avoid using them, you can use a parallel reduction instead. However a parallel reduction requires more shared memory, so it may not always be a win.

        // Only scatter pixels with actual valid samples in them
        //if (maxZSample >= minZSample)
        {
            // atomics only work on integers, but we can always cast float to int (depth is always positive)
            InterlockedMin( gs_TileMinDepthInt, asuint(minZSample) );
            InterlockedMax( gs_TileMaxDepthInt, asuint(maxZSample) );
        }
    }
    GroupMemoryBarrierWithGroupSync();

    // Calculate the min and max depth for this tile, 
    // to form the front and back of the frustum.

    float minTileZ = asfloat( gs_TileMinDepthInt );
    float maxTileZ = asfloat( gs_TileMaxDepthInt );

    // Construct frustum for this tile.

    // NOTE: This is all uniform per-tile (i.e. no need to do it per-thread) but fairly inexpensive
    // We could just precompute the frusta planes for each tile and dump them into a constant buffer...
    // They don't change unless the projection matrix changes since we're doing it in view space.
    // Then we only need to compute the near/far ones here tightened to our actual geometry.
    // The overhead of group synchronization/LDS or global memory lookup is probably as much as this
    // little bit of math anyways, but worth testing.

    // plane equations for the four sides of the frustum, with the positive half-space outside the frustum
    float4 frustumPlanes[6];
    BuildFrustumPlanes( groupId.xy, minTileZ, maxTileZ, frustumPlanes );


    // How many total lights?
    const uint numActiveLights = g_deferredLightCount.x;

    // Cull lights for this tile.

    // Loop over the lights and do a sphere vs. frustum intersection test.
    // Each thread now operates on a sample instead of a pixel.
    // (Detailed explanation here: http://www.gamedev.net/topic/625569-light-culling-in-a-compute-shader/?view=findpost&p=4944777)

    // process THREADS_PER_TILE lights in parallel

    for( uint lightIndex = localThreadIndex; lightIndex < numActiveLights; lightIndex += THREADS_PER_TILE )
    {
        float3  lightCenter = b_pointLights[ lightIndex ].positionAndRadius.xyz;
        float   lightRadius = b_pointLights[ lightIndex ].positionAndRadius.w;

        // test if sphere is intersecting or inside frustum

        if( FrustumIntersectsSphere( frustumPlanes, lightCenter, lightRadius ) )
        {
            // Append light to list:
            // do a thread-safe increment of the list counter 
            // and put the index of this light into the list.
            // Compaction might be better if we expect a lot of lights.
            uint writeOffset;
            InterlockedAdd( gs_TileNumLights, 1, writeOffset );
            gs_TileLightIndices[ writeOffset ] = lightIndex;
        }
    }

    // Sync threads
    GroupMemoryBarrierWithGroupSync();

    uint numLightsAffectingTile = gs_TileNumLights;

    // Only process onscreen pixels (tiles can span screen edges)
    //if( all( globalCoords.xy < g_FramebufferDimensions.xy ) )
    {
        SurfaceParameters surface = ReadSurfaceFromGBuffer( globalCoords );

        // RGB accumulated RGB HDR color, A: luminance for screenspace subsurface scattering
        float4 compositedLighting = 0;

        for( uint tileLightIndex = 0; tileLightIndex < numLightsAffectingTile; tileLightIndex++ )
        {
            const uint lightIndex = gs_TileLightIndices[ tileLightIndex ];
            const DeferredLight light = b_pointLights[ lightIndex ];

            compositedLighting.rgb += ComputeLighting( viewSpacePosition, surface, light );
        }

        t_litTarget[globalCoords.xy] = float4( compositedLighting.rgb, 1 );
    }
}
\$\endgroup\$

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.