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 Answer 1

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\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .