Preparazione al colloquio Computer Graphics

Domande per Colloqui Computer Graphics Developer

15 domande selezionate per colloquio di computer grafica, raggruppate per livello di seniority. Usale per ripassare i fondamenti, i compromessi pratici e il ragionamento di produzione a livello senior.

Avvia un Colloquio AI su Computer GraphicsNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande Junior

1Descrivi il linear blend skinning e come le matrici ossee vengono applicate a un vertice con influenze multiple.

Il Linear Blend Skinning (LBS) è una tecnica di deformazione geometrica utilizzata per animare mesh 3D basate su una gerarchia scheletrica sottostante. Nell'animazione scheletrica, ogni osso animato si muove rispetto alla sua configurazione di riferimento (la bind pose). Per trasformare un vertice influenzato da più ossa: 1. La posizione originale del vertice nello spazio della mesh viene trasformata nello spazio locale di ciascun osso moltiplicandola per la Matrice di Inverse Bind Pose dell'osso ($B_i^{-1}$). 2. Il vertice viene quindi trasformato dallo spazio locale dell'osso nello spazio della posa animata corrente utilizzando la Matrice dell'Osso Animato ($M_i$). La trasformazione composita $S_i = M_i \cdot B_i^{-1}$ è la matrice della palette di skinning. 3. La posizione finale del vertice skinned viene calcolata come la somma pesata lineare di tutti gli ossi influenti: $$v' = \sum_{i=1}^{k} w_i \cdot (M_i \cdot B_i^{-1} \cdot v)$$ dove i pesi scalari degli ossi $w_i$ devono essere normalizzati (cioè, $\sum w_i = 1.0$). Sulla GPU (Graphics Processing Unit), questo viene tipicamente eseguito nel vertex shader (o in un pre-pass di skinning tramite compute shader) recuperando la palette di ossa precalcolata da un buffer uniforme/strutturato utilizzando gli attributi dell'indice dell'osso del vertice, e miscelando linearmente le posizioni. Le normali e le tangenti dei vertici vengono trasformate utilizzando la parte rotazionale della matrice di skinning miscelata e rinormalizzate.

#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);
}
Prova a rispondere a questa domanda con un coach IA

2Descrivi l'instancing della GPU (Graphics Processing Unit) e quali dati e disposizioni per istanza rendono efficiente il rendering di molti oggetti simili.

L'instancing su GPU (Graphics Processing Unit) è una tecnica di rendering che disegna molteplici copie della stessa geometria di base (condividendo buffer di vertici e indici) in una singola draw call (ad esempio, `DrawIndexedInstanced` in Direct3D o `glDrawElementsInstanced` in OpenGL), riducendo drasticamente l'overhead del driver CPU (Central Processing Unit)-GPU e il numero di draw call dell'API. Dati per Istanza: Per garantire che le istanze appaiano e si comportino in modo distinto, vengono forniti dati per istanza, comunemente inclusi: - Dati di trasformazione: Matrice World, o posizione/rotazione/scala impacchettata. - Proprietà del materiale: Tinte di colore, offset/scale UV, o indici ID materiale. - Parametri dinamici: Fase di animazione, offset lightmap, o flag di visibilità. Disposizioni dei Dati e Metodi di Accesso: 1. Buffer di Vertici Istanzati: Un buffer di vertici dedicato legato con un tasso di incremento per istanza (ad esempio, `D3D11_INPUT_PER_INSTANCE_DATA`). La GPU avanza automaticamente il buffer per istanza. 2. `StructuredBuffer` / `Uniform Buffer` (SSBO / Constant Buffer): I dati dell'istanza vengono caricati in un array di buffer, e lo shader dei vertici indicizza al suo interno usando l'identificatore di istanza di sistema integrato (`SV_InstanceID` in HLSL, `gl_InstanceID` in GLSL). Mantenere i dati per istanza compatti (ad esempio, matrici affini 3x4 o posizione + quaternione invece di matrici 4x4 complete, e colori FP16/uint32 impacchettati) riduce al minimo la larghezza di banda della memoria della GPU e ottimizza l'utilizzo della cache.

struct InstanceData {
    float4x4 worldMatrix;
    float4   colorTint;
};

StructuredBuffer<InstanceData> gInstanceData : register(t0);

struct VSInput {
    float3 position : POSITION;
    float3 normal   : NORMAL;
};

struct VSOutput {
    float4 position : SV_POSITION;
    float4 color    : COLOR;
};

VSOutput main(VSInput input, uint instanceID : SV_InstanceID)
{
    VSOutput output;
    InstanceData inst = gInstanceData[instanceID];
    
    float4 worldPos = mul(inst.worldMatrix, float4(input.position, 1.0));
    output.position = mul(gViewProjMatrix, worldPos);
    output.color    = inst.colorTint;
    return output;
}
Prova a rispondere a questa domanda con un coach IA

3Spiega il *frustum culling*, l'*occlusion culling* e il *back-face culling* e dove ciascuno si verifica tipicamente in un *renderer*.

Il *frustum culling*, l'*occlusion culling* e il *back-face culling* sono tre tecniche complementari di visibilità che scartano primitive non visibili in diverse fasi e con diverse granularità in una *pipeline* di *rendering*: 1. **Frustum Culling**: Scarta la geometria che si trova completamente al di fuori del *frustum* di vista della telecamera. Viene tipicamente eseguito su volumi di delimitazione grossolani (come AABB (Axis-Aligned Bounding Box) o sfere di delimitazione) sulla CPU (Central Processing Unit) prima della sottomissione di *draw*, o sulla GPU (Graphics Processing Unit) tramite *compute shader* in *pipeline* di *rendering* guidate dalla GPU. 2. **Occlusion Culling**: Scarta oggetti o primitive che sono all'interno del *frustum* ma nascoste dietro altra geometria opaca. Può verificarsi sulla CPU (usando la rasterizzazione *software* o la visibilità precalcolata) o sulla GPU (usando *query* di occlusione *hardware*, test del *buffer* di profondità Hi-Z tramite *compute* della GPU o *meshlet culling*) prima della rasterizzazione completa. 3. **Back-Face Culling**: Scarta i singoli poligoni le cui normali superficiali sono rivolte lontano dalla telecamera. Questo viene tradizionalmente eseguito automaticamente da *hardware* a funzione fissa durante la configurazione/rasterizzazione dei triangoli sulla GPU basato sull'ordine di avvolgimento nello spazio schermo, sebbene possa anche essere valutato grossolanamente su coni normali di *cluster* (ad esempio, in *mesh shader*).

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
}
Prova a rispondere a questa domanda con un coach IA

4Come le curve spline come Bézier, B-spline e Catmull-Rom generano percorsi fluidi o strisce di geometria?

Le curve spline forniscono formulazioni parametriche $\mathbf{P}(t)$ per definire percorsi 3D fluidi, tracce di telecamera e strisce di geometria estruse (come nastri, strade o tubi). 1. **Tipi e Proprietà delle Curve:** - **Curve di Bézier:** Formulate con polinomi di Bernstein. Interpolano solo gli estremi; i punti di controllo intermedi definiscono gli handle di tangenza. La connessione di segmenti con continuità $C^1$ richiede handle di tangenza collineari. - **B-spline:** Costruite utilizzando funzioni base su un vettore dei nodi. Forniscono controllo locale e alta continuità parametrica ($C^2$ per le cubiche), ma generalmente non passano attraverso i punti di controllo interni. - **Spline Catmull-Rom:** Una classe di spline interpolanti che passano direttamente attraverso tutti i punti di controllo interni garantendo automaticamente la continuità $C^1$, rendendole ideali per percorsi creati dall'utente. 2. **Generazione di Percorsi e Geometria:** - La valutazione della spline al parametro $t$ produce la posizione $\mathbf{P}(t)$ e il vettore tangente $\mathbf{T}(t) = \mathbf{P}'(t)$. - Per estrudere nastri o tubi 3D, è necessario un sistema di coordinate ortogonale (Normale $\mathbf{N}(t)$ e Binormale $\mathbf{B}(t)$) lungo la curva. - I frame di Frenet-Serret standard falliscono o si invertono nei punti di flesso dove la curvatura $\kappa = 0$. Per prevenire una torsione innaturale del nastro, i **Parallel Transport Frames (Bishop Frames)** propagano un orientamento di riferimento in modo fluido lungo la curva minimizzando la torsione rotazionale.

struct Vector3 { float x, y, z; };

Vector3 EvaluateCatmullRom(const Vector3& p0, const Vector3& p1, const Vector3& p2, const Vector3& p3, float t) {
    float t2 = t * t;
    float t3 = t2 * t;
    return 0.5f * ( (2.0f * p1) +
                    (-p0 + p2) * t +
                    (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 +
                    (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3 );
}
Prova a rispondere a questa domanda con un coach IA

5Cos'è un buffer di comando, e perché la registrazione multi-threaded dei comandi è importante nei motori di fascia alta?

Un buffer di comando (o lista di comandi in Direct3D 12) è una struttura dati in memoria dove i comandi grafici, di calcolo e di trasferimento — come l'impostazione dello stato della pipeline, l'associazione dei descrittori, l'emissione di draw call e la registrazione delle barriere della pipeline — vengono registrati sulla CPU per la successiva sottomissione ed esecuzione asincrona su una coda della GPU. La registrazione multi-threaded dei comandi è fondamentale nei motori di fascia alta perché la preparazione dei draw call lato CPU, l'associazione dello stato e il culling sono stati tradizionalmente i principali colli di bottiglia. Rimuovendo i vincoli di contesto single-threaded, le API esplicite consentono a un motore di suddividere un frame in task di rendering indipendenti su più thread worker della CPU. Ad esempio, i passaggi delle ombre, i pezzi del G-buffer e la post-elaborazione possono essere registrati simultaneamente. Le API moderne facilitano ciò tramite buffer di comando primari e secondari (Vulkan) o liste di comandi e bundle (D3D12). I buffer di comando secondari e i bundle consentono ai thread worker di registrare sottoinsiemi di comandi di disegno che possono essere eseguiti all'interno di un buffer di comando primario sul thread di sottomissione, massimizzando l'utilizzo della CPU multi-core e riducendo al minimo i blocchi della coda della GPU.

// Worker Thread Job:
void RecordShadowPassChunk(VkCommandBuffer secondaryCmdBuf, const RenderJob& job) {
    VkCommandBufferInheritanceInfo inheritInfo{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO };
    inheritInfo.renderPass = job.shadowRenderPass;
    
    VkCommandBufferBeginInfo beginInfo{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT;
    beginInfo.pInheritanceInfo = &inheritInfo;
    
    vkBeginCommandBuffer(secondaryCmdBuf, &beginInfo);
    vkCmdBindPipeline(secondaryCmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, job.pipeline);
    vkCmdDrawIndexed(secondaryCmdBuf, job.indexCount, 1, 0, 0, 0);
    vkEndCommandBuffer(secondaryCmdBuf);
}

// Main Thread Submission:
// vkCmdExecuteCommands(primaryCmdBuf, secondaryCount, secondaryCmdBuffers.data());
// vkQueueSubmit(queue, 1, &submitInfo, fence);
Prova a rispondere a questa domanda con un coach IA

6In che modo i push constants o i root constants differiscono dai buffer uniformi/costanti, e quando dovrebbero essere usati?

I push constants (in Vulkan) e i root constants (in DirectX 12) forniscono un meccanismo per passare piccole quantità di dati uniformi direttamente in linea all'interno del command buffer o della root signature, bypassando l'overhead di allocazione, aggiornamento e binding di risorse buffer GPU (Graphics Processing Unit) basate su descrittori. Al contrario, gli Uniform Buffer Objects (UBO) o i Constant Buffer Objects (CBO) sono supportati da allocazioni di memoria GPU dedicate che sono legate alla pipeline tramite descrittori, tabelle di descrittori o set di descrittori. Poiché i push/root constants sono incorporati nello stream di comandi stesso, sono ideali per dati ad alta frequenza, per ogni draw call, che cambiano frequentemente (come matrici di trasformazione degli oggetti, indici di materiali/mesh, valori di tempo o offset dinamici). Tuttavia, hanno limiti di dimensione rigorosi (ad esempio, Vulkan garantisce un limite minimo di soli 128 byte, e lo spazio della root signature di D3D12 è limitato a 64 DWORD, condivise con i root descriptor e le tabelle). I buffer uniformi/costanti dovrebbero essere usati quando il payload dei dati supera i limiti di dimensione dei push constant, quando i dati sono condivisi tra più draw call (come matrici di camera/vista per ogni frame, illuminazione globale della scena o impostazioni dell'ambiente), o quando è necessaria una memorizzazione persistente tra i passaggi.

// Push Constant setup
struct PushData {
    glm::mat4 modelMatrix;
    uint32_t materialIndex;
};

// Command buffer recording: inline write directly into command stream
PushData data = { object.transform, object.matID };
vkCmdPushConstants(cmdBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushData), &data);
vkCmdDrawIndexed(cmdBuffer, indexCount, 1, 0, 0, 0);

// Compared to UBO: requires updating mapped GPU buffer, managing offsets/ring buffers, and binding descriptor sets
vkCmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perObjectDescriptorSet, 0, nullptr);
Prova a rispondere a questa domanda con un coach IA

7Descrivi i vari spazi di coordinate che un vertice attraversa, dallo spazio modello allo spazio schermo, in un renderizzatore in tempo reale.

In una pipeline di rendering in tempo reale, un vertice tipicamente transita attraverso diversi spazi di coordinate: Spazio Modello (Locale), Spazio Mondo, Spazio Vista (Telecamera), Spazio Clip, Coordinate del Dispositivo Normalizzate (NDC) e Spazio Schermo (Viewport/Finestra). Il vertice inizia nello Spazio Modello relativo all'origine locale dell'asset. Moltiplicando per la matrice Modello/Mondo lo posiziona e lo orienta nello Spazio Mondo condiviso. Moltiplicando per la matrice di Vista lo trasforma nello Spazio Vista, dove la telecamera è all'origine e guarda lungo una direzione di vista standard. Successivamente, moltiplicando per la matrice di Proiezione le coordinate vengono trasformate nello Spazio Clip 4D `(xc, yc, zc, wc)`, dove la geometria viene clippata contro il volume di vista. Dopo il clipping, l'hardware a funzione fissa esegue la divisione prospettica (dividendo `xc, yc, zc` per `wc`) per produrre Coordinate del Dispositivo Normalizzate 3D (NDC). Infine, la Trasformazione Viewport mappa le coordinate NDC a coordinate pixel 2D dello Spazio Schermo e valori del depth buffer.

// Vertex Shader Stage
float4 worldPos = mul(modelMatrix, float4(inPosition, 1.0));
float4 viewPos  = mul(viewMatrix, worldPos);
float4 clipPos  = mul(projMatrix, viewPos); // Output to rasterizer

// Hardware Fixed-Function Stages:
// 1. Clipping against [-w, w]
// 2. Perspective Divide: ndcPos = clipPos.xyz / clipPos.w;
// 3. Viewport Transform -> Screen pixel coordinates (x_px, y_px)
Prova a rispondere a questa domanda con un coach IA

Domande Middle

8Confronta il Linear Blend Skinning con il Dual-Quaternion Skinning in termini di qualità della deformazione, artefatti e complessità ingegneristica.

Il Linear Blend Skinning (LBS) e il Dual-Quaternion Skinning (DQS) rappresentano due approcci distinti alla deformazione di mesh scheletriche: 1. **Qualità della Deformazione e Artefatti**: * LBS calcola i vertici trasformati tramite interpolazione lineare delle matrici di trasformazione delle ossa. Sebbene veloce, LBS soffre di perdita di volume durante rotazioni e torsioni severe, in particolare l'artefatto 'candy-wrapper' (effetto carta di caramella) dove la geometria cilindrica collassa lungo l'asse di torsione. * DQS rappresenta le trasformazioni rigide delle ossa come quaternioni duali unitari (che combinano rotazione e traslazione). Quando miscelato (ad esempio, usando il Dual Linear Blending), DQS preserva naturalmente il volume ed elimina gli artefatti di torsione 'candy-wrapper'. Tuttavia, DQS introduce i propri artefatti, come rigonfiamenti o pizzicamenti in corrispondenza di pieghe articolari estreme. 2. **Complessità Ingegneristica e di Implementazione**: * LBS supporta nativamente trasformazioni affini complete (traslazione, rotazione e scala o shear non uniformi) utilizzando pipeline di matrici 4x4 standard. * DQS gestisce nativamente solo le trasformazioni rigide. La gestione della scalatura (specialmente la scalatura non uniforme) richiede deformazione multi-pass, decomposizione polare o separazione scala-shear. Inoltre, DQS richiede la gestione dell'antipodalità durante il blending (controllando i prodotti scalari dei quaternioni duali per prendere il percorso di rotazione più breve ed evitare il ribaltamento/collasso della mesh), rendendo la matematica dello shader e la pipeline degli asset più complesse.

struct DualQuat {
    float4 rot;
    float4 trans;
};

DualQuat BlendDualQuaternions(uint4 indices, float4 weights, StructuredBuffer<DualQuat> boneDQs)
{
    DualQuat dq0 = boneDQs[indices.x];
    DualQuat blended = dq0;
    blended.rot *= weights.x;
    blended.trans *= weights.x;

    [unroll]
    for (int i = 1; i < 4; ++i)
    {
        DualQuat dqi = boneDQs[indices[i]];
        // Antipodality check: ensure shortest path
        float signVal = dot(dq0.rot, dqi.rot) < 0.0 ? -1.0 : 1.0;
        blended.rot += dqi.rot * (weights[i] * signVal);
        blended.trans += dqi.trans * (weights[i] * signVal);
    }
    float len = length(blended.rot);
    blended.rot /= len;
    blended.trans /= len;
    return blended;
}
Prova a rispondere a questa domanda con un coach IA

9I personaggi animati si deformano in modo errato solo su alcune mesh. Quali dati di asset e shader ispezioneresti?

Quando i personaggi animati si deformano in modo errato solo su un sottoinsieme di mesh, il problema deriva tipicamente da mancate corrispondenze nei dati tra la pipeline degli asset, il layout dei vertici o le costanti dello shader. Un'ispezione sistematica dovrebbe coprire: 1. Layout dei Vertici e Limiti degli Indici delle Ossa: Assicurarsi che gli indici delle ossa dei vertici non superino il numero di ossa dello scheletro o non causino un overflow del loro tipo di dato impacchettato (ad es., usando `uint8`/`ubyte4` quando lo scheletro ha >256 ossa, causando un "wrap-around" dell'indice). 2. Normalizzazione dei Pesi delle Ossa: Verificare che la somma dei pesi delle ossa per vertice sia uguale a 1.0. Pesi non normalizzati fanno sì che i vertici si restringano verso o si allontanino dallo scheletro. 3. Matrici di Bind Pose Inverse (IBMs): Confermare che le matrici di bind inverse della mesh corrispondano alla posa di riposo (rest pose) e allo spazio di coordinate dello scheletro. Bind pose non corrispondenti causano l'esplosione o il dislocamento errato della mesh. 4. Influenze Massime per Vertice: Controllare se l'esportatore DCC ha esportato più influenze delle ossa per vertice (ad es., 8 influenze) di quante ne supporti il layout del vertex buffer o lo shader (ad es., 4 influenze), eliminando i pesi senza rinormalizzazione. 5. Gerarchia dello Scheletro e Indicizzazione della Palette: Convalidare che le mappature degli indici delle ossa nella mesh corrispondano alla palette delle matrici delle ossa caricata nei buffer costanti/strutturati.

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!");
    }
}
Prova a rispondere a questa domanda con un coach IA

10Cosa sono i morph target o blend shape, e come vengono combinati con lo skinning scheletrico per l'animazione facciale?

I morph target (o blend shape) rappresentano deformazioni geometriche memorizzate come offset delta per vertice (posizioni delta, normali delta e opzionalmente tangenti delta) relative a una mesh base in posizione di riposo (rest-pose). Ogni morph target è controllato da un peso scalare (tipicamente da 0.0 a 1.0), e gli attributi del vertice deformati sono calcolati come: `Morphed_Attribute = Base_Attribute + Sum(Weight_i * Delta_i)`. Quando si combinano i morph target con lo skinning scheletrico (ad esempio, per l'animazione facciale): 1. Ordine di Valutazione: I delta dei morph target devono essere valutati nello spazio modello della posa neutra/di legame (neutral/bind pose) prima che venga applicato lo skinning scheletrico. 2. Passaggio di Skinning: Le posizioni e le normali morfed vengono successivamente trasformate dalle matrici ossee dello skinning scheletrico. L'applicazione del morphing prima dello skinning assicura che le espressioni facciali si deformino naturalmente con i movimenti della testa e le rotazioni delle articolazioni della mascella. Dal punto di vista delle prestazioni e della larghezza di banda, memorizzare e leggere ingenuamente copie complete della mesh per decine di blend shape causa una forte pressione sulla larghezza di banda della memoria. Implementazioni pratiche memorizzano delta sparsi (solo vertici non-zero), comprimono i formati delta (ad esempio, FP16 o interi quantizzati), o utilizzano pre-passaggi con compute shader della GPU per calcolare i vertici morfed una volta prima di passaggi di rendering multipli.

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];
}
Prova a rispondere a questa domanda con un coach IA

11Spiega la selezione geometrica del Level of Detail (LOD), la semplificazione della mesh e le strategie di transizione che bilanciano stabilità visiva, preservazione degli attributi e prestazioni.

Il Level of Detail (LOD) geometrico ottimizza le prestazioni di rendering riducendo la complessità della mesh man mano che gli oggetti si allontanano dalla telecamera, bilanciando la fedeltà visiva e la frequenza dei fotogrammi (*frame rate*). 1. **Selezione dell'LOD**: Gli LOD dovrebbero essere selezionati utilizzando metriche nello spazio schermo (come il diametro della sfera di delimitazione proiettata, la percentuale dell'altezza dello schermo o l'errore di pixel proiettato) piuttosto che la distanza statica nello spazio mondo, per tenere conto del FOV (Field of View) della telecamera e dei cambiamenti di risoluzione. Per prevenire rapide oscillazioni tra gli LOD ai confini di distanza (*'LOD thrashing'*), viene applicata l'isteresi mantenendo soglie separate per il passaggio a un LOD superiore e per quello a un LOD inferiore. 2. **Semplificazione della Mesh**: La generazione offline si basa comunemente sulle Metriche di Errore Quadriche (QEM) tramite collassi iterativi degli spigoli. Per mantenere la qualità visiva, gli algoritmi di semplificazione devono preservare le silhouette dei bordi e penalizzare la distorsione geometrica, oltre a preservare gli attributi dei vertici (giunzioni UV, divisioni delle normali, colori dei vertici e pesi di *skinning*) incorporando termini di errore degli attributi nella metrica quadrica. 3. **Strategie di Transizione**: Per prevenire un improvviso '*popping*' visivo, i motori utilizzano: * **Crossfading Dithering / Stippling a Griglia (*Screen-Door Stippling*)**: Scarta i pixel nel *pixel shader* utilizzando un *pattern* di *dithering* interlacciato (es. matrice di Bayer), sfumando uniformemente tra gli LOD senza richiedere *alpha blending* o interrompere l'*early-Z*. * **Geomorphing**: Interpola le posizioni dei vertici tra mesh LOD adiacenti sulla GPU in una breve finestra di transizione.

float CalculateLODDither(float2 screenPos, float lodBlendFactor)
{
    const float bayer4x4[16] = {
         0.0/16.0,  8.0/16.0,  2.0/16.0, 10.0/16.0,
        12.0/16.0,  4.0/16.0, 14.0/16.0,  6.0/16.0,
         3.0/16.0, 11.0/16.0,  1.0/16.0,  9.0/16.0,
        15.0/16.0,  7.0/16.0, 13.0/16.0,  5.0/16.0
    };
    uint2 pixelCoord = (uint2)screenPos.xy % 4;
    float threshold = bayer4x4[pixelCoord.y * 4 + pixelCoord.x];
    return (lodBlendFactor - threshold);
}

// In pixel shader: if (CalculateLODDither(input.position.xy, lodTransitionAlpha) < 0.0) discard;
Prova a rispondere a questa domanda con un coach IA

12Cos'è l'ottimizzazione del buffer degli indici o della cache dei vertici e perché l'ordine dei triangoli influisce sull'efficienza della cache post-trasformazione?

L'ottimizzazione della cache dei vertici (o del buffer degli indici) riordina gli indici dei triangoli e i dati dei vertici in una mesh per massimizzare i tassi di "hit" nelle cache dei vertici hardware della GPU (Graphics Processing Unit). Le GPU hanno due cache dei vertici principali: 1. **Cache post-trasformazione (Post-Transform Cache)**: Una piccola cache FIFO (First-In, First-Out) / LRU (Least Recently Used) che memorizza gli output dello shader dei vertici trasformati (posizioni, attributi). Quando i triangoli adiacenti condividono i vertici, fare riferimento a questi vertici in modo ravvicinato nello stream degli indici consente alla GPU di riutilizzare gli output dello shader memorizzati nella cache invece di eseguire lo shader dei vertici più volte per lo stesso vertice. 2. **Cache pre-trasformazione (Pre-Transform Cache)**: La cache di memoria L1/L2 della GPU per i dati raw del buffer dei vertici. Riordinare i dati del buffer dei vertici in modo che corrispondano all'ordine di primo accesso degli indici ottimizzati massimizza la località spaziale e l'efficienza della larghezza di banda della memoria. L'ordine dei triangoli determina direttamente la sequenza di accesso nella cache post-trasformazione. Gli algoritmi di ottimizzazione (come l'algoritmo di Tom Forsyth o Tipsify) assegnano punteggi di riutilizzo dinamici ai vertici basati su valenza e posizione nella cache, dando priorità ai triangoli che completano i riferimenti rimanenti a vertici recentemente memorizzati nella cache per minimizzare il Tasso medio di cache miss (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;
}
Prova a rispondere a questa domanda con un coach IA

Domande Senior

13Quali approcci di occlusion culling evitano i blocchi (stalls) CPU-GPU e il popping errato?

Le query di occlusion hardware tradizionali causano blocchi (stalls) sincroni di readback CPU-GPU se la CPU attende i risultati della visibilità all'interno dello stesso frame. Ritardare i readback di un frame evita i blocchi ma introduce latenza temporale, causando popping visibile quando gli oggetti appena visibili non vengono renderizzati immediatamente. Per evitare sia i blocchi CPU-GPU che il popping visivo, le architetture di produzione moderne utilizzano: 1. **Occlusion Culling Hi-Z (Hierarchical-Z) a due fasi guidato dalla GPU**: La GPU testa i bounding box contro una piramide di profondità Hierarchical-Z (Hi-Z) generata dal frame precedente. Gli oggetti noti per essere visibili vengono disegnati nella Fase 1 (generando la profondità iniziale del frame corrente). Gli oggetti precedentemente occlusi vengono ritestati contro il buffer Hi-Z aggiornato del frame corrente nella Fase 2; qualsiasi oggetto appena rivelato viene renderizzato immediatamente prima dell'illuminazione e del post-processing, eliminando il popping con zero readback della CPU. 2. **Rasterizzazione Software della CPU**: Un buffer di profondità a bassa risoluzione viene rasterizzato puramente su thread worker della CPU (utilizzando SIMD) da mesh di occluder semplificate. La CPU testa i bounding box in modo sincrono senza la necessità di query GPU o di incorrere in latenza di trasferimento GPU-a-CPU. 3. **Bounding Conservativo e Isteresi Temporale**: L'espansione dei volumi di delimitazione o il ritardo delle retrocessioni dello stato di visibilità evitano il culling prematuro durante i rapidi movimenti della telecamera.

// 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;
    }
}
Prova a rispondere a questa domanda con un coach IA

14Descrivi una tipica pipeline di elaborazione degli asset, dalla mesh autoriale ai buffer della GPU (Graphics Processing Unit) in fase di runtime, inclusa la generazione delle tangenti, la quantizzazione, la validazione e l'ottimizzazione.

Una pipeline standard di elaborazione degli asset trasforma le mesh DCC (Digital Content Creation) autoriali grezze (FBX, glTF, USD) in formati binari ad alte prestazioni e pronti per la GPU attraverso cinque fasi principali: 1. Acquisizione e Validazione: La mesh di origine viene pulita rimuovendo vertici duplicati o inutilizzati, scartando triangoli degeneri/con area zero, verificando la geometria manifold, gestendo i NaN (Not a Number) e suddividendo le mesh multi-materiale in sub-mesh distinte. 2. Generazione dello Spazio Tangente: Tangenti e bitangenti vengono calcolate utilizzando algoritmi standardizzati (principalmente MikkTSpace) per garantire parità visiva con gli strumenti di normal baking. Questo tiene conto correttamente delle cuciture UV e delle mappe UV speculari (memorizzando l'orientamento – handedness – in `tangent.w`). 3. Ottimizzazione: Gli indici vengono riordinati per l'efficienza della cache dei vertici post-trasformazione (ad es., Forsyth/Tipsify), i buffer dei vertici vengono riordinati per la località di fetch dei vertici pre-trasformazione e vengono generati livelli di LOD (Level of Detail) o meshlet. 4. Quantizzazione e Impacchettamento degli Attributi: Gli attributi dei vertici sono quantizzati per ridurre l'ingombro di memoria e la larghezza di banda della memoria: posizioni a `half`/`unorm` a 16 bit o interi normalizzati, normali e tangenti a `SNORM` a 8 bit o codifiche ottaedriche (Oct16/Oct32), e UV a `float`/`unorm` a 16 bit. Gli attributi possono essere interleavati (AoS - Array of Structures) o divisi in più stream (SoA - Structure of Arrays, ad esempio, solo la posizione per i pre-pass di profondità). 5. Cooking e Serializzazione: Buffer, volumi di delimitazione (AABB/sfere) e tabelle LOD vengono serializzati in file binari piatti che non richiedono patching dei puntatori in fase di runtime, permettendo un rapido upload DMA (Direct Memory Access) ai buffer della GPU tramite memoria di 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)
Prova a rispondere a questa domanda con un coach IA

15Spiega i meshlet, il culling dei cluster, gli shader mesh e le pipeline di micro-geometria densa per grandi scene statiche.

Le pipeline di meshlet e le architetture di micro-geometria densa (come Nanite di Unreal) sostituiscono le grandi draw call con buffer di indici con piccoli cluster di geometria delimitati chiamati 'meshlet'. 1. **Meshlet:** Un meshlet è un cluster di geometria tipicamente limitato a 32–128 vertici e fino a 128–256 triangoli. Ogni meshlet contiene indici dei vertici locali, stream di attributi e dati di bounding precalcolati (una sfera di bounding e un cono normale). 2. **Shader mesh e di amplificazione:** Sostituiscono la pipeline a funzione fissa di vertici, assemblaggio primitivo e shader di geometria. Gli Shader di Amplificazione (Task) valutano il frustum a livello di cluster, l'occlusione e il culling delle facce posteriori tramite cono normale su gruppi di meshlet. I meshlet sopravvissuti richiamano gli Shader Mesh, dove un gruppo di thread trasforma cooperativamente i vertici nella Memoria Condivisa on-chip (LDS - Local Data Share) e produce direttamente gli indici primitivi al rasterizzatore. 3. **Pipeline di micro-geometria densa:** La geometria ad alta densità produce triangoli sub-pixel che soffrono di un grave quad-overdraw (dove l'hardware standard rasterizza quad ausiliari 2x2, eseguendo shader di pixel completi per un solo pixel coperto). I moderni sistemi di micro-geometria densa utilizzano strutture LOD (Level Of Detail) di cluster gerarchici (DAG - Directed Acyclic Graph) per selezionare dinamicamente i LOD dei cluster, garantendo lunghezze dei bordi di circa 1 pixel, e spesso combinano la rasterizzazione hardware per poligoni grandi con rasterizzatori software compute personalizzati per micro-poligoni sub-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);
    }
}
Prova a rispondere a questa domanda con un coach IA