📐 Математика · Комп'ютерна графіка
📅 Квітень 2026⏱ 16 хв🟡 Початковий–середній рівень

Лінійна алгебра для 3D-графіки: вектори, матриці, кватерніони

Кожен трикутник, відрендерений на екрані, пройшов щонайменше чотири множення матриць, перш ніж досягти ваших очей. Матриця моделі розміщує його у світі, матриця виду рухає світ перед камерою, матриця проєкції застосовує перспективу, а матриця області перегляду відображає на піксельні координати. Розуміння лінійної алгебри за цими перетвореннями — основа всього 3D- програмування — від коду шейдерів до фізичних рушіїв і машинного навчання з 3D-даними.

1. Вектори: точки, напрямки та операції

Вектор у 3D-графіці — це кортеж (x, y, z), що представляє або точку в просторі, або напрямок з величиною. Розрізнення важливе: перенесення напрямку не змінює напрямок, але перенесення точки змінює її положення.

Основні операції

// Скалярний добуток: скалярний результат a · b = aₓbₓ + a_y b_y + a_z b_z = |a||b|cosθ Застосування: - Кут між векторами: θ = arccos(a·b / |a||b|) - Проєкція a на b: proj = (a·b̂)·b̂ де b̂ = b/|b| - Дифузне освітлення: N·L = cos(кута до світла) для затінення Ламберта - Відсікання задніх граней: if (N·V < 0) пропустити трикутник // Векторний добуток: векторний результат (перпендикулярний до обох входів) a × b = (a_y b_z − a_z b_y, a_z bₓ − aₓ b_z, aₓ b_y − a_y bₓ) |a × b| = |a||b|sinθ Застосування: - Нормаль трикутника: N = (B−A) × (C−A), потім нормалізувати - Побудова ортонормованого базису (напр. системи камери) - Момент сили: τ = r × F у фізичній симуляції - Перевірка орієнтації: (a×b)·c визначає орієнтацію системи координат // Нормалізація: зробити одиничної довжини â = a / |a| де |a| = sqrt(aₓ² + a_y² + a_z²) Примітка: нормалізація нульового вектора невизначена (потрібен захист від NaN)
Зіткнення угод: OpenGL та WebGL використовують вектори-стовпці з правим множенням (M·v). DirectX та HLSL використовують вектори-рядки з лівим множенням (v·M). Те саме геометричне перетворення вимагає транспонування матриці між цими двома угодами. GLM (C++) використовує угоду OpenGL; glMatrix (JavaScript) також використовує вектори-стовпці зі стовпцевим зберіганням (та сама розкладка масиву, що й у OpenGL).

2. Матриці та типи перетворень

Матриця 3×3 кодує будь-яке лінійне перетворення: поворот, масштабування, зсув. Матриця 4×4 кодує будь-яке афінне перетворення в 3D: лінійні перетворення плюс перенесення (через однорідні координати). Шейдери GPU та всі 3D API працюють з матрицями 4×4.

Матриці повороту (3×3)

// Поворот навколо осі X на кут θ Rₓ(θ) = | 1 0 0 | | 0 cosθ −sinθ | | 0 sinθ cosθ | // Поворот навколо осі Y на кут θ R_y(θ) = | cosθ 0 sinθ | | 0 1 0 | | −sinθ 0 cosθ | // Поворот навколо осі Z на кут θ R_z(θ) = | cosθ −sinθ 0 | | sinθ cosθ 0 | | 0 0 1 | // Поворот навколо довільної одиничної осі â на кут θ (Родриг) R = cosθ·I + (1−cosθ)·âaᵀ + sinθ·[â]× де [â]× — кососиметрична матриця векторного добутку â

Масштабування та перенесення

// Неоднорідне масштабування (4×4 однорідне) S(sₓ, s_y, s_z) = | sₓ 0 0 0 | | 0 s_y 0 0 | | 0 0 s_z 0 | | 0 0 0 1 | // Перенесення (4×4 однорідне) T(tₓ, t_y, t_z) = | 1 0 0 tₓ | | 0 1 0 t_y | | 0 0 1 t_z | | 0 0 0 1 |

Комбінування перетворень: порядок має значення

Множення матриць асоціативне, але не комутативне: повернути, потім перенести ≠ перенести, потім повернути. Угода полягає в тому, що найправіша матриця застосовується першою до вектора-стовпця:

// Застосувати: спочатку масштаб, потім поворот, потім перенесення M = T · R · S v_world = M · v_local = T · (R · (S · v_local)) ↑ застосовано останнім ↑ другим ↑ застосовано першим Помилка: M = S · R · T спершу перенесе, потім поверне, розмістивши об'єкт у зовсім іншому місці.

3. Однорідні координати

3D-перенесення не можна представити як множення матриці 3×3 — перенесення не є лінійним перетворенням (воно не зберігає початок координат). Елегантний розв'язок: підняти 3D-координати до 4D, додавши четверту компоненту w.

// 3D-точка → однорідна: (x, y, z) → (x, y, z, 1) // 3D- вектор/напрямок: (x, y, z) → (x, y, z, 0) Компонента w відрізняє точки (w=1) від напрямків (w=0): T · (x, y, z, 1)ᵀ = (x+tₓ, y+t_y, z+t_z, 1) ✓ перенесено T · (x, y, z, 0)ᵀ = (x, y, z, 0) ✓ напрямок незмінний! // Перетворення назад у 3D (перспективне ділення): (X, Y, Z, W) → (X/W, Y/W, Z/W) для W ≠ 0 Після матриці перспективної проєкції W ≠ 1 у загальному випадку. GPU виконує перспективне ділення автоматично наприкінці етапу вершинного шейдера (простір відсікання → NDC).
Однорідна точка на нескінченності: (x, y, z, 0) представляє напрямок (x, y, z) як «точку на нескінченності» — границю (tx, ty, tz, 1) при t→∞. Паралельні прямі в проєктивній геометрії перетинаються в точці на нескінченності, тому залізничні колії сходяться в точці сходу при перспективному рендерингу.

4. Конвеєр перетворень MVP

Вершина проходить через три простори координат, перш ніж стати пікселем. Кожен простір з'єднується множенням матриці:

// Повний ланцюг перетворень вершини v_clip = P · V · M · v_local M = матриця моделі (локальний простір → світовий простір) V = матриця виду (світовий простір → простір камери/ока) P = матриця проєкції (простір ока → простір відсікання) // Матриця моделі: розмістити + орієнтувати + масштабувати об'єкт M = T_world · R_world · S_local // Матриця виду: обернена до світового перетворення камери // Камера в pos, дивиться на target, вектор «вгору» — up: V = lookAt(pos, target, up) = inverse(T_pos · R_camera) // Матриця перспективної проєкції (угода OpenGL, NDC: [-1,1]) P = | 2n/(r−l) 0 (r+l)/(r−l) 0 | | 0 2n/(t−b) (t+b)/(t−b) 0 | | 0 0 −(f+n)/(f−n) −2fn/(f−n)| | 0 0 −1 0 | Спрощено для симетричної піраміди (l=−r, b=−t): fovY = вертикальне поле зору в радіанах aspect = width/height P[0][0] = 1/(aspect·tan(fovY/2)) P[1][1] = 1/tan(fovY/2) P[2][2] = −(f+n)/(f−n) P[2][3] = −2fn/(f−n) P[3][2] = −1

Після P·V·M·v результат перебуває в просторі відсікання. GPU відсікає трикутники до куба відсікання, потім виконує перспективне ділення (÷w), щоб дістатися NDC (нормалізованих координат пристрою, x∈[−1,1], y∈[−1,1], z∈[−1,1] для OpenGL). Нарешті перетворення області перегляду відображає NDC на піксельні координати:

// Перетворення області перегляду (NDC → пікселі вікна) x_px = (x_ndc + 1) / 2 · viewport_width + x_origin y_px = (y_ndc + 1) / 2 · viewport_height + y_origin // Примітка: y може бути перевернутим залежно від API (D3D перевертає, OpenGL ні)

5. Кватерніони для поворотів

Кути Ейлера (рискання, тангаж, крен) інтуїтивні, але страждають від складанного замикання (gimbal lock): коли дві осі повороту вирівнюються, губиться один ступінь свободи й плавна інтерполяція руйнується. Матриці поворотів працюють, але використовують 9 чисел для лише 3 ступенів свободи й накопичують похибку з рухомою комою за багато операцій композиції. Кватерніони розв'язують обидві проблеми, використовуючи лише 4 числа.

Кватерніон q = (w, x, y, z) = w + xi + yj + zk, де i, j, k — уявні одиниці, що задовольняють i² = j² = k² = ijk = −1. Одиничний кватерніон (|q|=1) представляє 3D-поворот:

// Кватерніон, що кодує поворот на кут θ навколо одиничної осі â=(ax,ay,az): q = (cos(θ/2), ax·sin(θ/2), ay·sin(θ/2), az·sin(θ/2)) = (w, x, y, z) // Застосування повороту до вектора v (як чистий кватерніон v=(0,v)): v_rotated = q · v · q* де q* = (w, −x, −y, −z) (спряжений = обернений для одиничного q) // Еквівалентна матрична форма (тут для відображення у рядковому порядку): R = | 1−2(y²+z²) 2(xy−wz) 2(xz+wy) | | 2(xy+wz) 1−2(x²+z²) 2(yz−wx) | | 2(xz−wy) 2(yz+wx) 1−2(x²+y²)| // Композиція двох поворотів: множення кватерніонів q_total = q₂ · q₁ (застосувати q₁ першим, потім q₂) w = w₂w₁ − x₂x₁ − y₂y₁ − z₂z₁ x = w₂x₁ + x₂w₁ + y₂z₁ − z₂y₁ y = w₂y₁ − x₂z₁ + y₂w₁ + z₂x₁ z = w₂z₁ + x₂y₁ − y₂x₁ + z₂w₁

6. Інтерполяція та плавне обертання

Для анімації нам потрібні плавні переходи між орієнтаціями. Лінійна інтерполяція елементів матриці чи кутів Ейлера дає несталу кутову швидкість та погані результати. Кватерніонна інтерполяція — стандартний розв'язок.

LERP (лінійна інтерполяція)

// Наївна лінійна інтерполяція кватерніонів — швидка, але без сталої швидкості q(t) = normalize((1−t)·q₁ + t·q₂) для t ∈ [0, 1] Використовуйте, коли: різниця швидкості непомітна (малий діапазон t) або продуктивність критична (LERP — це одне додавання + нормалізація)

SLERP (сферична лінійна інтерполяція)

// Slerp — стала кутова швидкість уздовж великого кола на одиничній 4-сфері Ω = arccos(q₁ · q₂) (скалярний добуток компонент кватерніонів) q(t) = [sin((1−t)Ω) / sinΩ]·q₁ + [sin(tΩ) / sinΩ]·q₂ Граничні випадки: - Якщо q₁·q₂ < 0: змінити знак q₂ перед slerp (короткий шлях проти довгого) - Якщо Ω ≈ 0: перейти до LERP (кватерніони майже однакові) Slerp — золотий стандарт для змішування анімації — він зберігає сталу кутову швидкість та найкоротший шлях.
Складанне замикання пояснено: З кутами Ейлера (напр. порядок Z·Y·X), якщо поворот по Y становить ±90°, осі X та Z стають паралельними — вони обидві обертаються навколо тієї самої світової осі. Один ступінь свободи губиться. Жодна комбінація поворотів X та Z тоді не може дати поворот навколо втраченої осі. Кватерніони не мають такого виродження, бо вони оперують безпосередньо на 4D-одиничній сфері.

Squad та сплайни вищого порядку

Для плавних шляхів через кілька ключових орієнтацій сам SLERP створює C¹-розриви в ключових кадрах (швидкість неперервна, але кутове прискорення — ні). Squad (сферична кубічна інтерполяція) поширює криві Безьє на простір кватерніонів, даючи C²- гладкі шляхи обертання, що використовуються в ігрових системах камери та кінематографічній анімації.

7. Перетворення нормалей та матриця нормалей

Нормалі до поверхні — це вектори напрямку, і вони перетворюються інакше, ніж точки за неоднорідного масштабування. Якщо перетворити нормаль тією самою матрицею M, що й вершини, неоднорідний масштаб спотворить нормалі та зруйнує розрахунки освітлення.

// Неправильно: N_world = M · N_local (ламається за неоднорідного масштабу) // Правильно: N_world = transpose(inverse(M)) · N_local «Матриця нормалей» = transpose(inverse(M)) Часто попередньо обчислюється на CPU і передається як uniform: uniform mat3 normalMatrix; // у GLSL Для ортогональних матриць (лише поворот, без масштабу): inverse(M) = transpose(M) → матриця нормалей = сама M (тож чистий поворот перетворює нормалі правильно тією самою матрицею) Начерк доведення: Якщо t — дотична (Mv — перетворена точка), нормаль N має залишатися перпендикулярною: N·t = 0. Після перетворення: (AN)·(Mt) = 0 → (AN)ᵀ(Mt) = 0 → Nᵀ(AᵀM)t = 0 → AᵀM = I → A = (M⁻¹)ᵀ = transpose(inverse(M)) ✓

8. Лінійна алгебра на GPU: GLSL та WGSL

Мови шейдерів GPU мають вбудовані типи лінійної алгебри. GPU виконує операції з векторами та матрицями за одну інструкцію в усіх смугах SIMD-процесора — те, що в коді CPU потребує циклу, на GPU є однією операцією.

GLSL (WebGL / OpenGL)

// Вбудовані типи GLSL vec2, vec3, vec4 // float-вектори mat2, mat3, mat4 // float-матриці (стовпцевий порядок) ivec3, uvec3, bvec3 // цілі, беззнакові, булеві вектори // Поширені операції (усі вбудовані, апаратно прискорені) float d = dot(a, b); // скалярний добуток vec3 n = cross(a, b); // векторний добуток vec3 u = normalize(v); // нормалізація (одинична довжина) float l = length(v); // евклідова довжина vec3 r = reflect(d, n); // вектор відбиття vec3 rf = refract(d, n, eta); // вектор заломлення float f = mix(a, b, t); // лінійна інтерполяція (LERP) vec3 c = clamp(v, 0.0, 1.0); // покомпонентне обмеження // Множення матриці на вектор (постмноження вектора-стовпця) vec4 v_clip = projMatrix * viewMatrix * modelMatrix * v_local; // Доступ до стовпців (mat4 — масив з 4 стовпців vec4) vec4 col0 = myMat[0]; // перший стовпець float m23 = myMat[2][3];// рядок 3, стовпець 2 (стовпцевий порядок: стовпець 2, рядок 3)

WGSL (WebGPU)

// WGSL (мова шейдерів WebGPU) — подібна, але строгіша var v: vec3f = vec3f(1.0, 0.0, 0.0); var m: mat4x4f = /* ... */; let d: f32 = dot(a, b); let n: vec3f = normalize(cross(b - a, c - a)); // Множення матриць: те саме позначення let clip_pos: vec4f = uniforms.mvp * vertex_pos; // Ключова відмінність від GLSL: явні типи всюди, // немає неявних перетворень (1 замість 1.0 — помилка компіляції)
Зауваження щодо продуктивності: На сучасних GPU множення mat4×vec4 — це одна апаратна інструкція, що виконує 16 операцій множення з накопиченням одночасно. Перетворення пакета з одного мільйона вершин займає кілька мілісекунд на GPU середнього класу — вузьким місцем зазвичай є пропускна здатність пам'яті (отримання даних вершин), а не арифметика.

Практичні поради для 3D-програмування