Bagaimana cara saya lerp antara nilai-nilai yang diulang (seperti rona atau rotasi)?

25

contoh animasi bersama

Lihat Demo

Saya mencoba untuk membuat sambungan memutar dengan lancar di sekitar tengah kanvas, ke arah penunjuk mouse. Apa yang saya punya bekerja, tapi saya ingin menghidupkan jarak sesingkat mungkin ke sudut mouse. Masalah terjadi ketika nilai loop di sekitar garis horizontal ( 3.14dan -3.14). Arahkan mouse ke area itu untuk melihat bagaimana arah berubah dan dibutuhkan waktu yang lama untuk kembali.

Kode yang relevan

// 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 );

Bagaimana saya bisa membuatnya memutar jarak terpendek, bahkan "melintasi celah"?

jackrugile
sumber
Gunakan modulo. : D en.wikipedia.org/wiki/Modular_arithmetic
Vaughan Hilts
1
@ VaughanHilts Tidak yakin bagaimana saya menggunakannya dalam situasi saya. Bisakah Anda menguraikan lebih jauh?
jackrugile

Jawaban:

11

Ini tidak selalu merupakan metode terbaik, dan itu bisa lebih mahal secara komputasi (meskipun ini pada akhirnya tergantung pada bagaimana Anda menyimpan data Anda), tetapi saya akan membuat argumen bahwa nilai-nilai lerping 2D bekerja cukup baik di sebagian besar kasus. Alih-alih sudut sudut yang diinginkan, Anda dapat lerp vektor arah dinormalisasi yang diinginkan .

Salah satu keuntungan dari metode ini daripada metode “pilih rute terpendek ke sudut” adalah bahwa metode ini berfungsi ketika Anda perlu menyisipkan di antara lebih dari dua nilai.

Saat menyaring nilai rona, Anda bisa menggantinya huedengan [cos(hue), sin(hue)]vektor.

Dalam kasus Anda, lerping arah sendi yang dinormalisasi:

// 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);

Kode bisa lebih pendek jika Anda dapat menggunakan kelas vektor 2D. Contohnya:

// 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);
sam hocevar
sumber
Terima kasih, ini bekerja dengan baik di sini: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Saya mengerti sebagian besar dari apa yang Anda lakukan, tetapi dapatkah Anda menguraikan sedikit lebih banyak pada baris 5 kode Anda selama normalisasi? Tidak yakin apa yang sebenarnya terjadi di sana dx /= len...
jackrugile
1
Membagi vektor dengan panjangnya disebut normalisasi . Itu memastikan ia memiliki panjang 1. Bagian len ? len : 1.0hanya menghindari pembagian dengan nol, dalam kasus yang jarang bahwa mouse ditempatkan tepat pada posisi bersama. Ini bisa saja ditulis: if (len != 0) dx /= len;.
sam hocevar
-1. Jawaban ini jauh dari optimal dalam banyak kasus. Bagaimana jika Anda menginterpolasi antara dan 180°? Dalam bentuk vektor: [1, 0]dan [-1, 0]. Interpolasi vektor akan memberikan Anda baik , 180°, atau bagi dengan 0 error, dalam kasus t=0.5.
Gustavo Maciel
@ GustavoMaciel itu bukan "kebanyakan kasus", itu satu kasus sudut yang sangat spesifik yang tidak pernah terjadi dalam praktek. Juga, tidak ada pembagian dengan nol, periksa kodenya.
sam hocevar
@ GustavoMaciel telah memeriksa kode lagi, itu sebenarnya sangat aman dan bekerja persis seperti yang seharusnya bahkan dalam kasus sudut yang Anda gambarkan.
sam hocevar
11

Kuncinya adalah untuk mengingat bahwa sudut (setidaknya dalam ruang Euclidean) adalah periodik dengan 2 * pi. Jika perbedaan antara sudut saat ini dan sudut target terlalu besar (mis. Kursor telah melewati batas), sesuaikan sudut arus dengan menambahkan atau mengurangi 2 * pi.

Dalam hal ini, Anda dapat mencoba yang berikut ini: (Saya belum pernah memprogram dalam Javascript sebelumnya, jadi maafkan gaya koding saya.)

  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 : Dalam implementasi ini, memindahkan kursor terlalu cepat di sekitar pusat sambungan menyebabkannya menyentak. Ini adalah perilaku yang dimaksudkan, karena kecepatan sudut sendi selalu sebanding dengan dtheta. Jika perilaku ini tidak diinginkan, masalah dapat dengan mudah diperbaiki dengan menempatkan tutup pada percepatan sudut sambungan.

Untuk melakukan ini, kita harus melacak kecepatan sambungan dan memaksakan akselerasi maksimum:

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

Kemudian, untuk kenyamanan kami, kami akan memperkenalkan fungsi kliping:

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

Sekarang, kode gerakan kami terlihat seperti ini. Pertama, kami menghitung dthetaseperti sebelumnya, menyesuaikan joint.angleseperlunya:

  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;

Kemudian, alih-alih menggerakkan sambungan segera, kami menghitung kecepatan target dan menggunakannya clipuntuk memaksanya dalam rentang yang dapat kami terima.

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

Ini menghasilkan gerakan yang halus, bahkan saat berganti arah, saat melakukan perhitungan hanya dalam satu dimensi. Selain itu, memungkinkan kecepatan dan percepatan sambungan disesuaikan secara independen. Lihat demo di sini: http://codepen.io/anon/pen/HGnDF/

David Zhang
sumber
Metode ini datang sangat dekat, tetapi jika saya mouse saya terlalu cepat, itu mulai sedikit melompat ke arah yang salah. Demo di sini, beri tahu saya jika saya tidak menerapkannya dengan benar: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile
1
Saya tidak yakin apa yang Anda maksud dengan melompat sedikit ke arah yang salah; bagi saya, sambungan selalu bergerak ke arah yang benar. Tentu saja, jika Anda menggerakkan mouse Anda di tengah terlalu cepat, Anda kehabisan sendi, dan tersentak saat beralih dari bergerak dalam satu arah ke yang lain. Ini adalah perilaku yang dimaksudkan, karena kecepatan rotasi selalu sebanding dengan dtheta. Apakah Anda berniat untuk bersama memiliki momentum?
David Zhang
Lihat hasil edit terakhir saya untuk cara menghilangkan gerakan tersentak yang dibuat dengan melampaui sambungan.
David Zhang
Hai, coba tautan demo Anda, tetapi sepertinya itu hanya menunjuk ke yang asli saya. Apakah Anda menyimpan yang baru? Jika tidak baik-baik saja, saya harus bisa menerapkannya nanti malam dan melihat kinerjanya. Saya pikir apa yang Anda jelaskan dalam suntingan Anda lebih seperti yang saya cari. Akan kembali kepada Anda, terima kasih!
jackrugile
1
Oh, ya, kamu benar. Maaf, kesalahan saya. Saya akan memperbaikinya.
David Zhang
3

Saya suka jawaban lain yang diberikan. Sangat teknis!

Jika Anda mau, saya punya metode yang sangat sederhana untuk mencapai ini. Kami akan mengasumsikan sudut untuk contoh-contoh ini. Konsep ini dapat diekstrapolasi ke tipe nilai lainnya, seperti warna.

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;
}

Saya baru saja membuat ini di browser dan belum pernah diuji. Saya harap saya mendapatkan logika yang benar pada percobaan pertama.

[Sunting] 2017/06/02 - Jelaskan sedikit logika.

Mulai dengan menghitung distanceForward dan distanceBackwards, dan biarkan hasilnya melampaui batas (0-360).

Sudut normalisasi mengembalikan nilai-nilai tersebut ke kisaran (0-360). Untuk melakukan ini, Anda menambahkan 360 hingga nilainya di atas nol, dan kurangi 360 sementara nilainya di atas 360. Sudut awal / akhir yang dihasilkan akan menjadi setara (-285 sama dengan 75).

Anda selanjutnya menemukan sudut Normalisasi terkecil dari distanceForward atau distanceBackward. distanceForward dalam contoh menjadi 75, yang lebih kecil dari nilai normal distanceBackward (300).

Jika distanceForward adalah yang terkecil DAN endAngle <startAngle, perpanjang endAngle di luar 360 dengan menambahkan 360. (menjadi 375 dalam contoh).

Jika distanceBackward adalah yang terkecil DAN endAngle> startAngle, perpanjang endAngle ke bawah 0 dengan mengurangi 360.

Anda sekarang akan beralih dari startAngle (300) ke endAngle baru (375). Mesin harus secara otomatis menyesuaikan nilai di atas 360 dengan mengurangi 360 untuk Anda. Kalau tidak, Anda harus lerp dari 300 ke 360, MAKA lerp dari 0 hingga 15 jika mesin tidak menormalkan nilai untuk Anda.

Doug.McFarlane
sumber
Yah, walaupun atan2ide dasarnya sederhana, yang ini bisa menghemat ruang dan sedikit kinerja (tidak perlu menyimpan x dan y, juga untuk menghitung dosa, cos dan atan2 terlalu sering). Saya pikir perlu sedikit diperluas tentang mengapa solusi ini benar (yaitu memilih jalur terpendek pada lingkaran atau bola, seperti halnya SLERP untuk quaterions). Pertanyaan dan jawaban ini harus dimasukkan dalam komunitas wiki karena ini adalah masalah yang sangat umum dihadapi kebanyakan programmer gameplay.
teodron
Jawaban yang bagus Saya juga suka jawaban lain yang diberikan oleh @ David Zhang. Namun saya selalu mendapatkan kegugupan aneh menggunakan solusi yang diedit ketika saya berbelok cepat. Tetapi jawaban Anda sangat cocok dengan permainan saya. Saya tertarik pada teori matematika di balik jawaban Anda. Meskipun terlihat sederhana tetapi tidak jelas mengapa kita harus membandingkan jarak sudut dinormalisasi dari arah yang berbeda.
newguy
Saya senang bahwa kode saya berfungsi. Seperti yang disebutkan, ini baru diketik di browser, tidak diuji dalam proyek yang sebenarnya. Saya bahkan tidak ingat menjawab pertanyaan ini (tiga tahun lalu)! Tapi, setelah melihatnya, sepertinya saya baru saja memperluas rentang (0-360) untuk memungkinkan nilai tes melampaui / sebelum kisaran ini untuk membandingkan perbedaan derajat total, dan mengambil perbedaan terpendek. Normalisasi hanya membawa nilai-nilai itu ke kisaran (0-360) lagi. Jadi distanceForward menjadi 75 (-285 + 360), yang lebih kecil dari distanceBackward (285), jadi itu adalah jarak terpendek.
Doug.McFarlane
Karena distanceForward adalah jarak terpendek, bagian pertama dari klausa IF digunakan. Karena endAngle (15) kurang dari startAngle (300), kami menambahkan 360 untuk mendapatkan 375. Jadi, Anda akan beralih dari startAngle (300) ke endAngle baru (375). Mesin harus secara otomatis menyesuaikan nilai di atas 360 dengan mengurangi 360 untuk Anda.
Doug.McFarlane
Saya mengedit jawaban untuk menambahkan lebih banyak kejelasan.
Doug.McFarlane