Article Index

Problème

L’éclairage – direct ou indirect – par des sources nombreuses est toujours problématiques en temps réel, car il induit une surcharge de la bande passante et (souvent) du calcul. Plusieurs techniques existent pour réduire les calculs, dont, notamment, le calcul des lumières uniquement en les zones qu’elles éclairent. Ceci peut être effectué en espace image (tiled rendering) ou en espace scène (clustered rendering). Le tiled rendering fait cependant un clustering plus approximatif des sources, et des cas particuliers existent où la technique devient non-bénéfique (dans des configurations où la profondeur varie trop sur une même tuile pour permettre de sélectionner efficacement les lumières affectantes).

Cette article propose une implémentation de l'article Clustered Deferred and Forward Shading, Olsson, Billeter, et Assarsson, 2012

Choix de la technique

Nous nous intéresserons donc au clustered shading ; la technique présentée dans Clustered Deferred and Forward Shading, Olsson, Billeter, et Assarsson, 2012 présente en effet quelques avantages sur le tiled rendering. La segmentation est, de par sa nature 3D, mieux adaptée, et permet d’éviter les variations de performance liées aux changements de vue. Les auteurs précisent que les pires cas sont également mieux gérés. La technique permet également de prendre d’autres paramètres de la scène en compte lors pour l’éligibilité des lumières, et en particulier les normales. Le nombre de lumières supporté annoncé sur papier est estimé à 1,000,000 de sources (ponctuelles). La gestion des ombres n’est, en revanche, pas plus prise en charge qu’avec du tiled rendering, dans le cadre many-lights.

Clustering

Pour la réalisation du many-light, plusieurs étapes de calcul ont été développées afin d’optimiser la répartition des données au sein des grilles de calculs parallèles, évidement en essayent au mieux de minimiser les caches miss et d’obtenir des programmes avec le moins de branche miss possibles. Dans cette optique, un premier programme établit un filtrage avec un test de frustum sur toutes les Lights, cela permet de créer un premier filtrage hautement parallèle sur toutes les lights, pour exclure toutes les lumières qui n’appartiennent pas à la grille de cluster et donc supprimer un test à l’étape suivante. C’est le rôle du premier compute shader. L’étape suivante consiste donc à itérer sur toutes les lumières visibles dans le frustum. Ce dernier étant découpé dans une grille en 3 dimensions dont la taille est restreinte par le matériel et donnée lors de l’instanciation. Chaque lumière est répartie dans la grille en fonction de sa position, cela permet de diminuer drastiquement le nombre de lumières à prendre en compte lors du shading.


Étapes de calcul de l’éclairage many-lights. Les deux premières constituent la phase de clustering, encompute shaders. La dernière représente le calcul de l’éclairage. Le SSBO mentionné est celui appelég_buffer(oug_texture) et qui contient les sources lumineuses. Il y en a plusieurs autres, cruciaux également

Critère de clustering L’un des critères les plus directs est la position. Elle sera considérée dans l’espace Camera. Le plus naturel serait de subdiviser le frustum uniformément, mais on obtiendrait des clusters très inégaux. Pour pallier à ce problème, on considère le logarithme de ces positions, ce qui offre des clusters répartis de manière plus égale. Il est également possible d’utiliser d’autres critères pour les clusters. Olsson, Billeter, et Assarsson proposent un clustering tenant compte de la normale d’un fragment, ce qui permet de potentiellement réduire encore le nombre de lumières affectant un même fragment. Le cas limite suivant doit être évité : il peut tout à fait arriver qu’un fragment soit affecté par toutes les lumières, si elles sont toutes concentrées en un point. Ce cas sera évité en instaurant une limite au nombre de sources affectant simultanément un point. Nous pensons à une limite située entre 128 et 1024 sources. Ce cas devrait arriver très peu en pratique, malgré tout, mais nous permet d’optimiser notre calcul.

Stockage et structure de données Pour chaque cluster il faut stocker, d’une manière ou d’une autre, les sources qu’il contient. Cette question est difficile à résoudre, notamment car elle demande de faire un compromis entre la latence induite par des indirections dans le cas d’une représentation indexée et la difficulté que constitue la recopie et le transfert des données sur GPU dans le cas d’une duplication. Après discussion, nous sommes arrivés à la conclusion que le système de cache du GPU amortirait probablement très bien la latence liée au déréférencement alors que la recopie des sources poserait de gros problèmes de bande passante pour un gain par rapport au déréférencement minime. Une représentation possible consiste à créer un index des lumières par cluster et à les regrouper dans une liste, de manière semblable à ce qui est fait en tiled shading dans Real-time Many-light Management and Shadows with Clustered Shading p.62.


Filtrage des lumières visibles

La première optimisation, consiste à diminuer le nombre de sources lumineuse global à prendre en compte, pour cela nous avons plusieurs informations à notre disposition. Premièrement grâce aux informations de la caméra, nous connaissons sont champs de vision, que l'ont peut modélisé par une pyramide (cf figure ci-dessous), c'est ce que l'on appelle le frustum. Ensuite nous avons différentes matrices pour passer entre les différents systèmes de coordonnés (scène, caméra, ...). Finalement nous connaissons la position de chaque lumières et sont rayon d'influence.

On peu donc faire un test de collision des lumières entre le frustum et la sphère d'influence de chaque lumières, si le test échoue, c'est que la lumière est en dehors du champ de vision. Sinon on stocke l'indice de la lumière dans une liste pour ne considéré que ces dernières plus tard (variable screenLights). Pour évité des calcules redondants dans les étapes suivantes, on peu sauvegarder quelques 'informations (ici NDCCoords de taille fixe) qui contient alors la position écran et le rayon d'influence de toutes les lumières visibles. Le code glsl ci-dessous en montre partiellement l'implémentation.

layoutscreenLights(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;

#include "ClusteredStruct.glsl"
#include "ClusteredCollision.glsl"
#include "ClusteredFrustum.glsl"

uniform mat4 projView;
uniform mat4 proj;
uniform mat4 view;

void main()
{
    vec4 frustum[6];
    FrustumFromMatrix(projView, frustum);
    
    vec4 worldLight = vec4(lightData[gl_GlobalInvocationID.x].pos4.xyz, 1);
    bool colliding = QuickSphereColliding(frustum, vec4(worldLight.xyz, lightData[gl_GlobalInvocationID.x].lightRadius));

    // If light affects any clusters on screen, send to next shader for allocation, 
    // else cull.
    
    if(colliding)
    {
        vec4 viewPos = projView * worldLight;
        vec4 ndcCoord = vec4(viewPos.xyz, lightData[gl_GlobalInvocationID.x].lightRadius);
        
        uint currentLightCount = atomicCounterIncrement(count);
        screenLights[currentLightCount] = int(gl_GlobalInvocationID.x);
        
        float w = 1.0f / viewPos.w;
        NDCCoords[currentLightCount] = ndcCoord * w;
    }
}

Assignation des lumières dans le cluster

 Après avoir supprimées les lumières invisibles, ont cherche à répartir ces lumières au sein d'un cluster en fonction de leurs positions 3d de l'espace écrans

  • La première étapes consiste à diviser le frustum de la camera en cluster, et on assigne un indice à chacun

  • La division du frustum vue de la camera (affichage des indices visible)

  • Ensuite on effectue un test de collision de sphère entre toutes les lumières et chacun des sous-frustum, s'il réussi on stocke la lumière dans une liste assignée au même cluster, on peu voir ici le nombre de lumière affectées (0=bleu, rouge=100)

layout(local_size_x = X_GRID_SIZE, local_size_y = Y_GRID_SIZE, local_size_z = Z_GRID_SIZE) in;
#include "ClusteredStruct.glsl"
#include "ClusteredFrustum.glsl"
#include "ClusteredCollision.glsl"

uniform mat4 projView;
uniform mat4 proj;
uniform mat4 view;

void main()
{
    uint xIndex = uint(gl_GlobalInvocationID.x);
    uint yIndex = uint(gl_GlobalInvocationID.y);
    uint zIndex = uint(gl_GlobalInvocationID.z);

    // linear adresse inside the cluser grid
    uint index = xIndex + (yIndex * GRID_SIZE.x) + (zIndex * GRID_SIZE.x * GRID_SIZE.y);

    uint lightsOnScreen = atomicCounter(count);
    int intersections = 0;
    
    for(int i = 0; i < lightsOnScreen; i++)
    {
        bool colliding = SphereColliding(cubePlanes[index], NDCCoords[i].xzyw);

        if(colliding)
        {
            int lightIndex = screenLights[i];
            tileLights[index][intersections] = lightIndex;
            intersections++;
        }
    }

    lightIndexes[index] = intersections;
}


Finalement pour le rendue

http://fsi-dpt-info.univ-tlse3.fr/master-igai/2017-g2/images/many-light.png

layout (location = 0) in vec3 in_position;
layout (location = 1) in vec3 in_texcoord;
layout (location = 2) in vec3 in_normal;
layout (location = 3) in vec3 in_tangent;
layout (location = 4) in vec3 in_viewVector;

#include "PBRStructs.glsl"
#include "ClusteredStruct.glsl"

uniform Transform transform;
uniform vec2 screenSize;

out vec4 fragColor;

int tile;

/**
 * return the number of light index inside the current tile
 */
uint light_count()
{
    return lightIndexes[tile];
}

/**
 * compute the light contributuon from a light source at the index inside the tile
 */
vec4 light_contribution_from(uint j)
{
    int lightIndex = tileLights[tile][j];
    
    vec3 lightPosition = lightData[lightIndex].pos4.xyz;
         lightPosition = vec3(transform.proj * transform.view * vec4(lightPosition, 1.0));
    float dist = length(lightPosition - in_position);

    //Diffuse
    vec3 viewDir = normalize(-in_position);
    vec3 lightDir = normalize(lightPosition - in_position);
    vec3 diffuse = max(dot(in_normal, lightDir), 0.0) * lightData[lightIndex].lightColour.rgb;

    //Specular
    vec3 halfDir = normalize(lightDir + viewDir);
    float specPower = pow(max(dot(in_normal, halfDir), 0.0), 500.0);
    vec3 specular = lightData[lightIndex].lightColour.rgb * specPower;

    //Attenuation
    float attenuation = 0;

    attenuation  = 1.0 - clamp(dist / lightData[lightIndex].lightRadius, 0.0, 1.0);
    attenuation *= lightData[lightIndex].intensity;

    diffuse *= attenuation;
    specular *= attenuation;

    return vec4(vec3(diffuse + specular), 0.0);
}

//
// Main part. To be removed in the benefit of Template.frag.glsl.
//

void main(void)
{
    //Transform screenspace coordinates into a tile index
    vec3 ScreenCoord = abs(vec3(gl_FragCoord.xy, in_position.z-1.0) / vec3(screenSize, FIXED_ZFAR));
    ivec3 GridCoord = ivec3(ScreenCoord * GRID_SIZE);

    // calculate the tile index corresponding to the cluster position
    tile = GridCoord.x + (GridCoord.y * GRID_SIZE.x) +  + (GridCoord.z * GRID_SIZE.x * GRID_SIZE.y);
    
    for(int j = 0; j < light_count(); j++)
        fragColor += light_contribution_from(j);
}

ont utilise les sources lumineuses en fonction de la position de fragment :

Copyrigth

Les résultats et les codes présenter ont été effectuer lors de mon projet de M2, et visais à ajouter certaines fonctionnalités au moteur graphique de l'IRIT, le RadiumEngine