Làm thế nào để tôi lerp giữa các giá trị vòng lặp (chẳng hạn như màu sắc hoặc xoay)?


25

ví dụ hoạt hình chung

Xem bản demo

Tôi đang cố gắng làm cho khớp xoay trơn tru xung quanh tâm của khung vẽ, hướng về góc của con trỏ chuột. Những gì tôi có tác dụng, nhưng tôi muốn nó làm động khoảng cách ngắn nhất có thể để đến góc chuột. Sự cố xảy ra khi các vòng lặp giá trị xung quanh tại đường ngang ( 3.14-3.14). Di chuột qua khu vực đó để xem cách chuyển hướng và phải đi đường dài xung quanh.

Mã liên quan

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

Làm thế nào tôi có thể làm cho nó xoay khoảng cách ngắn nhất, thậm chí là "trên khoảng cách"?



1
@VaughanHilts Không chắc tôi sẽ sử dụng tình huống của mình như thế nào. Bạn có thể giải thích thêm?
jackrugile

Câu trả lời:


11

Nó không phải luôn luôn là phương pháp tốt nhất và nó có thể tốn kém hơn về mặt tính toán (mặc dù điều này cuối cùng phụ thuộc vào cách bạn lưu trữ dữ liệu của bạn), nhưng tôi sẽ đưa ra lập luận rằng việc nới lỏng các giá trị 2D hoạt động hợp lý trong phần lớn các trường hợp. Thay vì nới một góc mong muốn, bạn có thể lerp vectơ hướng chuẩn hóa mong muốn .

Một ưu điểm của phương pháp này so với phương pháp chọn đường đi ngắn nhất đến phương thức góc góc là nó hoạt động khi bạn cần nội suy giữa hơn hai giá trị.

Khi nới các giá trị màu, bạn có thể thay thế huebằng một [cos(hue), sin(hue)]vectơ.

Trong trường hợp của bạn, nới lỏng hướng khớp chuẩn hóa:

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

Mã có thể ngắn hơn nếu bạn có thể sử dụng lớp vectơ 2D. Ví dụ:

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

Cảm ơn bạn, điều này đang hoạt động rất tốt ở đây: codepen.io/jackrugile/pen/45c356f06f08ebea0e58daa4d06d204f Tôi hiểu hầu hết những gì bạn đang làm, nhưng bạn có thể giải thích thêm một chút về dòng 5 của mã trong quá trình chuẩn hóa không? Không chắc chắn chính xác những gì đang xảy ra ở đó dx /= len...
jackrugile

1
Chia một vectơ theo chiều dài của nó được gọi là chuẩn hóa . Nó đảm bảo nó có chiều dài 1. Phần len ? len : 1.0chỉ tránh sự phân chia bằng 0, trong trường hợp hiếm hoi là chuột được đặt chính xác tại vị trí khớp. Nó có thể đã được viết : if (len != 0) dx /= len;.
sam hocevar

-1. Câu trả lời này là xa tối ưu trong hầu hết các trường hợp. Điều gì xảy ra nếu bạn đang nội suy giữa 180°? Ở dạng vector: [1, 0][-1, 0]. Vectơ Interpolating sẽ cung cấp cho bạn một trong hai , 180°hoặc chia cho 0 lỗi, trong trường hợp t=0.5.
Gustavo Maciel

@GustavoMaciel không phải là hầu hết các trường hợp khác, đó là một trường hợp góc rất cụ thể không bao giờ xảy ra trong thực tế. Ngoài ra, không có phân chia cho số không, kiểm tra mã.
sam hocevar 24/07/2015

@GustavoMaciel đã kiểm tra lại mã, nó thực sự cực kỳ an toàn và hoạt động chính xác như trong trường hợp góc bạn mô tả.
sam hocevar 24/07/2015

11

Bí quyết là hãy nhớ rằng các góc (ít nhất là trong không gian Euclide) được định kỳ bằng 2 * pi. Nếu chênh lệch giữa góc hiện tại và góc mục tiêu quá lớn (tức là con trỏ đã vượt qua ranh giới), chỉ cần điều chỉnh góc hiện tại bằng cách thêm hoặc trừ 2 * pi tương ứng.

Trong trường hợp này, bạn có thể thử các cách sau: (Tôi chưa bao giờ lập trình bằng Javascript trước đây, vì vậy hãy tha thứ cho phong cách mã hóa của tôi.)

  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 : Trong quá trình thực hiện này, việc di chuyển con trỏ quá nhanh xung quanh tâm khớp khiến nó bị giật. Đây là hành vi dự định, vì vận tốc góc của khớp luôn tỷ lệ thuận với dtheta. Nếu hành vi này là không mong muốn, vấn đề có thể dễ dàng được khắc phục bằng cách đặt một nắp trên gia tốc góc của khớp.

Để làm điều này, chúng ta sẽ cần theo dõi vận tốc của khớp và tăng tốc tối đa:

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

Sau đó, để thuận tiện, chúng tôi sẽ giới thiệu chức năng cắt:

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

Bây giờ, mã chuyển động của chúng tôi trông như thế này. Đầu tiên, chúng tôi tính toán dthetanhư trước, điều chỉnh joint.anglekhi cần thiết:

  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;

Sau đó, thay vì di chuyển khớp ngay lập tức, chúng tôi tính toán vận tốc mục tiêu và sử dụng clipđể buộc nó trong phạm vi chấp nhận được.

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

Điều này tạo ra chuyển động trơn tru, ngay cả khi chuyển hướng, trong khi thực hiện các phép tính chỉ trong một chiều. Hơn nữa, nó cho phép vận tốc và gia tốc của khớp được điều chỉnh độc lập. Xem bản demo tại đây: http://codepen.io/anon/pen/HGnDF/


Phương pháp này thực sự rất gần, nhưng nếu con chuột của tôi quá nhanh, nó bắt đầu nhảy sai một chút. Trình diễn ở đây, cho tôi biết nếu tôi không thực hiện đúng: codepen.io/jackrugile/pen/db40aee91e1c0b693346e6cec4546e98
jackrugile

1
Tôi không chắc ý của bạn là gì khi nhảy sai một chút; Đối với tôi, khớp luôn luôn đi đúng hướng. Tất nhiên, nếu bạn di chuyển chuột xung quanh trung tâm quá nhanh, bạn sẽ chạy ra khỏi khớp và nó giật khi nó chuyển từ di chuyển theo hướng này sang hướng khác. Đây là hành vi dự định, vì tốc độ quay luôn tỷ lệ thuận với dtheta. Bạn có ý định cho khớp để có một số động lực?
David Zhang

Xem bản chỉnh sửa cuối cùng của tôi để biết cách loại bỏ chuyển động giật được tạo ra bằng cách vượt qua khớp.
David Zhang

Hey, đã thử liên kết demo của bạn, nhưng có vẻ như nó chỉ trỏ đến liên kết ban đầu của tôi. Bạn đã lưu một cái mới? Nếu không ổn, tôi sẽ có thể thực hiện điều này vào tối nay và xem nó hoạt động như thế nào. Tôi nghĩ những gì bạn mô tả trong bản chỉnh sửa của bạn là nhiều hơn những gì tôi đang tìm kiếm. Sẽ lấy lại cho bạn, cảm ơn!
jackrugile

1
Ồ, vâng, bạn đúng. Xin lỗi, là lỗi của tôi. Tôi sẽ sửa nó.
David Zhang

3

Tôi thích các câu trả lời khác được đưa ra. Rất kỹ thuật!

Nếu bạn muốn, tôi có một phương pháp rất đơn giản để thực hiện điều này. Chúng tôi sẽ giả định góc cho những ví dụ này. Khái niệm này có thể được ngoại suy thành các loại giá trị khác, chẳng hạn như màu sắc.

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

Tôi chỉ tạo cái này trong trình duyệt và chưa bao giờ được thử nghiệm. Tôi hy vọng tôi có logic ngay lần thử đầu tiên.

[Chỉnh sửa] 2017/06/02 - Làm rõ logic một chút.

Bắt đầu bằng cách tính toán distanceForward và distanceBackwards, và cho phép các kết quả mở rộng ra ngoài phạm vi (0-360).

Bình thường hóa các góc đưa các giá trị đó trở lại phạm vi (0-360). Để thực hiện việc này, bạn thêm 360 cho đến khi giá trị trên 0 và trừ 360 trong khi giá trị cao hơn 360. Các góc bắt đầu / kết thúc sẽ tương đương (-285 giống với 75).

Tiếp theo bạn tìm góc Chuẩn hóa nhỏ nhất của khoảng cách Khoảng cách tiến hoặc Khoảng cách lùi. distanceForward trong ví dụ trở thành 75, nhỏ hơn giá trị chuẩn hóa của distanceBackward (300).

Nếu distanceForward là nhỏ nhất VÀ endAngle <startAngle, hãy mở rộng endAngle ngoài 360 bằng cách thêm 360. (ví dụ: nó trở thành 375).

Nếu distanceBackward là nhỏ nhất VÀ endAngle> startAngle, hãy mở rộng endAngle xuống dưới 0 bằng cách trừ 360.

Bây giờ bạn sẽ chuyển từ startAngle (300) sang endAngle mới (375). Công cụ sẽ tự động điều chỉnh các giá trị trên 360 bằng cách trừ 360 cho bạn. Nếu không, bạn sẽ phải lerp từ 300 đến 360, THÌ lerp từ 0 đến 15 nếu động cơ không bình thường hóa các giá trị cho bạn.


Chà, mặc dù atan2ý tưởng dựa trên rất đơn giản, nhưng ý tưởng này có thể tiết kiệm một số không gian và một chút hiệu suất (không cần lưu trữ x và y, cũng không phải tính toán sin, cos và atan2 quá thường xuyên). Tôi nghĩ rằng đáng để mở rộng một chút về lý do tại sao giải pháp này là chính xác (nghĩa là chọn con đường ngắn nhất trên một vòng tròn hoặc hình cầu, giống như SLERP làm cho các câu hỏi). Câu hỏi và câu trả lời này nên được đưa vào wiki cộng đồng vì đây là vấn đề thực sự phổ biến mà hầu hết các lập trình viên trò chơi phải đối mặt.
teodron

Câu trả lời chính xác. Tôi cũng thích câu trả lời khác được đưa ra bởi @David Zhang. Tuy nhiên, tôi luôn nhận được một jitter lạ bằng cách sử dụng giải pháp chỉnh sửa của anh ấy khi tôi thực hiện một bước nhanh. Nhưng câu trả lời của bạn phù hợp hoàn hảo trong trò chơi của tôi. Tôi quan tâm đến lý thuyết toán học đằng sau câu trả lời của bạn. Mặc dù có vẻ đơn giản nhưng không rõ ràng tại sao chúng ta nên so sánh khoảng cách góc chuẩn hóa của các hướng khác nhau.
newguy ngày

Tôi rất vui vì mã của tôi hoạt động. Như đã đề cập, điều này chỉ được gõ vào trình duyệt, không được thử nghiệm trong một dự án thực tế. Tôi thậm chí không thể nhớ trả lời câu hỏi này (ba năm trước)! Nhưng, nhìn qua, có vẻ như tôi chỉ mở rộng phạm vi (0-360) để cho phép các giá trị thử nghiệm vượt xa / trước phạm vi này để so sánh tổng độ chênh lệch và lấy chênh lệch ngắn nhất. Bình thường hóa chỉ đưa các giá trị đó vào phạm vi (0-360) một lần nữa. Vì vậy, distanceForward trở thành 75 (-285 + 360), nhỏ hơn distanceBackward (285), vì vậy đây là khoảng cách ngắn nhất.
Doug.McFarlane

Vì distanceForward là khoảng cách ngắn nhất, nên phần đầu tiên của mệnh đề IF được sử dụng. Vì endAngle (15) nhỏ hơn startAngle (300), chúng tôi thêm 360 để nhận 375. Vì vậy, bạn sẽ lerp từ startAngle (300) sang endAngle mới (375). Công cụ sẽ tự động điều chỉnh các giá trị trên 360 bằng cách trừ 360 cho bạn.
Doug.McFarlane

Tôi chỉnh sửa câu trả lời để thêm rõ ràng hơn.
Doug.McFarlane
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.