Як я шукаю між цими значеннями (наприклад, відтінком або обертанням)?


25

приклад спільної анімації

Переглянути демонстраційну версію

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

Відповідний код

// ease the current angle to the target angle
joint.angle += ( joint.targetAngle - joint.angle ) * 0.1;

// get angle from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;  
joint.targetAngle = Math.atan2( dy, dx );

Як я можу змусити його обертатись на найкоротшій відстані, навіть "через розрив"?


Використовуйте модуль. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts

1
@VaughanHilts Не знаю, як би я скористався цією ситуацією. Чи можете ви детальніше розробитись?
jackrugile

Відповіді:


11

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

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

Коли lerping відтінок значення, ви можете замінити hueз [cos(hue), sin(hue)]вектором.

У вашому випадку, лерпіруючи нормалізований напрямок суглоба:

// get normalised direction from joint to mouse
var dx = e.clientX - joint.x,
    dy = e.clientY - joint.y;
var len = Math.sqrt(dx * dx + dy * dy);
dx /= len ? len : 1.0; dy /= len ? len : 1.0;
// get current direction
var dirx = cos(joint.angle),
    diry = sin(joint.angle);
// ease the current direction to the target direction
dirx += (dx - dirx) * 0.1;
diry += (dy - diry) * 0.1;

joint.angle = Math.atan2(diry, dirx);

Код може бути коротшим, якщо ви можете використовувати 2D векторний клас. Наприклад:

// get normalised direction from joint to mouse
var desired_dir = normalize(vec2(e.clientX, e.clientY) - joint);
// get current direction
var current_dir = vec2(cos(joint.angle), sin(joint.angle));
// ease the current direction to the target direction
current_dir += (desired_dir - current_dir) * 0.1;

joint.angle = Math.atan2(current_dir.y, current_dir.x);

Дякую, тут чудово працює: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Я розумію, що ви робите, але чи можете ви трохи детальніше розкрити рядок 5 свого коду під час нормалізації? Не впевнений, що саме там відбувається dx /= len...
jackrugile

1
Поділ вектора на його довжину називається нормалізацією . Це забезпечує його довжину 1. len ? len : 1.0Частина просто уникає поділу на нуль, в рідкісному випадку, якщо миша розміщується точно в положенні суглоба. Це могло бути написано: if (len != 0) dx /= len;.
sam hocevar

-1. Ця відповідь у більшості випадків далеко не оптимальна. Що робити, якщо ви інтерполюєте між і 180°? У векторній формі: [1, 0]і [-1, 0]. Інтерполяція векторів дасть вам або , 180°або розділити на 0 помилки, у випадку t=0.5.
Gustavo Maciel

@GustavoMaciel - це не "більшість випадків", це один дуже специфічний кутовий випадок, який просто ніколи не трапляється на практиці. Також немає поділу на нуль, перевірте код.
sam hocevar

@GustavoMaciel знову перевіривши код, він насправді надзвичайно безпечний і працює точно так, як слід, навіть у описаному вами кутовому випадку.
sam hocevar

11

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

У цьому випадку ви можете спробувати наступне: (Я ніколи раніше не програмував у Javascript, тому пробачте мій стиль кодування.)

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;
  joint.angle += ( joint.targetAngle - joint.angle ) * joint.easing;

EDIT : У цій реалізації занадто швидко переміщення курсору по центру суглоба призводить до його ривків. Це призначена поведінка, оскільки кутова швидкість суглоба завжди пропорційна dtheta. Якщо така поведінка небажана, проблему можна легко усунути, поставивши ковпачок на кутове прискорення суглоба.

Для цього нам потрібно буде стежити за швидкістю суглоба і накласти максимальне прискорення:

  joint = {
    // snip
    velocity: 0,
    maxAccel: 0.01
  },

Тоді для нашої зручності ми введемо функцію відсікання:

function clip(x, min, max) {
  return x < min ? min : x > max ? max : x
}

Тепер наш код руху виглядає приблизно так. Спочатку ми обчислюємо, dthetaяк і раніше, коригуючи joint.angleпри необхідності:

  var dtheta = joint.targetAngle - joint.angle;
  if (dtheta > Math.PI) joint.angle += 2*Math.PI;
  else if (dtheta < -Math.PI) joint.angle -= 2*Math.PI;

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

  var targetVel = ( joint.targetAngle - joint.angle ) * joint.easing;
  joint.velocity = clip(targetVel,
                        joint.velocity - joint.maxAccel,
                        joint.velocity + joint.maxAccel);
  joint.angle += joint.velocity;

Це створює плавний рух навіть при переключенні напрямків, виконуючи обчислення лише в одному вимірі. Крім того, це дозволяє самостійно регулювати швидкість і прискорення суглоба. Дивіться демонстрацію тут: http://codepen.io/anon/pen/HGnDF/


Цей метод дійсно близький, але якщо я занадто швидко натискаю мишкою, вона починає трохи стрибати неправильно. Демонструйте тут, дайте мені знати, якщо я не реалізував це правильно: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile

1
Я не впевнений, що ви маєте на увазі, трохи стрибнувши неправильно; для мене суглоб завжди рухається в правильному напрямку. Звичайно, якщо ви занадто швидко рухаєте мишкою по центру, ви перегоняєте суглоб, і він смикається, коли він переходить від руху в одному напрямку в інший. Це призначена поведінка, оскільки швидкість обертання завжди пропорційна dtheta. Чи мали намір суглоб мати деякий імпульс?
Девід Чжан

Ознайомтесь з моєю останньою редакцією щодо способу усунення ривкового руху, створюваного обтяженням суглоба.
Девід Чжан

Гей, спробував ваше демо-посилання, але, схоже, він просто вказує на моє оригінальне. Ви зберегли нову? Якщо це не добре, я мав би змогу реалізувати це пізніше сьогодні ввечері і подивитися, як це працює. Я думаю, що те, що ти описав у своїй редакції, - це більше те, що я шукав. Звернемось до вас, дякую!
jackrugile

1
О, так, ти маєш рацію. Вибачте, моя помилка. Я це виправлю.
Девід Чжан

3

Я люблю інші дані відповіді. Дуже технічна!

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

double MAX_ANGLE = 360.0;
double startAngle = 300.0;
double endAngle = 15.0;
double distanceForward = 0.0;  // Clockwise
double distanceBackward = 0.0; // Counter-Clockwise

// Calculate both distances, forward and backward:
distanceForward = endAngle - startAngle;    // -285.00
distanceBackward = startAngle - endAngle;   // +285.00

// Which direction is shortest?
// Forward? (normalized to 75)
if (NormalizeAngle(distanceForward) < NormalizeAngle(distanceBackward)) {
    // Adjust for 360/0 degree wrap
    if (endAngle < startAngle) endAngle += MAX_ANGLE; // Will be above 360
}

// Backward? (normalized to 285)
else {
    // Adjust for 360/0 degree wrap
    if (endAngle > startAngle) endAngle -= MAX_ANGLE; // Will be below 0
}

// Now, Lerp between startAngle and endAngle. 

// EndAngle can be above 360 if wrapping clockwise past 0, or
// EndAngle can be below 0 if wrapping counter-clockwise before 0.
// Normalize each returned Lerp value to bring angle in range of 0 to 360 if required.  Most engines will automatically do this for you.


double NormalizeAngle(double angle) {
    while (angle < 0) 
        angle += MAX_ANGLE;
    while (angle >= MAX_ANGLE) 
        angle -= MAX_ANGLE;
    return angle;
}

Я просто створив це в браузері і ніколи не тестувався. Я сподіваюся, що я зрозумів логіку в першій спробі.

[Редагувати] 2017/06/02 - трохи уточнив логіку.

Почніть з обчислення відстані вперед та відстані назад, і дозвольте результатам вийти за межі діапазону (0-360).

Нормалізуючі кути повертають ці значення в діапазон (0-360). Для цього ви додаєте 360, поки значення не перевищує нуль, і віднімаєте 360, а значення вище 360. Отримані кути початку / кінця будуть еквівалентними (-285 - це те саме, що 75).

Далі ви знайдете найменший нормований кут відстані вперед або відстань назад. відстаньВперед у прикладі стає 75, що менше нормованого значення відстаніВперед (300).

Якщо distanceForward є найменшим AND endAngle <startAngle, продовжте endAngle за межею 360, додавши 360. (це стає 375 у прикладі).

Якщо distanceBackward найменший І endAngle> startAngle, подовжте endAngle до нижче 0, віднімаючи 360.

Тепер ви переглянете від startAngle (300) до нового endAngle (375). Двигун повинен автоматично регулювати значення вище 360, віднімаючи 360 для вас. Інакше вам доведеться лерпувати від 300 до 360, ТОГО лерп від 0 до 15, якщо двигун не нормалізує значення для вас.


Ну, хоча atan2основана ідея проста, це може заощадити простір та трохи продуктивності (не потрібно зберігати х і у, ні занадто часто обчислювати гріх, cos і atan2). Я думаю, що варто трохи розібратися в тому, чому це рішення є правильним (тобто вибір найкоротшого шляху по колу чи сфері, як SLERP для кватерніонів). Це питання та відповіді мають бути викладені у вікі спільноти, оскільки це справді поширене питання, з яким стикаються більшість ігрових програмістів.
теодрон

Чудова відповідь. Мені також подобається інша відповідь, яку дав @David Zhang. Однак я завжди отримую дивний тремтіння, використовуючи його відредаговане рішення, коли роблю швидкий поворот. Але ваша відповідь ідеально вписується в мою гру. Мене цікавить теорія математики, що стоїть за вашою відповіддю. Хоча це виглядає просто, але не очевидно, чому нам слід порівнювати нормовану кутову відстань різних напрямків.
newguy

Я радий, що мій код працює. Як уже згадувалося, це було щойно введено у браузер, а не перевірене в реальному проекті. Я навіть не пам'ятаю, щоб відповісти на це запитання (три роки тому)! Але, дивлячись на це, схоже, я просто розширював діапазон (0-360), щоб тестові значення вийшли за межі / перед цим діапазоном, щоб порівняти загальну різницю градусів і взяти найменшу різницю. Нормалізація просто знову приводить ці значення до діапазону (0-360). Так відстань вперед стає 75 (-285 + 360), що менше, ніж відстаньВперед (285), тому це найкоротша відстань.
Doug.McFarlane

Оскільки distanceForward - найкоротша відстань, використовується перша частина пункту IF. Оскільки endAngle (15) менше, ніж startAngle (300), ми додамо 360, щоб отримати 375. Отже, ви б лерп від startAngle (300) до нового endAngle (375). Двигун повинен автоматично регулювати значення вище 360, віднімаючи 360 для вас.
Doug.McFarlane

Я відредагував відповідь, щоб додати більше ясності.
Doug.McFarlane
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.