Стаття
Робототехніка та кінематика · ⏱ ≈ 15 хв читання · Останнє оновлення: 23 червня 2026 р.

IK FABRIK: з нуля на JavaScript

FABRIK — Forward And Backward Reaching Inverse Kinematics — розв’язує задачу позиціонування суглобів роботизованої руки так, щоб її кінчик досягав цілі. На відміну від методів на основі якобіана, FABRIK суто геометричний: він чергує підтягування ланцюга до мети (прямий прохід) та закріплення кореня (зворотний прохід), збігаючись лише за кілька ітерацій. Ми реалізуємо повний алгоритм, включно з обмеженнями кутів та інтерактивною симуляцією на canvas.

1. Задача зворотної кінематики

Роботизована рука — це ланцюг жорстких ланок, з’єднаних обертальними суглобами. Пряма кінематика (FK) проста: за кутами суглобів θ₁…θₙ обчислюємо положення кінцевого ефектора. Зворотна кінематика (IK) — це обернена задача: за бажаним положенням кінцевого ефектора знайти кути суглобів, що його дають.

Недовизначена

Для плоскої руки з 3 суглобами, що дотягується до 2D-цілі, нескінченно багато конфігурацій кутів є дійсними. Розв’язок — це сімейство, а не єдина відповідь.

Перевизначена

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

Нелінійна

Положення суглобів містять суми косинусів і синусів — система тригонометрична й загалом має кілька гілок розв’язку.

Перевага FABRIK

Жодного обернення матриць, жодних сингулярностей, швидка збіжність за ~5 ітерацій для типових поз. Працює з n-ланковими ланцюгами та розгалуженими структурами.

2. Огляд прямої кінематики

Для плоского 2D-ланцюга з n ланок довжин ℓ₁, ℓ₂, …, ℓₙ та кумулятивних кутів α₁, α₂, …, αₙ (кожен виміряний від світової осі x):

Положення суглобів (починаючи з кореня p₀): p₀ = (x₀, y₀) (якір / корінь) pᵢ = pᵢ₋₁ + ℓᵢ · (cos αᵢ, sin αᵢ) Кінцевий ефектор: p_n = Σᵢ ℓᵢ · (cos αᵢ, sin αᵢ) + p₀

Загальний радіус досяжності руки — R = Σᵢ ℓᵢ. Якщо ціль T задовольняє |T − p₀| > R, ціль поза межами досяжності, і рука має максимально витягнутися до неї.

3. Алгоритм FABRIK

FABRIK запропонували Aristidou та Lasenby у 2011 році. Кожна ітерація складається з двох проходів:

Прямий прохід — підтягуємо кінчик до цілі:

1. Встановити p_n = T (помістити кінчик у ціль) 2. Для i = n-1 до 0: dir = (pᵢ − pᵢ₊₁) / |pᵢ − pᵢ₊₁| pᵢ = pᵢ₊₁ + dir · ℓᵢ₊₁

Зворотний прохід — повторно закріплюємо корінь:

3. Встановити p₀ = root_anchor 4. Для i = 1 до n: dir = (pᵢ − pᵢ₋₁) / |pᵢ − pᵢ₋₁| pᵢ = pᵢ₋₁ + dir · ℓᵢ

Повторюємо, доки |p_n − T| < допуск (зазвичай 0.01 px) або не досягнуто максимальної кількості ітерацій (~10). Для досяжних цілей алгоритм гарантовано збігається.

Складність: кожна ітерація — O(n), лінійна за кількістю суглобів. Порівняйте з псевдооберненим якобіаном, що потребує O(n³) на крок для обернення матриці.

4. Обмеження кутів суглобів

Реальні суглоби мають обмежений діапазон руху. Обмеження застосовуються під час кожного проходу шляхом обтинання вектора напрямку до дозволеного кутового діапазону навколо напрямку попередньої кістки:

Для суглоба i з обмеженням [θ_min, θ_max]: 1. Обчислити поточний кут кістки i відносно кістки i−1 2. Обтнути: θ = clamp(θ_current, θ_min, θ_max) 3. Перерахувати напрямок з обтятим кутом 4. Встановити pᵢ = pᵢ₋₁ + clampedDir · ℓᵢ

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

5. Розгалужені ланцюги

Розгалужена рука (наприклад, кисть з кількома пальцями) має деревну структуру. FABRIK природно розширюється:

  1. Виконати прямий прохід на кожному підланцюзі до його цілі.
  2. Усереднити корені підланцюгів, щоб знайти спільну позицію кореня.
  3. Виконати зворотний прохід на стовбурі від спільного кореня вниз до справжнього якоря.
  4. Повторювати до збіжності.

Крок усереднення за замовчуванням розподіляє напруження порівну між гілками або може бути зваженим за пріоритетом підланцюга (наприклад, домінантна рука отримує більшу вагу).

6. Порівняння з якобіаном

Класичний підхід до IK використовує якобіан J функції FK, що відображає швидкість суглобів на швидкість кінцевого ефектора. Правило оновлення таке:

Δθ = Jᵀ (J Jᵀ + λ²I)⁻¹ Δe (демпфований метод найменших квадратів / Левенберга-Марквардта) e = T − p_n (похибка кінцевого ефектора) λ = коефіцієнт демпфування (уникає сингулярностей)

Методи на основі якобіана збігаються за меншу кількість ітерацій для складних цілей, краще обробляють надлишковість і можуть мінімізувати вторинні цілі (наприклад, уникати перешкод) через проєкцію в нуль-простір. Але вони потребують лінійної алгебри O(n³) на крок і можуть розбігатися поблизу сингулярностей. FABRIK кращий для інтерактивних застосунків реального часу, де надійність важливіша за оптимальність.

7. JavaScript — повна реалізація FABRIK

// Зворотна кінематика FABRIK — демо на 2D Canvas

class FABRIKChain {
  /**
   * @param {number[]} lengths  - довжини ланок [l0, l1, ..., l_{n-1}]
   * @param {{x,y}}   anchor   - фіксована позиція кореня
   */
  constructor(lengths, anchor) {
    this.lengths = lengths;
    this.n = lengths.length;
    // ініціалізуємо суглоби вертикально
    this.joints = [];
    let y = anchor.y;
    for (let i = 0; i <= this.n; i++) {
      this.joints.push({x: anchor.x, y});
      if (i < this.n) y += lengths[i];
    }
    this.anchor = {x: anchor.x, y: anchor.y};
    this.totalLength = lengths.reduce((a, b) => a + b, 0);
    this.constraints = null; // опційно [{min, max}, ...]
  }

  solve(target, maxIter = 10, tol = 0.5) {
    const J = this.joints;
    const L = this.lengths;

    // Перевірка досяжності
    const dist = Math.hypot(target.x - this.anchor.x, target.y - this.anchor.y);
    if (dist > this.totalLength) {
      // Повністю витягуємося до цілі
      for (let i = 0; i < this.n; i++) {
        const dx = target.x - J[i].x, dy = target.y - J[i].y;
        const r = Math.hypot(dx, dy);
        J[i + 1].x = J[i].x + L[i] * dx / r;
        J[i + 1].y = J[i].y + L[i] * dy / r;
      }
      return;
    }

    for (let iter = 0; iter < maxIter; iter++) {
      // --- Прямий прохід ---
      J[this.n].x = target.x; J[this.n].y = target.y;
      for (let i = this.n - 1; i >= 0; i--) {
        const dx = J[i].x - J[i + 1].x;
        const dy = J[i].y - J[i + 1].y;
        const r = Math.hypot(dx, dy);
        J[i].x = J[i + 1].x + L[i] * dx / r;
        J[i].y = J[i + 1].y + L[i] * dy / r;
      }

      // --- Зворотний прохід ---
      J[0].x = this.anchor.x; J[0].y = this.anchor.y;
      for (let i = 0; i < this.n; i++) {
        let dx = J[i + 1].x - J[i].x;
        let dy = J[i + 1].y - J[i].y;
        // застосовуємо кутові обмеження, якщо визначені
        if (this.constraints && this.constraints[i] && i > 0) {
          const prevDx = J[i].x - J[i - 1].x;
          const prevDy = J[i].y - J[i - 1].y;
          const baseAngle = Math.atan2(prevDy, prevDx);
          let relAngle = Math.atan2(dy, dx) - baseAngle;
          const {min, max} = this.constraints[i];
          relAngle = Math.max(min, Math.min(max, relAngle));
          const absAngle = baseAngle + relAngle;
          dx = Math.cos(absAngle); dy = Math.sin(absAngle);
        }
        const r = Math.hypot(dx, dy);
        J[i + 1].x = J[i].x + L[i] * dx / r;
        J[i + 1].y = J[i].y + L[i] * dy / r;
      }

      const err = Math.hypot(J[this.n].x - target.x, J[this.n].y - target.y);
      if (err < tol) break;
    }
  }

  draw(ctx, accent = '#fb923c') {
    const J = this.joints;
    // Ланки
    ctx.beginPath();
    ctx.strokeStyle = accent;
    ctx.lineWidth = 4;
    ctx.lineCap = 'round';
    ctx.moveTo(J[0].x, J[0].y);
    for (let i = 1; i < J.length; i++) ctx.lineTo(J[i].x, J[i].y);
    ctx.stroke();
    // Суглоби
    for (let i = 0; i < J.length; i++) {
      ctx.beginPath();
      ctx.fillStyle = i === 0 ? '#64748b' : i === J.length - 1 ? '#34d399' : accent;
      ctx.arc(J[i].x, J[i].y, i === 0 ? 7 : 5, 0, Math.PI * 2);
      ctx.fill();
    }
  }
}

// Використання: рука, що слідує за мишею
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const arm = new FABRIKChain([60, 50, 40, 30], {x: 200, y: 100});
let mouse = {x: 200, y: 300};
canvas.addEventListener('mousemove', e => {
  const r = canvas.getBoundingClientRect();
  mouse = {x: e.clientX - r.left, y: e.clientY - r.top};
});
function loop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  arm.solve(mouse);
  arm.draw(ctx);
  // малюємо ціль
  ctx.beginPath();
  ctx.fillStyle = '#34d399';
  ctx.arc(mouse.x, mouse.y, 6, 0, Math.PI * 2);
  ctx.fill();
  requestAnimationFrame(loop);
}
loop();

8. Застосування

Живе демо: Симуляція механізмів використовує схожий ітеративний підхід IK для анімації багатоланкових механічних з’єднань у реальному часі.
⚙️ Відкрити «Механізми» →