Préparation aux entretiens en Graphisme Informatique
Questions d'entretien Développeur en Graphisme Informatique
15 questions d'entretien en graphisme informatique sélectionnées, groupées par niveau d'ancienneté. Utilisez-les pour réviser les fondamentaux, les compromis pratiques et le raisonnement de production de niveau senior.
1Décrivez le skinning par mélange linéaire (LBS) et comment les matrices osseuses sont appliquées à un sommet avec de multiples influences.
Le Skinning par Mélange Linéaire (LBS) est une technique de déformation géométrique utilisée pour animer des maillages 3D basés sur une hiérarchie squelettique sous-jacente. Dans l'animation squelettique, chaque os animé se déplace par rapport à sa configuration de référence (la pose d'attache). Pour transformer un sommet influencé par plusieurs os :
1. La position originale du sommet dans l'espace du maillage est transformée dans l'espace local de chaque os en la multipliant par la Matrice de Pose d'Attache Inverse de l'os ($B_i^{-1}$).
2. Le sommet est ensuite transformé de l'espace local de l'os vers l'espace de la pose animée actuelle en utilisant la Matrice Osseuse Animée de l'os ($M_i$). La transformation composite $S_i = M_i \cdot B_i^{-1}$ est la matrice de palette de skinning.
3. La position finale du sommet skiné est calculée comme la somme pondérée linéaire sur tous les os influents : $$v' = \sum_{i=1}^{k} w_i \cdot (M_i \cdot B_i^{-1} \cdot v)$$
où les poids scalaires des os $w_i$ doivent être normalisés (c'est-à-dire $\sum w_i = 1.0$). Sur le GPU, cela est typiquement exécuté dans le shader de sommets (ou une pré-passe de skinning par calcul) en récupérant la palette d'os précalculée à partir d'un buffer uniforme/structuré en utilisant les attributs d'indice d'os du sommet, et en mélangeant linéairement les positions. Les normales et les tangentes des sommets sont transformées en utilisant la partie rotationnelle de la matrice de skinning mélangée et sont re-normalisées.
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in uvec4 inBoneIndices; // Up to 4 bone influences
layout(location = 3) in vec4 inBoneWeights; // Normalized: sum to 1.0
layout(set = 0, binding = 0) uniform BonePalette {
mat4 boneMatrices[128]; // Pre-multiplied: M_i * B_i^-1
};
layout(location = 0) out vec3 outNormal;
void main() {
mat4 skinMatrix = inBoneWeights.x * boneMatrices[inBoneIndices.x] +
inBoneWeights.y * boneMatrices[inBoneIndices.y] +
inBoneWeights.z * boneMatrices[inBoneIndices.z] +
inBoneWeights.w * boneMatrices[inBoneIndices.w];
vec4 skinnedPosition = skinMatrix * vec4(inPosition, 1.0);
gl_Position = u_ViewProjection * skinnedPosition;
// Transform normal with rotational part of skinMatrix and normalize
outNormal = normalize(mat3(skinMatrix) * inNormal);
}
2Décrivez l'instanciation GPU (Graphics Processing Unit) et comment les données et les agencements par instance rendent le rendu de nombreux objets similaires efficace.
L'instanciation GPU est une technique de rendu qui dessine plusieurs copies de la même géométrie de base (partageant les tampons de sommets et d'indices) en un seul appel de tirage (draw call) (par exemple, `DrawIndexedInstanced` dans Direct3D ou `glDrawElementsInstanced` dans OpenGL), réduisant drastiquement la surcharge du pilote CPU (Central Processing Unit)-GPU et le nombre d'appels de tirage de l'API (Application Programming Interface).<br><br>Données par instance : Pour que les instances aient des apparences et des comportements distincts, des données par instance sont fournies, incluant couramment :<br>- **Données de transformation** : Matrice monde, ou position/rotation/échelle compactées.<br>- **Propriétés du matériau** : Teintes de couleur, décalages/échelles UV, ou indices d'ID de matériau.<br>- **Paramètres dynamiques** : Phase d'animation, décalages de lightmap, ou drapeaux de visibilité.<br><br>Agencements des données et méthodes d'accès :<br>1. **Tampons de sommets instanciés** : Un tampon de sommets dédié lié avec un taux d'avancement par instance (par exemple, `D3D11_INPUT_PER_INSTANCE_DATA`). Le GPU avance automatiquement le tampon par instance.<br>2. **`StructuredBuffer` / `Uniform Buffer` (SSBO / `Constant Buffer`)** : Les données d'instance sont téléchargées dans un tableau tampon, et le nuanceur de sommets (vertex shader) y accède en utilisant l'identifiant d'instance système intégré (`SV_InstanceID` en HLSL, `gl_InstanceID` en GLSL). Garder les données par instance compactes (par exemple, des matrices affines 3x4 ou position + quaternion au lieu de matrices 4x4 complètes, et des couleurs FP16/uint32 compactées) minimise la bande passante mémoire du GPU et optimise l'utilisation du cache.
3Expliquez le culling de frustum, le culling d'occlusion et le culling de faces arrière, et où chacun se produit généralement dans un moteur de rendu.
Le culling de frustum, le culling d'occlusion et le culling de faces arrière sont trois techniques de visibilité complémentaires qui permettent de rejeter les primitives non visibles à différentes étapes et granularités dans un pipeline de rendu :<br><br>1. **Culling de frustum** : Rejette la géométrie se trouvant complètement en dehors du frustum de vue de la caméra. Il est généralement effectué sur des volumes englobants grossiers (comme les AABBs (Axis-Aligned Bounding Boxes) ou les sphères englobantes) sur le CPU (Central Processing Unit) avant la soumission du tirage (draw submission), ou sur le GPU (Graphics Processing Unit) via des nuanceurs de calcul (compute shaders) dans les pipelines de rendu pilotés par le GPU.<br>2. **Culling d'occlusion** : Rejette les objets ou les primitives qui sont à l'intérieur du frustum mais cachés derrière d'autres géométries opaques. Il peut se produire sur le CPU (en utilisant la rastérisation logicielle ou la visibilité précalculée) ou sur le GPU (en utilisant des requêtes d'occlusion matérielles, des tests de tampon de profondeur Hi-Z de calcul GPU ou le culling de meshlet) avant la rastérisation complète.<br>3. **Culling de faces arrière** : Rejette les polygones individuels dont les normales de surface sont orientées loin de la caméra. Cela est traditionnellement effectué automatiquement par le matériel à fonction fixe pendant la configuration/rastérisation des triangles sur le GPU, basé sur l'ordre des sommets dans l'espace écran (screen-space winding order), bien que cela puisse également être évalué grossièrement sur les cônes normaux de cluster (par exemple, dans les nuanceurs de maillage (mesh shaders)).
struct Plane { glm::vec3 normal; float distance; };
struct Sphere { glm::vec3 center; float radius; };
bool isSphereInsideFrustum(const Sphere& sphere, const Plane frustumPlanes[6]) {
for (int i = 0; i < 6; ++i) {
// Signed distance from plane to sphere center
float dist = glm::dot(frustumPlanes[i].normal, sphere.center) + frustumPlanes[i].distance;
if (dist < -sphere.radius) {
return false; // Completely outside
}
}
return true; // Inside or intersecting
}
4Comment les courbes splines telles que Bézier, B-spline et Catmull-Rom génèrent-elles des chemins lisses ou des bandes de géométrie ?
Les courbes splines fournissent des formulations paramétriques $\mathbf{P}(t)$ pour définir des chemins 3D lisses, des trajectoires de caméra et des bandes de géométrie extrudées (telles que des rubans, des routes ou des tubes).
1. **Types et propriétés des courbes :**
* **Courbes de Bézier :** Formulées avec des polynômes de Bernstein. Elles n'interpolent que les points d'extrémité ; les points de contrôle intermédiaires définissent des poignées de tangente. Connecter des segments avec une continuité $C^1$ nécessite des poignées de tangente colinéaires.
* **B-splines :** Construites à l'aide de fonctions de base sur un vecteur de nœuds. Elles offrent un contrôle local et une haute continuité paramétrique ($C^2$ pour les cubiques), mais ne passent généralement pas par les points de contrôle intérieurs.
* **Splines de Catmull-Rom :** Une classe de splines interpolantes qui passent directement par tous les points de contrôle intérieurs tout en assurant automatiquement une continuité $C^1$, ce qui les rend idéales pour les chemins créés par l'utilisateur.
2. **Génération de chemins et de géométrie :**
* L'évaluation de la spline au paramètre $t$ produit la position $\mathbf{P}(t)$ et le vecteur tangent $\mathbf{T}(t) = \mathbf{P}'(t)$.
* Pour extruder des rubans ou des tubes 3D, un repère de coordonnées orthogonal (Normale $\mathbf{N}(t)$ et Binormale $\mathbf{B}(t)$) est nécessaire le long de la courbe.
* Les repères de Frenet-Serret standards échouent ou se retournent aux points d'inflexion où la courbure $\kappa = 0$. Pour éviter une torsion non naturelle du ruban, les **repères de transport parallèle (repères de Bishop)** propagent une orientation de référence en douceur le long de la courbe en minimisant la torsion rotationnelle.
5Qu'est-ce qu'un tampon de commandes (command buffer), et pourquoi l'enregistrement de commandes multithreadé est-il important dans les moteurs de jeux haut de gamme ?
Un tampon de commandes (ou liste de commandes dans Direct3D 12) est une structure de données en mémoire où les commandes graphiques, de calcul et de transfert — telles que la définition de l'état du pipeline, la liaison des descripteurs, l'émission des appels de dessin (draw calls) et l'enregistrement des barrières de pipeline — sont enregistrées sur le CPU pour une soumission ultérieure et une exécution asynchrone sur une file d'attente du GPU. L'enregistrement de commandes multithreadé est crucial dans les moteurs de jeux haut de gamme car la préparation des appels de dessin côté CPU, la liaison d'état et le culling ont traditionnellement été les principaux goulots d'étranglement. En supprimant les contraintes de contexte à thread unique, les API (Application Programming Interface) explicites permettent à un moteur de diviser une trame en tâches de rendu indépendantes réparties sur plusieurs threads de travail CPU. Par exemple, les passes d'ombres, les blocs de G-buffer et le post-traitement peuvent être enregistrés simultanément. Les API modernes facilitent cela via des tampons de commandes primaires et secondaires (Vulkan) ou des listes de commandes et des bundles (D3D12). Les tampons de commandes secondaires et les bundles permettent aux threads de travail d'enregistrer des sous-ensembles de commandes de dessin qui peuvent être exécutés à l'intérieur d'un tampon de commandes primaire sur le thread de soumission, maximisant l'utilisation des CPU multi-cœurs et minimisant les blocages de la file d'attente GPU.
6En quoi les constantes push ou les constantes racine diffèrent-elles des tampons uniformes/constantes, et quand faut-il les utiliser ?
Les constantes push (dans Vulkan) et les constantes racine (dans DirectX 12) offrent un mécanisme pour passer de petites quantités de données uniformes directement en ligne dans le tampon de commandes ou la signature racine, contournant ainsi la surcharge liée à l'allocation, la mise à jour et la liaison des ressources de tampons GPU (Graphics Processing Unit) basées sur des descripteurs. En revanche, les tampons uniformes (UBO) ou les tampons de constantes (CBO) sont pris en charge par des allocations de mémoire GPU dédiées qui sont liées au pipeline via des descripteurs, des tables de descripteurs ou des ensembles de descripteurs. Étant donné que les constantes push/racine sont intégrées directement dans le flux de commandes, elles sont idéales pour les données par tirage de haute fréquence qui changent fréquemment (telles que les matrices de transformation d'objets, les indices de matériaux/maillages, les valeurs de temps ou les décalages dynamiques). Cependant, elles ont des limites de taille strictes (par exemple, Vulkan garantit une limite minimale de seulement 128 octets, et l'espace de signature racine D3D12 est plafonné à 64 DWORD (Double Word) partagés avec les descripteurs racine et les tables). Les tampons uniformes/constantes doivent être utilisés lorsque la charge utile de données dépasse les limites de taille des constantes push, lorsque les données sont partagées entre plusieurs tirages (tels que les matrices de caméra/vue par image, l'éclairage de scène global ou les paramètres d'environnement), ou lorsqu'un stockage persistant entre les passes est nécessaire.
7Décrivez les espaces de coordonnées qu'un sommet traverse, de l'espace modèle à l'espace écran, dans un moteur de rendu en temps réel.
Dans un pipeline de rendu en temps réel, un sommet traverse généralement plusieurs espaces de coordonnées : l'espace modèle (local), l'espace monde, l'espace vue (caméra), l'espace de découpage (clip space), les coordonnées normalisées de l'appareil (NDC - Normalized Device Coordinates) et l'espace écran (viewport/fenêtre).
Le sommet commence dans l'espace modèle par rapport à l'origine locale de l'objet. La multiplication par la matrice Modèle/Monde le positionne et l'oriente dans l'espace monde partagé. La multiplication par la matrice de Vue le transforme en espace de vue, où la caméra est à l'origine et regarde dans une direction de vision standard. Ensuite, la multiplication par la matrice de Projection transforme les coordonnées en espace de découpage 4D $(x_c, y_c, z_c, w_c)$, où la géométrie est découpée par rapport au volume de vue. Après le découpage, le matériel à fonction fixe effectue la division perspective (en divisant $x_c, y_c, z_c$ par $w_c$) pour produire des coordonnées normalisées de l'appareil (NDC) 3D. Enfin, la transformation du viewport mappe les coordonnées NDC aux coordonnées de pixels 2D de l'espace écran et aux valeurs du tampon de profondeur.
8Comparez le skinning par mélange linéaire avec le skinning par quaternions duaux en termes de qualité de déformation, d'artefacts et de complexité d'ingénierie.
Le Skinning par Mélange Linéaire (LBS) et le Skinning par Quaternions Duaux (DQS) représentent deux approches distinctes de la déformation de maillage squelettique :
1. **Qualité de déformation et artefacts :**
* Le LBS (Linear Blend Skinning) calcule les sommets transformés par interpolation linéaire de matrices de transformation d'os. Bien que rapide, le LBS souffre d'une perte de volume lors de rotations et de torsions sévères, notamment l'artefact de l'« emballage de bonbon » où la géométrie cylindrique s'effondre le long de l'axe de torsion.
* Le DQS (Dual-Quaternion Skinning) représente les transformations rigides des os sous forme de quaternions duaux unitaires (combinant rotation et translation). Lorsqu'il est mélangé (par exemple, en utilisant le mélange linéaire dual), le DQS préserve naturellement le volume et élimine les artefacts de torsion en « emballage de bonbon ». Cependant, le DQS introduit ses propres artefacts, tels que le gonflement ou le pincement lors de flexions articulaires extrêmes.
2. **Complexité d'ingénierie et d'implémentation :**
* Le LBS prend nativement en charge les transformations affines complètes (translation, rotation et mise à l'échelle non uniforme ou cisaillement) à l'aide de pipelines de matrices 4x4 standard.
* Le DQS ne gère nativement que les transformations rigides. La gestion de la mise à l'échelle (en particulier la mise à l'échelle non uniforme) nécessite une déformation multipasses, une décomposition polaire ou une séparation échelle-cisaillement. De plus, le DQS exige une gestion de l'antipodalité lors du mélange (vérification des produits scalaires des quaternions duaux pour prendre le chemin de rotation le plus court et éviter le retournement/l'effondrement du maillage), ce qui rend les calculs de shader et le pipeline d'actifs plus complexes.
9Les personnages animés se déforment incorrectement uniquement sur certains maillages. Quelles données d'actif (asset) et de shader inspecteriez-vous ?
Lorsque les personnages animés se déforment incorrectement uniquement sur un sous-ensemble de maillages, le problème provient généralement d'inadéquations de données à travers la pipeline d'actifs (assets), l'agencement des sommets (vertex layout) ou les constantes de shader. Une inspection systématique devrait couvrir :
1. **Agencement des sommets et limites d'index des os :** Assurez-vous que les indices d'os des sommets (vertex bone indices) ne dépassent pas le nombre d'os du squelette ou ne débordent pas de leur type de données compacté (par exemple, en utilisant `uint8`/`ubyte4` lorsque le squelette a plus de 256 os, ce qui provoque un bouclage d'index).
2. **Normalisation des poids des os :** Vérifiez que la somme des poids des os par sommet est égale à 1.0. Des poids non normalisés entraînent le rétrécissement des sommets vers le squelette ou leur éloignement.
3. **Matrices de pose de liaison inverse (IBMs) :** Confirmez que les matrices de liaison inverse du maillage correspondent à la pose de repos du squelette et à son espace de coordonnées. Des poses de liaison non concordantes peuvent faire exploser le maillage ou le décaler incorrectement.
4. **Influences maximales par sommet :** Vérifiez si l'exportateur DCC (Digital Content Creation) a exporté plus d'influences d'os par sommet (par exemple, 8 influences) que ce que l'agencement du tampon de sommets (vertex buffer layout) ou le shader supporte (par exemple, 4 influences), entraînant la suppression de poids sans re-normalisation.
5. **Hiérarchie du squelette et indexation de la palette :** Validez que les mappages d'indices d'os dans le maillage correspondent à la palette de matrices d'os téléchargée vers les tampons constants/structurés.
struct SkinVertex {
float position[3];
uint8_t boneIndices[4];
uint8_t boneWeights[4]; // UNORM8
};
void ValidateMeshSkinData(const std::vector<SkinVertex>& vertices, uint32_t maxBoneCount)
{
for (size_t i = 0; i < vertices.size(); ++i)
{
const auto& v = vertices[i];
int weightSum = 0;
for (int b = 0; b < 4; ++b)
{
assert(v.boneIndices[b] < maxBoneCount && "Bone index exceeds palette size!");
weightSum += v.boneWeights[b];
}
assert(std::abs(weightSum - 255) <= 1 && "Bone weights do not normalize to 1.0!");
}
}
10Que sont les cibles de morphing (morph targets) ou les formes de mélange (blend shapes), et comment sont-elles combinées avec le skinning squelettique pour l'animation faciale ?
Les cibles de morphing (morph targets ou blend shapes) représentent des déformations géométriques stockées sous forme de décalages delta par sommet (positions delta, normales delta et éventuellement tangentes delta) par rapport à un maillage de pose de repos de base. Chaque cible de morphing est contrôlée par un poids scalaire (généralement de 0,0 à 1,0), et les attributs de sommet déformés sont calculés comme suit : `Morphed_Attribute = Base_Attribute + Sum(Weight_i * Delta_i)`.
Lors de la combinaison des cibles de morphing avec le skinning squelettique (par exemple, pour l'animation faciale) :
1. **Ordre d'évaluation :** Les deltas des cibles de morphing doivent être évalués dans l'espace modèle de la pose neutre/liée avant l'application du skinning squelettique.
2. **Passe de skinning :** Les positions et normales morphées sont ensuite transformées par les matrices osseuses du skinning squelettique. L'application du morphing avant le skinning garantit que les expressions faciales se déforment naturellement avec les rotations de la tête et des articulations de la mâchoire.
Du point de vue des performances et de la bande passante, le stockage et la lecture naïfs de copies complètes du maillage pour des dizaines de formes de mélange entraînent une forte pression sur la bande passante mémoire. Les implémentations pratiques stockent des deltas épars (uniquement les sommets non nuls), compressent les formats de delta (par exemple, FP16 ou entiers quantifiés), ou utilisent des pré-passes de compute shader GPU pour calculer les sommets morphés une seule fois avant plusieurs passes de rendu.
struct VertexInput {
float3 position : POSITION;
float3 normal : NORMAL;
uint4 boneIndices : BLENDINDICES;
float4 boneWeights : BLENDWEIGHT;
};
// 1. Accumulate morph deltas in local rest space
float3 morphedPos = input.position;
float3 morphedNorm = input.normal;
for (int i = 0; i < activeMorphCount; ++i) {
morphedPos += morphDeltasPos[i] * morphWeights[i];
morphedNorm += morphDeltasNorm[i] * morphWeights[i];
}
morphedNorm = normalize(morphedNorm);
// 2. Skin morphed geometry to world space
float4 skinnedPos = 0;
float3 skinnedNorm = 0;
for (int b = 0; b < 4; ++b) {
float4x4 boneMat = BoneMatrices[input.boneIndices[b]];
skinnedPos += mul(boneMat, float4(morphedPos, 1.0)) * input.boneWeights[b];
skinnedNorm += mul((float3x3)boneMat, morphedNorm) * input.boneWeights[b];
}
11Expliquez la sélection géométrique du niveau de détail (LOD), la simplification de maillage et les stratégies de transition qui équilibrent la stabilité visuelle, la préservation des attributs et les performances.
Le niveau de détail (LOD) géométrique optimise les performances de rendu en réduisant la complexité du maillage à mesure que les objets s'éloignent de la caméra, équilibrant ainsi la fidélité visuelle et la fréquence d'images.
1. **Sélection du LOD**: Les LODs doivent être sélectionnés en utilisant des métriques en espace écran (telles que le diamètre de la sphère englobante projetée, le pourcentage de hauteur d'écran ou l'erreur de pixel projetée) plutôt qu'une distance statique en espace monde pour tenir compte du champ de vision (FOV) de la caméra et des changements de résolution. Pour éviter une oscillation rapide entre les LODs aux limites de distance ('papillonnement LOD'), une hystérésis est appliquée en maintenant des seuils distincts pour le passage vers un niveau supérieur et vers un niveau inférieur.
2. **Simplification de Maillage**: La génération hors ligne repose généralement sur les métriques d'erreur quadratique (QEM) via des collapsus d'arêtes itératifs. Pour maintenir la qualité visuelle, les algorithmes de simplification doivent préserver les silhouettes de bordure et pénaliser la distorsion géométrique, ainsi que préserver les attributs de sommet (coutures UV, ruptures de normales, couleurs de sommet et poids de skinning) en incorporant des termes d'erreur d'attribut dans la métrique quadratique.
3. **Stratégies de Transition**: Pour éviter un 'saut' visuel abrupt, les moteurs utilisent:
- **Fondu enchaîné tramé / Pointillage écran** (Dithered Crossfading / Screen-Door Stippling): Rejette des pixels dans le *pixel shader* en utilisant un motif de tramage entrelacé (par exemple, la matrice de Bayer), réalisant une transition douce entre les LODs sans nécessiter de mélange alpha ou de briser l'*early-Z*.
- **Géomorphing**: Interpole les positions des sommets entre des maillages LOD adjacents sur le GPU sur une courte fenêtre de transition.
12Qu'est-ce que l'optimisation des tampons d'index ou des caches de sommets, et pourquoi l'ordre des triangles affecte-t-il l'efficacité du cache post-transformation ?
L'optimisation du cache de sommets (ou tampon d'index) réorganise les indices de triangles et les données de sommets dans un maillage pour maximiser les taux d'accès dans les caches de sommets matériels du GPU (Graphics Processing Unit). Les GPU ont deux caches de sommets principaux :
1. **Cache post-transformation** : Un petit cache FIFO (First-In, First-Out)/LRU (Least Recently Used) stockant les sorties transformées du nuanceur de sommets (positions, attributs). Lorsque des triangles adjacents partagent des sommets, référencer ces sommets de près dans le flux d'indices permet au GPU de réutiliser les sorties de nuanceur mises en cache au lieu d'exécuter le nuanceur de sommets plusieurs fois pour le même sommet.
2. **Cache pré-transformation** : Le cache mémoire L1/L2 du GPU pour les données brutes du tampon de sommets. Réorganiser les données du tampon de sommets pour correspondre à l'ordre de premier accès des indices optimisés maximise la localité spatiale et l'efficacité de la bande passante mémoire.
L'ordre des triangles détermine directement la séquence d'accès dans le cache post-transformation. Les algorithmes d'optimisation (tels que l'algorithme de Tom Forsyth ou Tipsify) attribuent des scores de réutilisation dynamiques aux sommets basés sur la valence et la position dans le cache, priorisant les triangles qui complètent les références restantes aux sommets récemment mis en cache pour minimiser le Taux Moyen d'Échecs de Cache (ACMR - Average Cache Miss Ratio).
float calculateVertexScore(int cachePosition, int remainingValence) {
if (remainingValence == 0) return -1.0f;
float score = 0.0f;
if (cachePosition >= 0) {
if (cachePosition < 3) {
score = 0.75f; // Recent vertex in cache (bonus for immediate reuse)
} else {
score = std::pow(1.0f - (cachePosition - 3) / 29.0f, 1.5f); // Gradual falloff
}
}
// Bonus for vertices with few remaining triangles (clearing valence faster)
score += 2.0f * std::pow(remainingValence, -0.5f);
return score;
}
13Quelles approches d'élimination des objets cachés (occlusion culling) évitent les blocages CPU-GPU et les apparitions incorrectes (popping) ?
Les requêtes d'occlusion matérielles traditionnelles provoquent des blocages de lecture synchrone CPU-GPU si le CPU attend les résultats de visibilité dans la même trame. Retarder les lectures d'une trame évite les blocages mais introduit une latence temporelle, causant des apparitions subites (popping) visibles lorsque les objets nouvellement visibles ne sont pas rendus immédiatement. Pour éviter à la fois les blocages CPU-GPU et le popping visuel, les architectures de production modernes utilisent :
1. **Élimination des objets cachés (Occlusion Culling) GPU en deux phases basée sur Hi-Z** : Le GPU teste les boîtes englobantes par rapport à une pyramide de profondeur Hi-Z (Hierarchical-Z) générée à partir de la trame précédente. Les objets connus pour être visibles sont dessinés dans la Phase 1 (générant la profondeur initiale de la trame actuelle). Les objets précédemment occlus sont retestés par rapport au tampon Hi-Z mis à jour de la trame actuelle dans la Phase 2 ; tous les objets nouvellement révélés sont rendus immédiatement avant l'éclairage et le post-traitement, éliminant le popping avec zéro lecture CPU.
2. **Rastérisation logicielle CPU** : Un tampon de profondeur à basse résolution est rastérisé purement sur des threads de travail CPU (utilisant SIMD) à partir de maillages d'occlusion simplifiés. Le CPU teste les boîtes englobantes de manière synchrone sans avoir besoin de requêtes GPU ni d'encourir de latence de transfert GPU-vers-CPU.
3. **Délimitation conservative et hystérésis temporel** : L'expansion des volumes englobants ou le retardement des rétrogradations de l'état de visibilité évitent l'élimination prématurée lors de mouvements rapides de la caméra.
// Phase 1: Render instances visible in the previous frame
[numthreads(64, 1, 1)]
void Phase1_CullCS(uint id : SV_DispatchThreadID) {
if (id >= totalInstances) return;
Instance inst = instances[id];
if (wasVisibleLastFrame[id] && TestHiZ(inst.bounds, prevFrameHiZ)) {
AppendDraw(phase1DrawBuffer, inst);
currentVisibility[id] = true;
}
}
// [Phase 1 draws -> depth buffer written -> Hi-Z updated for current frame]
// Phase 2: Test previously occluded objects against updated Hi-Z to avoid popping
[numthreads(64, 1, 1)]
void Phase2_CullCS(uint id : SV_DispatchThreadID) {
if (id >= totalInstances) return;
if (!currentVisibility[id] && TestHiZ(instances[id].bounds, currentFrameHiZ)) {
AppendDraw(phase2DrawBuffer, instances[id]);
currentVisibility[id] = true;
}
}
14Décrivez un pipeline typique de traitement d'actifs, du maillage créé aux tampons GPU d'exécution, incluant la génération de tangentes, la quantification, la validation et l'optimisation.
Un pipeline standard de traitement d'actifs transforme les maillages DCC (Digital Content Creation) bruts créés (FBX, glTF, USD) en formats binaires haute performance, prêts pour le GPU (Graphics Processing Unit), en passant par cinq étapes principales :
1. **Ingestion et Validation** : Le maillage source est nettoyé en supprimant les sommets dupliqués ou inutilisés, en écartant les triangles dégénérés/de surface nulle, en vérifiant la géométrie manifold, en gérant les NaN (Not a Number) et en divisant les maillages multi-matériaux en sous-maillages distincts.
2. **Génération de l'espace tangent** : Les tangentes et bitangentes sont calculées en utilisant des algorithmes standardisés (principalement MikkTSpace) pour garantir une parité visuelle avec les outils de 'normal baking'. Cela prend correctement en compte les coutures UV et les cartes UV miroir (stockage de l'orientation (handedness) dans tangent.w).
3. **Optimisation** : Les indices sont réorganisés pour l'efficacité du cache de sommets post-transformation (par exemple, Forsyth/Tipsify), les tampons de sommets sont réordonnés pour la localité de récupération des sommets pré-transformation, et des niveaux de détail (LOD) ou des meshlets sont générés.
4. **Quantification et regroupement d'attributs** : Les attributs de sommet sont quantifiés pour réduire l'empreinte mémoire et la bande passante mémoire : les positions en demi-flottants/unorm 16 bits ou entiers normalisés, les normales et tangentes en SNORM 8 bits ou encodages octaédriques (Oct16/Oct32), et les UV en flottants/unorm 16 bits. Les attributs peuvent être entrelacés (AoS - Array of Structures) ou divisés en plusieurs flux (SoA - Structure of Arrays, par exemple, position seulement pour les pré-passes de profondeur).
5. **Cuisson (Cooking) et sérialisation** : Les tampons, les volumes englobants (AABBs/sphères) et les tables LOD sont sérialisés dans des fichiers binaires plats ne nécessitant aucune correction de pointeur à l'exécution, permettant un téléchargement DMA (Direct Memory Access) rapide vers les tampons GPU via une mémoire de staging.
// Compress a float3 normal into 2D octahedral coordinates (8-bit SNORM each)
vec2 OctEncode(vec3 n) {
n /= (abs(n.x) + abs(n.y) + abs(n.z));
vec2 oct = (n.z >= 0.0) ? n.xy : (1.0 - abs(n.yx)) * sign(n.xy);
return oct * 0.5 + 0.5;
}
// Stored as 2x 8-bit unorm/snorm (2 bytes vs 12 bytes float3)
15Expliquez les meshlets, le filtrage de clusters, les shaders de maillage et les pipelines de micro-géométrie dense pour les grandes scènes statiques.
Les pipelines de meshlets et les architectures de micro-géométrie dense (telles que Nanite d'Unreal) remplacent les grands appels de dessin (draw calls) à mémoire tampon d'indices par de petits clusters géométriques délimités appelés 'meshlets'.
1. **Meshlets** : Un meshlet est un cluster de géométrie généralement limité à 32-128 sommets et jusqu'à 128-256 triangles. Chaque meshlet contient des indices de sommets locaux, des flux d'attributs et des données de délimitation précalculées (une sphère englobante et un cône de normales).
2. **Shaders de Maillage et d'Amplification** : Ils remplacent le pipeline à fonction fixe de sommets, d'assemblage de primitives et de shaders géométriques. Les shaders d'amplification (ou de tâche) évaluent le filtrage par frustum, l'occlusion et le cône de normales pour les faces arrière au niveau du cluster sur des groupes de meshlets. Les meshlets survivants dispatchent les shaders de maillage, où un groupe de threads (threadgroup) transforme coopérativement les sommets dans la mémoire partagée embarquée (LDS - Local Data Share) et produit directement les indices de primitives vers le rastériseur.
3. **Pipelines de Micro-Géométrie Dense** : La géométrie haute densité produit des triangles sous-pixel qui souffrent d'un quad-overdraw sévère (où le matériel standard rastérise des quads d'aide de 2x2 pixels, exécutant des shaders de pixels complets pour seulement 1 pixel couvert). Les systèmes modernes de micro-géométrie dense utilisent des structures hiérarchiques de Niveau de Détail (LOD) de clusters (DAGs) pour sélectionner dynamiquement les LODs de clusters, assurant des longueurs d'arête d'environ 1 pixel, et combinent souvent la rastérisation matérielle pour les grands polygones avec des rastériseurs logiciels personnalisés basés sur le calcul pour les micro-polygones sous-pixel.
#define MAX_VERTS 64
#define MAX_PRIMS 128
struct MeshletPayload { uint meshletIndices[32]; };
[outputtopology("triangle")]
[numthreads(32, 1, 1)]
void MainMS(
in uint gtid : SV_GroupThreadID,
in uint gid : SV_GroupID,
in payload MeshletPayload payloadData,
out vertices VertexOutput outVerts[MAX_VERTS],
out indices uint3 outIndices[MAX_PRIMS]
) {
uint meshletId = payloadData.meshletIndices[gid];
Meshlet m = meshlets[meshletId];
SetMeshOutputCounts(m.vertexCount, m.primitiveCount);
// Cooperatively transform vertices
for (uint v = gtid; v < m.vertexCount; v += 32) {
outVerts[v] = TransformVertex(m.vertexOffset + v);
}
// Output local triangle indices
for (uint p = gtid; p < m.primitiveCount; p += 32) {
outIndices[p] = GetMeshletTriangle(m.triangleOffset + p);
}
}