Урок · Середній рівень · ~45 хв
Математика · Three.js · Обертання · slerp · Шарнірне замикання

Кватерніони — обертання без шарнірного замикання

Кватерніони описують 3D-орієнтації як чотирикомпонентне число (x, y, z, w). Вони уникають шарнірного замикання, комбінують обертання без множення матриць і плавно інтерполюються за допомогою slerp. Цей посібник вибудовує інтуїцію з нуля та пов'язує кожне поняття з API Quaternion у Three.js.

1Що таке кватерніон?

Кватерніон зберігає 3D-обертання як вісь обертання та кут, компактно закодовані чотирма числами:

q = (x, y, z, w) = sin(θ/2)·n̂ + cos(θ/2)

Де — одиничний вектор осі, а θ — кут обертання. Обмеження одиничного кватерніона — x² + y² + z² + w² = 1. Для відсутності обертання q = (0, 0, 0, 1) — одиничний (тотожний) кватерніон.

// Вручну: вісь-кут → кватерніон function fromAxisAngle(ax, ay, az, angle) { const half = angle / 2; const s = Math.sin(half); return { x: ax * s, y: ay * s, z: az * s, w: Math.cos(half) }; } // Обертання на 90° навколо Y const q = fromAxisAngle(0, 1, 0, Math.PI / 2); // Результат: { x:0, y:0.7071, z:0, w:0.7071 }
Кватерніони існують у 4D-просторі — на одиничній сфері в 4D. І q, і -q описують те саме обертання (подвійне покриття). Саме тому slerp може обирати короткий шлях, перевіряючи знак скалярного добутку.

2Кути Ейлера та шарнірне замикання

Кути Ейлера (рискання, тангаж, крен) застосовують три послідовні обертання. Проблема: коли тангаж досягає ±90°, рискання та крен збігаються — об'єкт втрачає один ступінь свободи. Це і є шарнірне замикання (gimbal lock).

// Обертання Ейлера у Three.js — залежить від порядку! mesh.rotation.order = 'YXZ'; // рискання → тангаж → крен mesh.rotation.y = Math.PI / 4; // рискання 45° mesh.rotation.x = Math.PI / 2; // тангаж 90° → ШАРНІРНЕ ЗАМИКАННЯ mesh.rotation.z = Math.PI / 4; // цей крен тепер на тій самій осі, що й рискання! // Кватерніонний еквівалент завжди безпечний: const q = new THREE.Quaternion(); q.setFromEuler(new THREE.Euler(Math.PI/2, Math.PI/4, Math.PI/4, 'YXZ')); mesh.quaternion.copy(q); // без шарнірного замикання, та сама орієнтація

3Добуток Гамільтона — комбінування обертань

Щоб застосувати обертання B після обертання A, перемножте кватерніони: q = B * A. Зверніть увагу на порядок: правий кватерніон застосовується першим (як і в угоді про множення матриць).

// Добуток Гамільтона двох кватерніонів function mulQ(a, b) { return { x: a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y, y: a.w*b.y - a.x*b.z + a.y*b.w + a.z*b.x, z: a.w*b.z + a.x*b.y - a.y*b.x + a.z*b.w, w: a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z, }; } // Обертання вектора кватерніоном: v' = q * (0,v) * q⁻¹ // У Three.js: v.applyQuaternion(q) // Three.js — комбінуємо два обертання const qA = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), Math.PI/2); const qB = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0), Math.PI/4); const combined = qB.clone().multiply(qA); // B після A mesh.quaternion.copy(combined);

4Перетворення: вісь-кут, матриця, Ейлер

import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js'; const q = new THREE.Quaternion(); // З вісь-кут q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 3); // З об'єкта Euler q.setFromEuler(new THREE.Euler(0.1, 0.5, 0.2, 'XYZ')); // З матриці обертання (витягує кватерніон з верхнього блоку 3×3) const mat = new THREE.Matrix4().makeRotationY(Math.PI / 6); q.setFromRotationMatrix(mat); // З двох векторів напрямку (обертання ВІД a ДО b) const from = new THREE.Vector3(0, 1, 0); // вгору const to = new THREE.Vector3(1, 0, 0); // праворуч q.setFromUnitVectors(from, to); // ДО Ейлера const euler = new THREE.Euler().setFromQuaternion(q, 'YXZ'); // ДО вісь-кут (відновлюємо кодування) const axis = new THREE.Vector3(); const angle = 2 * Math.acos(q.w); axis.set(q.x, q.y, q.z).normalize();

5Slerp — плавна інтерполяція обертання

Slerp (Spherical Linear intERPolation — сферична лінійна інтерполяція) рухається вздовж великого кола на одиничній 4D-сфері, забезпечуючи сталу кутову швидкість і плавний результат — на відміну від лінійного lerp на кутах Ейлера.

const qStart = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, 0)); const qEnd = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, Math.PI, 0)); const qCur = new THREE.Quaternion(); // У циклі анімації (t йде 0 → 1) function animate(t) { // Завжди обираємо найкоротшу дугу — інвертуємо, якщо скалярний добуток від'ємний if (qStart.dot(qEnd) < 0) qEnd.negate(); qCur.slerpQuaternions(qStart, qEnd, t); mesh.quaternion.copy(qCur); } // Squad — плавний сплайн кватерніонів (через кілька ключових кадрів) const squad = new THREE.Quaternion(); THREE.Quaternion.slerpFlat( squad.toArray(), 0, // вихід qStart.toArray(), 0, // від qEnd.toArray(), 0, // до t // 0..1 );
Nlerp проти Slerp: Nlerp (нормалізація лінійно-інтерпольованого кватерніона) швидший і дає майже ідентичні результати для малих кутів (t < 0.3). Використовуйте Slerp для плавних анімацій камери, персонажа чи обертань на великий кут. Використовуйте Nlerp у фізичних інтеграторах, де переважають малі кути.

6Шпаргалка з API Quaternion у Three.js

const q = new THREE.Quaternion(x, y, z, w); // за замовчуванням одиничний (0,0,0,1) // Зміна q.identity() // скидання до відсутності обертання q.copy(other) // копіювання з іншого кватерніона q.set(x, y, z, w) // задаємо компоненти напряму q.normalize() // забезпечуємо одиничну довжину (виправлення дрейфу) // Комбінування q.multiply(other) // q = q * other (множення справа) q.premultiply(other) // q = other * q (множення зліва) q.conjugate() // інвертуємо одиничний кватерніон (заперечуємо x,y,z) // Інтерполяція q.slerp(qEnd, t) // slerp на місці у напрямку qEnd THREE.Quaternion.slerpFlat(dst,dOff, srcA,aOff, srcB,bOff, t) // статичний метод для пласких масивів // Застосування до вектора const rotated = someVec3.clone().applyQuaternion(q); // Витягування console.log(q.x, q.y, q.z, q.w); const arr = q.toArray(); // [x, y, z, w] const axis = new THREE.Vector3(); const angle = 2 * Math.acos(q.w); if (angle > 0.00001) axis.set(q.x, q.y, q.z).divideScalar(Math.sin(angle / 2));