درک چرخش سه‌بعدی در Three.js: زوایای اویلر، کواترنیون‌ها و Gimbal Lock

چرخش یکی از مهم‌ترین مفاهیم در برنامه‌نویسی سه‌بعدی است.

در نگاه اول، چرخاندن یک آبجکت ساده به نظر می‌رسد:

mesh.rotation.y = Math.PI / 2;

اما وقتی اپلیکیشن پیچیده‌تر می‌شود — دوربین‌ها، انیمیشن‌ها، فیزیک، کنترل کاراکتر یا صحنه‌های تعاملی — چرخش به یکی از گیج‌کننده‌ترین موضوع‌ها تبدیل می‌شود.

دلیلش ساده است: یک چرخش سه‌بعدی را می‌توان به چند شکل مختلف نمایش داد. در Three.js دو نمایش مهم وجود دارد:

  • زوایای اویلر (Euler Angles)
  • کواترنیون‌ها (Quaternions)

اینکه کی و چرا از هر کدام استفاده کنیم، یک مهارت پایه برای هر توسعه‌دهنده سه‌بعدی است.

۱. چرخش در فضای سه‌بعدی یعنی چه؟

فرض کنید کتابی در دست دارید. می‌توانید:

  • آن را به جلو و عقب خم کنید.
  • آن را به چپ و راست بچرخانید.
  • آن را مثل یک چرخ بچرخانید.

در دنیای سه‌بعدی این حرکت‌ها حول سه محور انجام می‌شوند:

      Y
      ↑
      |
      |
      O --------→ X
     /
    /
   Z

هر آبجکت می‌تواند حول محورهای X، Y و Z بچرخد. در Three.js:

mesh.rotation.x;
mesh.rotation.y;
mesh.rotation.z;

۲. چرخش با زوایای اویلر

زوایای اویلر چرخش را با سه مقدار توصیف می‌کنند: چرخش حول X، حول Y و حول Z.

دوربینی روی سه‌پایه را تصور کنید. جهتش را این‌طور توصیف می‌کنید: ۲۰ درجه به بالا، ۴۵ درجه به راست، ۱۰ درجه کج. این همان ایده زوایای اویلر است.

mesh.rotation.set(
  0,
  Math.PI / 4,
  0
);

یعنی X برابر ۰ درجه، Y برابر ۴۵ درجه و Z برابر ۰ درجه. Three.js مقدار Object3D.rotation را به شکل یک آبجکت Euler نگه می‌دارد.

درجه یا رادیان

کامپیوترها معمولاً به جای درجه از رادیان استفاده می‌کنند. تبدیل‌های رایج:

180° = Math.PI
 90° = Math.PI / 2
 45° = Math.PI / 4

mesh.rotation.y = Math.PI / 2; آبجکت را ۹۰ درجه حول محور Y می‌چرخاند. روش خواناتر:

mesh.rotation.y = THREE.MathUtils.degToRad(90);

۳. ترتیب چرخش: چرا مهم است؟

اینجا خیلی از توسعه‌دهنده‌ها غافلگیر می‌شوند. کتاب را در نظر بگیرید: اول آن را به بالا بچرخانید و بعد به راست. نتیجه با حالتی که اول به راست و بعد به بالا بچرخانید فرق دارد.

چرخش مثل جمع اعداد نیست: چرخش X ← چرخش Y با چرخش Y ← چرخش X یکی نیست. زوایای اویلر یک ترتیب دارند:

mesh.rotation.order = "XYZ";

ترتیب پیش‌فرض در Three.js برابر XYZ است، اما می‌توان آن را تغییر داد.

۴. مشکل: Gimbal Lock

Gimbal Lock یکی از بزرگ‌ترین ضعف‌های زوایای اویلر است. برای درکش، دوربینی را تصور کنید که داخل سه حلقه چرخان نصب شده — هر حلقه برای یک محور. در حالت عادی حلقه‌های X، Y و Z سه چرخش مستقل به ما می‌دهند.

حالا سیستم را ۹۰ درجه حول X بچرخانید. اتفاق غیرمنتظره‌ای می‌افتد: دو محور چرخش هم‌راستا می‌شوند. سیستم یک درجه آزادی از دست می‌دهد و حرکتی که باید حول Y باشد ممکن است مثل حرکت حول Z رفتار کند.

آبجکت خراب نشده است؛ مشکل از شیوه نمایش است. زوایای اویلر چرخش را به صورت دنباله‌ای از چرخش‌های محوری توصیف می‌کنند، پس بعضی جهت‌ها نتیجه مبهم می‌سازند.

مثال واقعی: هواپیما

هواپیما سه حرکت دارد: Pitch (دماغه بالا و پایین)، Yaw (گردش به چپ و راست) و Roll (چرخش بال‌ها). در حالت عادی این سه مستقل‌اند. اما اگر هواپیما مستقیم به سمت بالا باشد، دو محور روی هم می‌افتند و سیستم دیگر نمی‌تواند بین «دارم به چپ می‌چرخم؟» و «دارم Roll می‌کنم؟» تمایز بگذارد. این همان Gimbal Lock است.

۵. چرا کواترنیون‌ها وجود دارند 🤩

کواترنیون روش دیگری برای نمایش چرخش است. به جای «X را ۲۰ درجه، Y را ۴۰ درجه و Z را ۱۰ درجه بچرخان»، کواترنیون جهت نهایی آبجکت را توصیف می‌کند.

یک تشبیه خوب، ترک‌بال است. وقتی توپی را در دست می‌گیرید، فکر نمی‌کنید «اول حول X، بعد Y، بعد Z». فقط توپ را با دست می‌چرخانید و موقعیت نهایی‌اش مهم است — کواترنیون به این ایده نزدیک‌تر است.

۶. کواترنیون در Three.js

کواترنیون چهار مقدار دارد: x، y، z و w.

const q = new THREE.Quaternion();
console.log(q); // Quaternion { x, y, z, w }

این اعداد مثل موقعیت نیستند که x یعنی راست، y یعنی بالا و z یعنی جلو. مقادیر کواترنیون فقط در کنار هم معنا دارند — درست مثل RGB که یک کانال به‌تنهایی رنگ را تعیین نمی‌کند، اما ترکیبشان تعیین می‌کند.

۷. ساختن یک چرخش کواترنیونی

رایج‌ترین متد setFromAxisAngle() است: «حول این محور به این اندازه بچرخ». چرخاندن آبجکت ۲۰ درجه حول Y:

const quaternion = new THREE.Quaternion();
const axis = new THREE.Vector3(0, 1, 0);
const angle = THREE.MathUtils.degToRad(20);

quaternion.setFromAxisAngle(axis, angle);
mesh.quaternion.copy(quaternion);

۸. ترکیب چرخش‌ها: copy() در برابر multiply()

یکی از بزرگ‌ترین مزیت‌های کواترنیون این است که می‌توانیم چرخش‌ها را با خیال راحت ترکیب کنیم. گوشی‌ای را در دست تصور کنید: اول ۳۰ درجه به راست می‌چرخانید، بعد ۲۰ درجه دیگر به بالا کج می‌کنید. جهت نهایی ترکیب هر دو حرکت است. با زوایای اویلر این کار پیچیده می‌شود چون ترتیب محورها مهم است؛ با کواترنیون چرخش‌ها را با ضرب ترکیب می‌کنیم.

copy(): جایگزین کردن چرخش فعلی

copy() یعنی «چرخش قبلی را فراموش کن و این را استفاده کن»:

const q = new THREE.Quaternion();
q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(45));
mesh.quaternion.copy(q);

multiply(): اضافه کردن یک چرخش دیگر

multiply() یعنی «این چرخش جدید را روی چرخش فعلی اعمال کن»:

const spin = new THREE.Quaternion();
spin.setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(20));
mesh.quaternion.multiply(spin);

مثل چرخاندن فرمان ماشین: موقعیت فعلی + چرخش اضافه = نتیجه ترکیبی.

۹. کاربردی‌ترین متد: setFromUnitVectors()

گاهی زاویه چرخش را نمی‌دانیم و فقط می‌دانیم: «این آبجکت به این سمت نگاه می‌کند و می‌خواهم به سمت دیگری نگاه کند». چراغ‌قوه‌ای را تصور کنید که به بالا اشاره دارد و می‌خواهیم به جلو اشاره کند.

const from = new THREE.Vector3(0, 1, 0); // جهت فعلی
const to = new THREE.Vector3(1, 0, 0);   // جهت هدف

const q = new THREE.Quaternion();
q.setFromUnitVectors(from, to);
mesh.quaternion.copy(q);

این یکی از بزرگ‌ترین تفاوت‌های این دو است: اویلر می‌پرسد «حول هر محور چند درجه؟» و کواترنیون می‌پرسد «این جهت را چطور به آن جهت برسانم؟»

۱۰. کواترنیون و تفکر دنیای واقعی

کره جغرافیایی را در دست تصور کنید. فکر نمی‌کنید «X را ۱۵ درجه و بعد Y را ۴۰ درجه بچرخانم» — فقط کره را می‌چرخانید. کواترنیون جهت نهایی آبجکت را ذخیره می‌کند، نه تاریخچه اینکه چطور به آن رسیدیم.

۱۱. چرخش نرم با slerp()

گاهی نمی‌خواهیم آبجکت ناگهان به چرخش جدید بپرد: دوربینی که آرام به هدف نگاه می‌کند، رباتی که سرش را می‌چرخاند، سفینه‌ای که مسیرش را عوض می‌کند. برای این‌ها از slerp() استفاده می‌کنیم — درون‌یابی خطی کروی، یا به زبان ساده «حرکت نرم از یک چرخش به چرخش دیگر»:

mesh.quaternion.slerp(targetQuaternion, 0.1);

مقدار 0.1 یعنی در هر فریم «۱۰٪ به سمت چرخش هدف حرکت کن».

۱۲. اویلر یا کواترنیون: کی از کدام استفاده کنیم؟

وقتی به زاویه فکر می‌کنید از اویلر استفاده کنید — «این آبجکت را ۳۰ درجه بچرخان»:

mesh.rotation.y = THREE.MathUtils.degToRad(30);

مناسب چرخش‌های ساده، کنترل‌های رابط کاربری، زاویه‌های مشخص و ورودی‌های ادیتور.

وقتی به جهت یا ترکیب چرخش‌ها فکر می‌کنید از کواترنیون استفاده کنید — «این آبجکت را به آن سمت نگاه کن»، «این چرخش را روی چرخش فعلی اعمال کن»، «بین دو جهت نرم حرکت کن». مناسب دوربین، انیمیشن، فیزیک، کنترل کاراکتر و تعامل‌های سه‌بعدی پیچیده.

۱۳. رابطه اویلر و کواترنیون در Three.js

این دو، دو چرخش متفاوت نیستند — دو روش نمایش یک چرخش‌اند. Three.js هر دو را نگه می‌دارد: mesh.rotation نمایش اویلر و mesh.quaternion نمایش کواترنیونی است. پشت صحنه:

Euler → Quaternion → Matrix4 → GPU Rendering

۱۴. یک مثال کاربردی

روش ساده با اویلر:

function animate() {
  mesh.rotation.y += 0.01;
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

روش کواترنیونی:

const rotation = new THREE.Quaternion();
rotation.setFromAxisAngle(new THREE.Vector3(0, 1, 0), 0.01);

function animate() {
  mesh.quaternion.multiply(rotation);
  renderer.render(scene, camera);
  requestAnimationFrame(animate);
}

روش دوم وقتی چند چرخش را ترکیب می‌کنید کنترل بیشتری می‌دهد.

مدل ذهنی نهایی

  • اویلر: «حول این سه محور بچرخ» (X ← Y ← Z). فهمش آسان است اما ممکن است به Gimbal Lock برخورد کند.
  • کواترنیون: «آبجکت این جهت را داشته باشد.» اولش سخت‌تر است اما بسیار قدرتمندتر.

جمع‌بندی

چرخش فقط تغییر سه عدد نیست. سؤال مهم این است: «زاویه‌ها را می‌دانم؟» یا «جهتی که می‌خواهم را می‌دانم؟»

اگر زاویه‌ها را می‌دانید، معمولاً اویلر کافی است. اگر جهت دلخواه را می‌دانید یا باید چرخش‌ها را ترکیب کنید، کواترنیون ابزار درست است. وقتی این تفاوت را بفهمید، خیلی از موضوع‌های پیشرفته Three.js ساده‌تر می‌شوند.