Лінійна алгебра для 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θ·[â]× де [â]× — кососиметрична матриця векторного добутку â
Множення матриць асоціативне, але не комутативне:
повернути, потім перенести ≠ перенести, потім повернути. Угода полягає в тому, що
найправіша матриця застосовується першою до вектора-стовпця:
// Застосувати: спочатку масштаб, потім поворот, потім перенесення 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-програмування
Завжди нормалізуйте після операцій SLERP/з кватерніонами
— похибка з рухомою комою накопичується й зрештою робить |q| ≠ 1,
спричиняючи тонкі артефакти масштабування в матрицях поворотів.
Використовуйте glMatrix (JavaScript) або GLM (C++) для
промислової роботи — написана вручну матрична математика має тонкі помилки
стовпцевого/рядкового порядку, на налагодження яких ідуть години.
Уважно стежте за порядком множення — функції glMatrix
на кшталт mat4.multiply(out, a, b) обчислюють a·b,
тобто b застосовується першим. Багато помилок виникають через перестановку цього
порядку.
Уникайте обернення матриць на GPU —
обернення матриці з одинарною точністю, близької до виродженої,
швидко втрачає точність. Обчислюйте обернення на CPU й завантажуйте як uniform-и.
Точність глибини — перспективна проєкція
стискає глибину нелінійно, віддаючи більшість точності біля ближньої
площини. Використовуйте логарифмічний буфер глибини або обернений Z (near=1, far=0)
для масштабних сцен.