Lerp và Slerp trong Unity – cách dùng đúng

Cách sử dụng hàm Lerp và Slerp chính xác trong lập trình với Unity

 

     Trong bài viết này mình sẽ mô tả cách sử dụng hai hàm LerpSlerp chính xác, do 2 hàm này thường hay bị hướng dẫn cách sử dụng không được chính xác lắm trong nhiều bài hướng dẫn (cả trong lẫn ngoài nước). Có thể họ cố tình vậy vì 1 mục đích nào đó hoặc chỉ đơn giản là hiểu sai, thậm chí trên cả trang tài liệu của Unity cũng thường chỉ sai cách dùng, nhưng thôi mặc kệ, không phải việc của mình, trong bài viết này chúng ta sẽ nhìn nhận cách sử dụng chính xác của 2 hàm nội suy Lerp và Slerp.

 

     Bắt đầu nhé, trong Unity có 2 lớp là QuaternionVector đều có sử dụng 2 hàm nội suy là Lerp Slerp (không biết còn có lớp nào cũng sử dụng 2 hàm đó nữa hay không, hiện tại mình chỉ biết đến 2 lớp này), Vector thì chỉ có Vector3 là có cả 2 hàm còn Vector2 thì chỉ chứa mỗi Lerp thôi. Trong bài viết này thì mình sẽ không đề cập đến Vector hay Quaternion là gì, mặc định mọi người khi đọc bài này là đã biết tương đối về chúng rồi, hoặc nếu cảm thấy cần thiết thì mình sẽ viết ở 1 bài post nào đó khác sau 😋.

 

     E hèm, Lerp và Slerp  đều là 2 hàm nội suy và chúng đều có dạng là …(from, to, amount t). Để đề phòng ai chưa biết thì Nội suy là phương pháp ước tính giá trị của các điểm dữ liệu chưa biết trong phạm vi của 1 tập hợp rời rạc chứa 1 số điểm dữ liệu đã biết (theo Bách khoa toàn thư mở Wikipedia). Hồi còn đi học chúng ta hay có cái bài toán dạng này này: Cho hàm f(x), hãy tính hàm f(x) tại các điểm x = 1, x = 2,… đó chính là chúng ta đang nội suy để ước tính hàm f tại các điểm trung gian x đấy. (Mother nó hóa ra hồi còn đi học học toàn thứ hay ho nhưng không biết nó là cái gì và để làm gì nên học xong là bỏ xó, giờ tự nhiên phải học lại từ đầu 🤭 thiết nghĩ giáo dục của VN nên xem lại 😣).

 

     Với Vector3 thì 2 hàm đó như sau:

Vector3.Lerp(Vector3 from, Vector3 to, float t);

Vector3.Slerp(Vector3 from, Vector3 to, float t);

 

     Và với Quaternion thì là như này:

Quaternion.Lerp(Quaternion from, Quaternion to, float t);

Quaternion.SLerp(Quaternion from, Quaternion to, float t);

 

 

     Nhìn thì có vẻ 2 hàm giống y như nhau, công nhận là nó cũng giống nhau thật, cơ bản thì chúng ta sẽ không nhận ra được sự khác biệt nếu không rơi vào đúng tình huống mà bản chất của 2 hàm sẽ được thể hiện ra. Lerp là 1 phép nội suy tuyến tính (linear interpolation) và khoảng cách giữa mỗi bước là bằng nhau trên toàn bộ phép nội suy. Còn Slerp là 1 phép nội suy “tuyến tính hình cầu” (nghe là đã thấy khác nhau cmnr =_=)), nội suy được ánh xạ trên 1 phần tư của hình tròn và khoảng cách giữa mỗi bước là không tương đương nhau. Có thể nói trên 1 đường tròn thì Lerp là dây cung còn Slerp là 1 cung. Mình có vẽ 1 hình mô tả đơn giản như sau, hơi xấu tí vì programmer không rành việc của artis 😤

 

cach-su-dung-lerp-va-slerp-1

 

Ở đây là minh họa 1 phép nội suy từ điểm “From” đến điểm “To” với hàm Lerp là hình tròn xanh dương còn hàm Slerp là hình tròn xanh lá cây. Chúng ta có thể thấy 1 cách rõ ràng là trong bất cứ trường hợp nào thì hàm Lerp cũng sẽ nhanh hơn hàm Slerp (đi theo đường chim bay mà), nhưng khi các phép quay trở nên lớn, góc quay trở nên rộng (không phải rộng vừa đâu 🙃) thì hàm Lerp sẽ cho ta chất lượng kém hơn Slerp nhiều, thì 1 thằng di chuyển uốn éo lươn lẹo nhìn lại chả ngon canh hơn 1 thằng cứ thẳng đừ ra mà đi à.

 

OK, thế thôi nhé, đã nhận biết sự khác biệt cơ bản xong giờ là đến phần chủ đề chính của bài viết, sử dụng 2 hàm này làm sao cho đúng, ở đây mình sẽ chỉ minh họa trên hàm Lerptuy nhiên Slerp cũng sẽ không có sự khác biệt.

 

Có nhiều bài hướng dẫn online về Unity đã sử dụng chức năng nội suy này không được chính xác, bao gồm cả những hướng dẫn chính thức từ trang của Unity (WTF?), vào thời điểm mình học, giờ không biết là họ đã chỉnh lại chưa 😒 chắc là rồi, cũng khá lâu từ hồi đó.. E hèm, tuy nhiên hàm nội suy này rất dễ sử dụng 1 khi chúng ta hiểu cách thức nó hoạt động. Nó trả về 1 điểm giữa fromto dựa trên giá trị t (phương thức ở trên), vấn đề là t ở đây là gì? Đó là khối lượng hay có thể hiểu là phần trăm hiện tại đang hoạt động để có thể đạt đến điểm to kể từ khi bắt đầu từ điểm from, tham số t được giới hạn trong khoảng [0, 1] và ta có thể hiểu vậy là từ 0% -> 100%, khi tham số t đạt giá trị 1 nghĩa là hàm nội suy đã đạt 100% => done. Nhìn hình minh họa sau

 

cach-su-dung-lerp-va-slerp-2

 

     Ở đây mình sẽ minh họa 1 đoạn code để di chuyển 1 khối cube từ điểm A -> B và tiếp tục từ B -> C như sau:

cach-su-dung-lerp-va-slerp-3

 

Tạo 1 script đơn giản tên là: DemoCubeLerp:


public class DemoCubeLerp : MonoBehaviour
{
     public Transform aPoint = null;
     public Transform bPoint = null;
     public Transform cPoint = null;

     public float timeToMove = 1.0f;
     private float _startTime = 0.0f;
     private Vector3 _startPos = Vector3.zero;
     private Vector3 _endPos = Vector3.zero;
     private bool _isLerping = false;

     private void PrepareToLerp(Vector3 endPosition)
     {
          _startPos = transform.position;
          _endPos = endPosition;
          _startTime = Time.time;
          _isLerping = true;
     }

     private void Start()
     {
          PrepareToLerp(bPoint.position);
     }

     private void Update()
     {
          if (_isLerping)
          {
               float percentage = (Time.time - _startTime) / timeToMove;
               transform.position = Vector3.Lerp(_startPos, _endPos, percentage);
               if (percentage >= 1.0f)
               {
                    _isLerping = false;
                    if (transform.position == aPoint.position)
                         PrepareToLerp(bPoint.position);
                    else if (transform.position == bPoint.position)
                         PrepareToLerp(cPoint.position);
                    else
                         PrepareToLerp(aPoint.position);
               }
           }
     }
}

     Hàm này sẽ làm cho khối cube liên tục di chuyển từ A đến B, B đến C rồi lại C đến A, nó sẽ liên tục đi như vậy cho đến khi ta tắt máy 😂. Đây chính là cách sử dụng 2 hàm nội suy Lerp  và Slerp chính xác, với các hướng dẫn sai thì:

 

transform.position = Vector3.Lerp(_startPos, _endPos, percentage);

 

     Sẽ được viết thành:

 

transform.position = Vector3.Lerp(_startPos, _endPos, speed * Time.deltaTime);

 

     Hãy nhớ cụm speed * Time.deltaTime này là không chính xác nhé 🙂 À thêm 1 điểm ngoài lề tí là chúng ta có thể thấy cách khối cube di chuyển từ A -> B so với B -> C và C -> A là không giống nhau, vì mấu chốt trong đoạn code trên là chúng ta nội suy dựa trên timeToMove, nghĩa là khoảng cách giữa các điểm xa hay gần thì nó vẫn chỉ mất 1 khoảng thời gian duy nhất để di chuyển, vậy sẽ khá vô lý trong hầu hết các trường hợp, chúng ta cần thay đổi làm sao để khi khối cube di chuyển giữa các điểm nó vẫn luôn di chuyển với 1 “vận tốc” duy nhất, ở đây chúng ta đã có thời gian là timeToMove và quãng đường sẽ là _endPos – _startPos, với công thức tính vận tốc v = s / t các bạn hãy thử giải quyết nốt vấn đề này nhé ✌.

 

     Đây là bài viết đầu tiên của mình, nếu trong trường hợp có gì sai sót mong các bạn đọc bỏ qua nhé. Mình chỉ muốn chia sẻ kiến thức của bản thân với những người có cùng chung sở thích.  😆😆😆

 

Bạn nghĩ sao về nội dung bài viết này?

[Total: 2    Average: 5/5]

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *