Підготовка до співбесіди з комп'ютерної графіки

Запитання для співбесіди Computer Graphics Developer

15 обраних запитань для співбесіди з комп'ютерної графіки, згрупованих за рівнем досвіду. Використовуйте їх для повторення основ, практичних компромісів та міркувань щодо виробничих рішень на Senior рівні.

Почати AI-співбесіду з комп'ютерної графікиКредитна картка не потрібна. Доступна 1 безкоштовна сесія.
Практика технічних співбесід англійськоюРежим для тих, для кого англійська не є рідною, щоб тренувати технічні співбесіди.

Запитання для Junior

1Опишіть лінійне змішане скінінг (Linear Blend Skinning - LBS) та як матриці кісток застосовуються до вершини з множинними впливами.

Лінійне змішане скінінг (LBS) — це техніка геометричної деформації, що використовується для анімації 3D-мешів на основі базової скелетної ієрархії. У скелетній анімації кожна анімована кістка рухається відносно своєї референсної конфігурації (пози зв'язування). Щоб трансформувати вершину, на яку впливає декілька кісток: 1. Початкова позиція вершини в просторі мешу трансформується в локальний простір кожної кістки шляхом множення на матрицю оберненої пози зв'язування кістки ($B_i^{-1}$). 2. Потім вершина трансформується з локального простору кістки в простір поточної анімованої пози за допомогою матриці анімованої кістки ($M_i$). Складене перетворення $S_i = M_i \cdot B_i^{-1}$ є матрицею палітри скінінгу. 3. Кінцева позиція скінінгованої вершини обчислюється як лінійна зважена сума по всіх кістках, що впливають: $$v' = \sum_{i=1}^{k} w_i \cdot (M_i \cdot B_i^{-1} \cdot v)$$ де скалярні ваги кісток $w_i$ повинні бути нормалізовані (тобто $\sum w_i = 1.0$). На GPU це зазвичай виконується у вершинному шейдері (або на етапі попередньої обробки скінінгу обчислювальним шейдером) шляхом отримання попередньо обчисленої палітри кісток з uniform/structured буфера, використовуючи атрибути індексів кісток вершин, та лінійного змішування позицій. Нормалі та тангенси вершин трансформуються за допомогою обертальної частини змішаної скінінг-матриці та повторно нормалізуються.

#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);
}
Відповісти на це запитання з ШІ-тренером

2Опишіть інстансинг GPU та які дані та макети для кожного екземпляра роблять рендеринг багатьох подібних об'єктів ефективним.

Інстансинг GPU (Graphics Processing Unit) — це техніка рендерингу, яка малює кілька копій однієї базової геометрії (спільно використовуючи вершинні та індексні буфери) за один виклик малювання (наприклад, `DrawIndexedInstanced` у Direct3D або `glDrawElementsInstanced` у OpenGL). Це суттєво зменшує накладні витрати драйвера CPU-GPU (Central Processing Unit - Graphics Processing Unit) та кількість викликів малювання API (Application Programming Interface). Дані для кожного екземпляра: Щоб екземпляри виглядали та поводились по-різному, надаються дані для кожного екземпляра, які зазвичай включають: - Дані трансформації: матриця світу, або упаковані позиція/обертання/масштаб. - Властивості матеріалу: відтінки кольору, зміщення/масштаби UV-координат, або індекси ідентифікаторів матеріалів. - Динамічні параметри: фаза анімації, зміщення лайтмапи (lightmap), або прапорці видимості. Макети даних та методи доступу: 1. Інстансовані вершинні буфери: Спеціальний вершинний буфер, прив'язаний із кроком для кожного екземпляра (наприклад, `D3D11_INPUT_PER_INSTANCE_DATA`). GPU автоматично просуває буфер для кожного екземпляра. 2. `StructuredBuffer` / `Uniform Buffer` (`SSBO` / `Constant Buffer`): Дані екземпляра завантажуються в масив буфера, і вершинний шейдер індексує їх за допомогою вбудованого системного ідентифікатора екземпляра (`SV_InstanceID` в HLSL, `gl_InstanceID` в GLSL). Збереження компактності даних для кожного екземпляра (наприклад, афінні матриці 3x4 або позиція + кватерніон замість повних матриць 4x4, а також упаковані кольори `FP16`/`uint32`) мінімізує пропускну здатність пам'яті GPU та оптимізує використання кешу.

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;
}
Відповісти на це запитання з ШІ-тренером

3Поясніть відсікання за фрустумом (frustum culling), відсікання приховуванням (occlusion culling) та відсікання задніх граней (back-face culling) і де кожне з них зазвичай відбувається в рендерері.

Відсікання за фрустумом, відсікання приховуванням та відсікання задніх граней — це три взаємодоповнюючі методи визначення видимості, які відкидають невидимі примітиви на різних етапах та з різною гранулярністю в конвеєрі рендерингу: 1. **Відсікання за фрустумом (Frustum Culling)**: Відкидає геометрію, що повністю знаходиться за межами фрустума огляду камери. Зазвичай виконується на грубих обмежувальних об'ємах (як-от AABB (Axis-Aligned Bounding Box) або обмежувальні сфери) на CPU (Central Processing Unit) перед поданням на малювання, або на GPU (Graphics Processing Unit) через обчислювальні шейдери в конвеєрах рендерингу, керованих GPU. 2. **Відсікання приховуванням (Occlusion Culling)**: Відкидає об'єкти або примітиви, які знаходяться всередині фрустума, але приховані за іншою непрозорою геометрією. Може відбуватися на CPU (з використанням програмної растеризації або попередньо обчисленої видимості) або на GPU (з використанням апаратних запитів на приховування, тестів буфера глибини Hi-Z за допомогою обчислень GPU або відсікання мешлетів) до повної растеризації. 3. **Відсікання задніх граней (Back-Face Culling)**: Відкидає окремі полігони, нормалі поверхні яких спрямовані від камери. Це традиційно виконується автоматично апаратним забезпеченням з фіксованою функцією під час налаштування/растеризації трикутників на GPU на основі порядку обходу в екранному просторі, хоча може також грубо оцінюватися на конусах нормалей кластерів (наприклад, у меш-шейдерах).

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
}
Відповісти на це запитання з ШІ-тренером

4Як сплайнові криві, такі як Без'є (Bézier), B-сплайн (B-spline) та Катмулл-Ром (Catmull-Rom), генерують плавні шляхи або смуги геометрії?

Сплайнові криві надають параметричні формулювання $\mathbf{P}(t)$ для визначення плавних 3D шляхів, траєкторій камери та екструдованих смуг геометрії (таких як стрічки, дороги або труби). 1. **Типи кривих та їх властивості:** - **Криві Без'є (Bézier Curves):** Сформульовані за допомогою поліномів Бернштейна. Вони інтерполюють лише кінцеві точки; проміжні контрольні точки визначають тангенсні ручки. З'єднання сегментів з неперервністю $C^1$ вимагає колінеарних тангенсних ручок. - **B-сплайни (B-splines):** Побудовані з використанням базисних функцій над вектором вузлів. Вони забезпечують локальний контроль та високу параметричну неперервність ($C^2$ для кубічних), але зазвичай не проходять через внутрішні контрольні точки. - **Сплайни Катмулла-Рома (Catmull-Rom Splines):** Клас інтерполяційних сплайнів, які проходять безпосередньо через усі внутрішні контрольні точки, автоматично забезпечуючи неперервність $C^1$, що робить їх ідеальними для шляхів, створених користувачем. 2. **Генерація шляхів та геометрії:** - Оцінка сплайна за параметром $t$ дає позицію $\mathbf{P}(t)$ та тангенсний вектор $\mathbf{T}(t) = \mathbf{P}'(t)$. - Для екструдування 3D стрічок або труб потрібен ортогональний координатний фрейм (нормаль $\mathbf{N}(t)$ та бінормаль $\mathbf{B}(t)$) вздовж кривої. - Стандартні фрейми Френе-Серре (Frenet-Serret frames) дають збій або перевертаються в точках перегину, де кривина $\kappa = 0$. Щоб запобігти неприродному скручуванню стрічки, **фрейми паралельного перенесення (Parallel Transport Frames, або фрейми Бішопа (Bishop Frames))** плавно поширюють референсну орієнтацію вздовж кривої, мінімізуючи обертальне крутіння.

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 );
}
Відповісти на це запитання з ШІ-тренером

5Що таке буфер команд (command buffer), і чому багатопотоковий запис команд важливий у високопродуктивних рушіях?

Буфер команд (або список команд (command list) у Direct3D 12) — це структура даних у пам'яті, де команди графіки, обчислень та передачі даних, такі як встановлення стану конвеєра (pipeline state), прив'язка дескрипторів (binding descriptors), виклики малювання (draw calls) та запис бар'єрів конвеєра (pipeline barriers), записуються на центральному процесорі (ЦП) для подальшого надсилання та асинхронного виконання в черзі графічного процесора (GPU). Багатопотоковий запис команд (multi-threaded command recording) є критично важливим у високопродуктивних рушіях, оскільки підготовка викликів малювання на стороні ЦП, прив'язка стану та відсікання (culling) традиційно були основними вузькими місцями (bottlenecks). Усуваючи обмеження однопотокового контексту, явні API (Application Programming Interface) дозволяють рушію розділити кадр на незалежні завдання рендерингу між кількома робочими потоками (worker threads) ЦП. Наприклад, проходи тіней (shadow passes), фрагменти G-буфера (G-buffer chunks) та постобробка (post-processing) можуть бути записані одночасно. Сучасні API полегшують це за допомогою первинних і вторинних буферів команд (Vulkan) або списків команд та пакетів (bundles) (D3D12). Вторинні буфери команд та пакети дозволяють робочим потокам записувати підмножини команд малювання, які можуть бути виконані всередині первинного буфера команд у потоці надсилання, максимізуючи використання багатоядерного ЦП та мінімізуючи затримки черги графічного процесора (GPU queue stalls).

// 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);
Відповісти на це запитання з ШІ-тренером

6Чим push-константи або root-константи відрізняються від uniform/constant буферів, і коли їх слід використовувати?

Push-константи (у Vulkan) та root-константи (у DirectX 12) надають механізм для передачі невеликих обсягів уніфікованих даних безпосередньо в командний буфер або root-сигнатуру, оминаючи накладні витрати на виділення, оновлення та прив'язку ресурсів буфера GPU (графічного процесора), що підтримуються дескрипторами. На відміну від цього, Uniform-буфери (UBO) або Constant-буфери (CBO) підтримуються виділеними виділеннями пам'яті GPU, які прив'язуються до конвеєра за допомогою дескрипторів, таблиць дескрипторів або наборів дескрипторів. Оскільки push/root константи вбудовані безпосередньо в потік команд, вони ідеально підходять для даних, що часто змінюються для кожного виклику малювання (per-draw data) (таких як матриці трансформації об'єктів, індекси матеріалів/мешів, значення часу або динамічні зміщення). Однак вони мають суворі обмеження розміру (наприклад, Vulkan гарантує мінімальний ліміт лише 128 байтів, а простір root-сигнатури D3D12 обмежений 64 DWORD, які розподіляються з root-дескрипторами та таблицями). Uniform/constant буфери слід використовувати, коли обсяг даних перевищує обмеження розміру push-констант, коли дані використовуються спільно кількома викликами малювання (таких як матриці камери/виду для кожного кадру, глобальне освітлення сцени або налаштування оточення), або коли потрібне постійне зберігання між проходами.

// 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);
Відповісти на це запитання з ШІ-тренером

7Опишіть координатні простори, через які проходить вершина від модельного простору до екранного простору в рендерері реального часу.

У конвеєрі рендерингу реального часу вершина зазвичай проходить через кілька координатних просторів: Модельний (локальний) простір, Світовий простір, Простір перегляду (камери), Простір відсікання, Нормалізовані координати пристрою (NDC) та Екранний простір (порт перегляду/вікно). Вершина починається в Модельному просторі відносно локального початку координат об'єкта. Множення на матрицю Модель/Світ розміщує та орієнтує її у спільному Світовому просторі. Множення на матрицю Виду перетворює її в Простір перегляду, де камера знаходиться в початку координат, дивлячись у стандартному напрямку перегляду. Далі, множення на матрицю Проекції перетворює координати в 4D Простір відсікання $(x_c, y_c, z_c, w_c)$, де геометрія відсікається за обсягом перегляду. Після відсікання апаратне забезпечення з фіксованими функціями виконує перспективне ділення (ділення $x_c, y_c, z_c$ на $w_c$) для отримання 3D Нормалізованих координат пристрою (NDC). Нарешті, Перетворення порту перегляду (Viewport Transform) перетворює координати NDC на 2D піксельні координати Екранного простору та значення буфера глибини.

// 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)
Відповісти на це запитання з ШІ-тренером

Запитання для Middle

8Порівняйте лінійне змішування скінінгу (Linear Blend Skinning) з двійково-кватерніонним скінінгом (Dual-Quaternion Skinning) за якістю деформації, артефактами та інженерною складністю.

Лінійне змішування скінінгу (LBS) та двійково-кватерніонний скінінг (DQS) представляють два різні підходи до деформації скелетної сітки: 1. **Якість деформації та артефакти:** * LBS обчислює трансформовані вершини шляхом лінійної інтерполяції матриць кісткових перетворень. Хоча LBS є швидким, він страждає від втрати об'єму під час сильних обертань та скручування, зокрема від артефакту «цукеркової обгортки» (candy-wrapper), коли циліндрична геометрія колапсує вздовж осі скручування. * DQS представляє жорсткі перетворення кісток як одиничні двійкові кватерніони (поєднуючи обертання та переміщення). При змішуванні (наприклад, за допомогою подвійного лінійного змішування, Dual Linear Blending) DQS природно зберігає об'єм та усуває артефакти скручування типу «цукеркової обгортки». Однак DQS має власні артефакти, такі як здуття або защемлення при екстремальних вигинах суглобів. 2. **Інженерна складність та складність реалізації:** * LBS нативно підтримує повні афінні перетворення (переміщення, обертання та неоднорідне масштабування або зсув) за допомогою стандартних конвеєрів 4x4 матриць. * DQS нативно обробляє лише жорсткі перетворення. Обробка масштабування (особливо неоднорідного) вимагає багатопрохідної деформації, полярного розкладання або розділення масштабу та зсуву. Крім того, DQS вимагає обробки антиподальності під час змішування (перевірка скалярних добутків двійкових кватерніонів для вибору найкоротшого шляху обертання та уникнення перевертання/колапсу сітки), що ускладнює математику шейдерів та конвеєр обробки ресурсів.

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;
}
Відповісти на це запитання з ШІ-тренером

9Анімовані персонажі неправильно деформуються лише на деяких мешах. Які дані асетів та шейдерів ви б перевірили?

Коли анімовані персонажі неправильно деформуються лише на підмножині мешів (сіток), проблема зазвичай виникає через розбіжності в даних по всьому конвеєру асетів, розташуванню вершин або константах шейдера. Систематична перевірка повинна охоплювати: 1. **Розташування вершин та межі індексів кісток:** Переконайтеся, що індекси кісток вершин не перевищують кількість кісток скелета або не переповнюють їхній упакований тип даних (наприклад, використання `uint8/ubyte4`, коли скелет має >256 кісток, що викликає зациклення індексів). 2. **Нормалізація ваг кісток:** Перевірте, що сума ваг кісток для кожної вершини дорівнює 1.0. Ненормалізовані ваги призводять до того, що вершини стискаються до скелета або віддаляються від нього. 3. **Матриці оберненої прив'язувальної пози (IBMs):** Підтвердьте, що матриці оберненої прив'язувальної пози меша відповідають початковій позі скелета та його координатній системі. Неузгоджені прив'язувальні пози призводять до "вибуху" меша або його неправильного зміщення. 4. **Максимальна кількість впливів на вершину:** Перевірте, чи експортер із DCC (Digital Content Creation) програм експортував більше впливів кісток на вершину (наприклад, 8 впливів), ніж підтримує розташування вершинного буфера або шейдер (наприклад, 4 впливи), що призводить до скидання ваг без перенормалізації. 5. **Ієрархія скелета та індексація палітри:** Перевірте, чи відображення індексів кісток у меші відповідають палітрі матриць кісток, завантаженій у буфери констант/структуровані буфери.

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!");
    }
}
Відповісти на це запитання з ШІ-тренером

10Що таке морфи-цілі (morph targets) або бленд-шейпи (blend shapes), і як вони поєднуються зі скелетним скінінгом (skeletal skinning) для мімічної анімації?

Морфи-цілі (morph targets) або бленд-шейпи (blend shapes) представляють геометричні деформації, що зберігаються як зміщення дельта за вершинами (дельта-позиції, дельта-нормалі та, за бажанням, дельта-тангенси) відносно базової сітки в початковій позі. Кожна морфа-ціль контролюється скалярною вагою (зазвичай від 0.0 до 1.0), а деформовані атрибути вершин розраховуються як: `Morphed_Attribute = Base_Attribute + Sum(Weight_i * Delta_i)`. При поєднанні морфів-цілей зі скелетним скінінгом (наприклад, для мімічної анімації): 1. Порядок оцінки: Дельта морфів-цілей повинні оцінюватися в просторі моделі нейтральної/зв'язуючої пози, перш ніж застосовується скелетний скінінг. 2. Прохід скінінгу: Деформовані позиції та нормалі потім трансформуються матрицями кісток скелетного скінінгу. Застосування морфінгу до скінінгу забезпечує природну деформацію міміки при поворотах голови та обертаннях щелепних суглобів. З точки зору продуктивності та пропускної здатності, наївне зберігання та читання повних копій сіток для десятків бленд-шейпів викликає значний тиск на пропускну здатність пам'яті. Практичні реалізації зберігають розріджені дельти (лише ненульові вершини), стискають формати дельт (наприклад, FP16 або квантовані цілі числа) або використовують попередні проходи обчислювальних шейдерів GPU для одноразового обчислення деформованих вершин перед кількома проходами рендерингу.

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];
}
Відповісти на це запитання з ШІ-тренером

11Поясніть вибір геометричного рівня деталізації (LOD), спрощення сітки та стратегії переходу, які балансують візуальну стабільність, збереження атрибутів та продуктивність.

Геометричний рівень деталізації (LOD — Level of Detail) оптимізує продуктивність рендерингу, зменшуючи складність сітки в міру віддалення об'єктів від камери, балансуючи візуальну точність та частоту кадрів. 1. **Вибір LOD**: LOD слід вибирати, використовуючи метрики екранного простору (такі як діаметр проектованої обмежувальної сфери, відсоток висоти екрана або помилка проектованого пікселя), а не статичну відстань у світовому просторі, щоб враховувати зміни поля зору камери (FOV) та роздільної здатності. Щоб запобігти швидким коливанням між LOD на межах відстані ('трешінг LOD' — LOD thrashing), застосовується гістерезис шляхом підтримки окремих порогів для переходу вгору та вниз. 2. **Спрощення сітки**: Офлайн-генерація зазвичай базується на метриках квадратичної помилки (QEM — Quadric Error Metrics) через ітеративні згортання ребер. Для підтримки візуальної якості алгоритми спрощення повинні зберігати силуети кордонів та запобігати геометричним спотворенням, а також зберігати атрибути вершин (UV шви, розділення нормалей, кольори вершин та ваги скіннінгу), включаючи члени помилки атрибутів у квадратичну метрику. 3. **Стратегії переходу**: Щоб запобігти різкому візуальному 'вискакуванню', рушії використовують: * **Дитероване перехресне згасання / Муарний степлінг** (Dithered Crossfading / Screen-Door Stippling): Відкидає пікселі в піксельному шейдері, використовуючи чергуючий дитерний візерунок (наприклад, матрицю Баєра), плавно згасаючи між LOD без необхідності альфа-змішування або порушення раннього Z-буфера (early-Z). * **Геоморфінг** (Geomorphing): Інтерполює позиції вершин між сусідніми сітками LOD на графічному процесорі (GPU) протягом короткого вікна переходу.

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;
Відповісти на це запитання з ШІ-тренером

12Що таке оптимізація індексного буфера або кешу вершин, і чому порядок трикутників впливає на ефективність кешу після трансформації?

Оптимізація кешу вершин (або індексного буфера) змінює порядок індексів трикутників та даних вершин у сітці (mesh) для максимізації коефіцієнта влучань у апаратних кешах вершин графічного процесора (GPU). GPU мають два основні кеші вершин: 1. **Кеш після трансформації (Post-Transform Cache)**: Невеликий кеш FIFO (first-in, first-out) / LRU (least recently used), що зберігає виходи вершинного шейдера після трансформації (позиції, атрибути). Коли суміжні трикутники мають спільні вершини, посилання на ці вершини близько в потоці індексів дозволяє GPU повторно використовувати кешовані виходи шейдера замість багаторазового запуску вершинного шейдера для однієї і тієї ж вершини. 2. **Кеш перед трансформацією (Pre-Transform Cache)**: Кеш пам'яті L1/L2 GPU для сирих даних вершинного буфера. Зміна порядку даних вершинного буфера, щоб відповідати порядку першого доступу оптимізованих індексів, максимізує просторову локальність та ефективність пропускної здатності пам'яті. Порядок трикутників безпосередньо визначає послідовність доступу в кеші після трансформації. Алгоритми оптимізації (такі як алгоритм Тома Форсайта або Tipsify) присвоюють динамічні оцінки повторного використання вершинам на основі валентності та позиції в кеші, надаючи пріоритет трикутникам, які завершують інші посилання на нещодавно кешовані вершини, щоб мінімізувати середній коефіцієнт промахів кешу (Average Cache Miss Ratio, ACMR).

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;
}
Відповісти на це запитання з ШІ-тренером

Запитання для Senior

13Які підходи до відсікання оклюзій (occlusion culling) дозволяють уникнути простоїв між ЦП (Центральним Процесором) і ГП (Графічним Процесором) та неправильних візуальних «вискакувань»?

Традиційні апаратні запити оклюзії спричиняють синхронні простої при зчитуванні даних з ГП на ЦП (CPU-GPU readback stalls), якщо ЦП очікує результатів видимості в межах одного кадру. Відкладення зчитувань на один кадр дозволяє уникнути простоїв, але спричиняє тимчасову затримку, викликаючи помітні візуальні «вискакування» (popping), коли нові видимі об'єкти не відображаються негайно. Щоб уникнути як простоїв ЦП-ГП, так і візуальних «вискакувань», сучасні виробничі архітектури використовують: 1. **Двофазне відсікання оклюзій на основі ієрархічного Z-буфера (Hi-Z), кероване ГП.** ГП перевіряє обмежувальні рамки (bounding boxes) на відповідність піраміді глибини Hi-Z, згенерованій з попереднього кадру. Об'єкти, що відомо як видимі, малюються на Фазі 1 (генеруючи початкову глибину поточного кадру). Раніше оклюдовані об'єкти повторно тестуються проти оновленого Hi-Z буфера поточного кадру на Фазі 2; будь-які нововиявлені об'єкти рендеряться негайно перед освітленням і постобробкою, усуваючи «вискакування» без будь-яких зчитувань ЦП. 2. **Програмна растеризація на ЦП.** Буфер глибини з низькою роздільною здатністю растеризується виключно на робочих потоках ЦП (використовуючи SIMD) зі спрощених мешів оклюдерів. ЦП тестує обмежувальні рамки синхронно, не потребуючи запитів ГП та не зазнаючи затримки передачі даних з ГП на ЦП. 3. **Консервативне обмеження та часовий гістерезис.** Розширення обмежувальних об'ємів (bounding volumes) або затримка понижень стану видимості дозволяють уникнути передчасного відсікання під час швидкого руху камери.

// 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;
    }
}
Відповісти на це запитання з ШІ-тренером

14Опишіть типовий конвеєр обробки ресурсів від створеної моделі до буферів GPU часу виконання, включно з генерацією тангенсів, квантуванням, валідацією та оптимізацією.

Стандартний конвеєр обробки ресурсів перетворює необроблені авторські DCC (Digital Content Creation) моделі (FBX, glTF, USD) у високопродуктивні, готові для GPU (Graphics Processing Unit) бінарні формати за допомогою п'яти основних етапів: 1. **Імпорт та валідація**: Вихідна модель очищається шляхом видалення дублікатів або невикористовуваних вершин, відкидання вироджених/нульових за площею трикутників, перевірки багатовимірної (manifold) геометрії, обробки NaN (Not a Number) значень та розділення багатоматеріальних моделей на окремі підмоделі. 2. **Генерація тангенсного простору**: Тангенси та бітангенси обчислюються за допомогою стандартизованих алгоритмів (переважно MikkTSpace) для гарантування візуальної відповідності інструментам для запікання нормалей. Це правильно враховує UV-шви та віддзеркалені UV-ділянки (зберігаючи орієнтацію у tangent.w). 3. **Оптимізація**: Індекси перевпорядковуються для ефективності кешу вершин після трансформації (наприклад, Forsyth/Tipsify), буфери вершин перевпорядковуються для локальності вибірки вершин до трансформації, а також генеруються рівні деталізації (LOD) або мешлети. 4. **Квантування та пакування атрибутів**: Атрибути вершин квантуються для зменшення обсягу пам'яті та пропускної здатності пам'яті: позиції до 16-бітних half/unorm або нормалізованих цілих чисел, нормалі та тангенси до 8-бітних SNORM (Signed Normalized Integer) або октаедричних кодувань (Oct16/Oct32), а UV-координати до 16-бітних чисел з плаваючою комою/unorm. Атрибути можуть бути перемішані (AoS (Array of Structures)) або розділені на кілька потоків (SoA (Structure of Arrays), наприклад, тільки позиції для попередніх проходів глибини). 5. **Обробка (Cooking) та серіалізація**: Буфери, обмежувальні об'єми (AABBs (Axis-Aligned Bounding Boxes)/сфери) та таблиці LOD серіалізуються у плоскі бінарні файли, що не вимагають виправлення вказівників під час виконання, забезпечуючи швидке завантаження DMA (Direct Memory Access) в буфери GPU через проміжну (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)
Відповісти на це запитання з ШІ-тренером

15Поясніть мешлети, відсікання кластерів, меш-шейдери та конвеєри щільної мікрогеометрії для великих статичних сцен.

Конвеєри мешлетів та архітектури щільної мікрогеометрії (такі як Nanite в Unreal) замінюють великі виклики малювання з індексним буфером на невеликі, обмежені геометричні кластери, що називаються 'мешлетами'. 1. **Мешлети (Meshlets):** Це кластер геометрії, зазвичай обмежений 32–128 вершинами та до 128–256 трикутниками. Кожен мешлет містить локальні індекси вершин, потоки атрибутів та попередньо обчислені дані обмеження (обмежувальна сфера та нормальний конус). 2. **Меш- та шейдери посилення (Mesh and Amplification Shaders):** Вони замінюють конвеєр вершин, складання примітивів та геометричний шейдер з фіксованою функцією. Шейдери посилення (завдань) (Amplification/Task Shaders) оцінюють відсікання за фрустумом, оклюзією та зворотними гранями нормального конуса на рівні кластера в групах мешлетів. Мешлети, що пройшли відсікання, надсилають Меш-шейдери (Mesh Shaders), де група потоків спільно перетворює вершини в розділеній пам'яті на кристалі (on-chip Shared Memory / LDS) та безпосередньо виводить індекси примітивів до растеризатора. 3. **Конвеєри щільної мікрогеометрії (Dense Micro-Geometry Pipelines):** Геометрія високої щільності створює субпіксельні трикутники, які страждають від сильного перерисування квадрів (quad-overdraw) (де стандартне апаратне забезпечення растеризує допоміжні квадри розміром 2x2 пікселі, виконуючи повні піксельні шейдери лише для 1 покритого пікселя). Сучасні системи щільної мікрогеометрії використовують ієрархічні структури LOD (рівень деталізації) кластерів (DAG – орієнтовані ациклічні графи), щоб динамічно вибирати LOD кластерів, забезпечуючи довжину ребра близько 1 пікселя, і часто поєднують апаратну растеризацію для великих полігонів з користувацькими програмними растеризаторами на основі обчислень для субпіксельних мікрополігонів.

#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);
    }
}
Відповісти на це запитання з ШІ-тренером