Mẹo hoạt ảnh dùng làm mượt theo hàm mũ
- Kể từ khi bắt đầu làm các công việc liên quan đến hoạt ảnh, có một kỹ thuật hoạt ảnh đơn giản mà tôi gần như luôn sử dụng.
- Kỹ thuật này được dùng ở nhiều nơi khác nhau như xoay và di chuyển camera, di chuyển nhân vật trong game theo lượt, di chuyển các phần tử UI, làm mượt thay đổi âm lượng trong thư viện âm thanh, v.v.
- Đây không phải là kỹ thuật mới, bạn có thể đã từng nghe hoặc dùng qua rồi, nhưng tôi sẽ giải thích một vài ví dụ và nguyên lý toán học của nó.
Nút gạt
- Khi tạo một thành phần UI, giả sử bạn đang làm một nút gạt.
- Vị trí công tắc của nút gạt được tính theo trạng thái: nếu bật thì là
max_x, nếu tắt thì là min_x.
- Cách này hoạt động tốt, nhưng nếu không có hoạt ảnh thì trông hơi thiếu sức sống.
- Hoạt ảnh không chỉ làm mọi thứ đẹp mắt hơn mà còn giúp người dùng hiểu điều gì đang diễn ra.
- Thay vì chuyển ngay chỉ báo của nút gạt sang vị trí mới, ta đổi sang cho nó di chuyển mượt mà.
- Giờ ta cần cập nhật hoạt ảnh, nhưng cách này có nhược điểm là trông như đang di chuyển với tốc độ cố định.
- Có thể thêm hàm easing vào đây, ví dụ dùng các hàm như
3t^2-2t^3 hoặc sqrt(t).
- Sự khác biệt giữa các hàm easing này sẽ dễ nhận thấy hơn khi phát hoạt ảnh chậm.
- Giờ đây thay vì cập nhật vị trí công tắc, ta phải theo dõi trạng thái hoạt ảnh.
- Khi dùng
sqrt, ta cần chỉ rõ các hàm easing khác nhau tùy theo hướng hoạt ảnh.
- Cái nào trông đẹp nhất còn tùy gu, nhưng
sqrt là đẹp nhất. Nó khiến công tắc bắt đầu rất nhanh nhưng giảm tốc tốt khi đến gần đích.
- Nhược điểm của phiên bản này là ngay cả trong trường hợp đơn giản nhất cũng cần quản lý khá nhiều, và nếu người dùng bấm giữa chừng khi hoạt ảnh đang chạy thì sẽ xuất hiện sự giật cục do nhảy đột ngột.
Di chuyển camera
- Giả sử có tình huống bản đồ và camera cuộn hoặc di chuyển xung quanh.
- Ở đây cũng nên thêm hoạt ảnh.
- Bài viết đưa ra đoạn mã nội suy với tốc độ hằng số.
- Hiện tượng rung xảy ra sau khi hoạt ảnh hoàn tất là vì
target.x - position.x luân phiên đổi giữa số dương và số âm.
- Thay vì
sign(delta), cần một hàm kẹp giá trị delta.
- Cách này khá phức tạp so với một việc đơn giản.
- Khi tốc độ hoạt ảnh nhanh hơn thời điểm hoạt ảnh hoàn tất thì trông sẽ kỳ lạ.
- Có thể bỏ qua đầu vào của người dùng trong lúc hoạt ảnh đang chạy, nhưng điều đó tạo ra trải nghiệm rất khó chịu.
- Lời giải hoàn hảo, tất nhiên, là làm mượt theo hàm mũ.
- Mã gần như không thay đổi nếu so với ví dụ nút gạt.
Cơ chế bên trong
- Bài viết giải thích
1 - exp(- speed * dt) là gì và nó hoạt động như thế nào.
- Bắt đầu từ phiên bản đơn giản: làm cho tốc độ di chuyển tỉ lệ với khoảng cách giữa
position hiện tại và vị trí mới target, nhờ đó chuyển động nhanh hơn.
- Cách này không cần giữ bất kỳ trạng thái nào ngoài vị trí hiện tại và vị trí mục tiêu, và tự động điều chỉnh ngay cả khi
target thay đổi đột ngột.
- Tuy nhiên có một vấn đề nhỏ. Nếu
speed * dt lớn hơn 1 thì vị trí sẽ vượt quá mục tiêu.
- Để giải quyết, có thể kẹp giá trị này về 1.
- Lý do
speed * dt quá lớn là vì hoặc giá trị speed quá lớn, hoặc dt quá lớn.
- Với hoạt ảnh, sẽ thật tốt nếu mọi thứ hoạt động hoàn hảo khi áp dụng
dt.
Phương trình vi phân (ôi, không)
- Bài viết đưa ra cách tiếp cận hai bước để giải quyết vấn đề.
- Việc
position += (target - position) * speed * dt hoạt động với dt nhỏ nhưng thất bại với dt lớn là một vấn đề điển hình của lời giải số cho phương trình vi phân.
- Bài viết tìm hiểu phương trình này thực sự giải cái gì.
- Đồng thời giải thích rằng
position += (target - position) * (1 - exp(- speed * dt)) mới là công thức đúng cho mọi dt.
Chọn tốc độ
- Ta thường nghĩ về hoạt ảnh theo thời lượng.
- Với công thức mũ, về mặt kỹ thuật hoạt ảnh sẽ hoàn tất trong thời gian vô hạn.
- Ý nghĩa của tham số
speed là 1 / speed chính là khoảng thời gian để position tiến gần target thêm một lượng theo hệ số e = 2.71828....
Làm mượt theo hàm mũ
- Nếu tìm kiếm "làm mượt theo hàm mũ", bạn có thể thấy một bài wiki trông như hoàn toàn không liên quan, nhưng thực ra nó có công thức rất giống với nội dung được bàn trong bài này.
- Nếu giả sử
dt luôn giống nhau và target thay đổi ở mỗi vòng lặp, ta có thể đánh chỉ số các giá trị theo số lần lặp để tính thứ như position[i] = (target[i] - position[i - 1]) * factor.
Tiêu đề đoạn cuối
- Tôi đã ấp ủ ý tưởng cho bài viết này suốt vài tháng và rất vui vì cuối cùng cũng hoàn thành được.
- Cảm ơn bạn đã xem và đọc nhật ký phát triển.
Ý kiến của GN⁺
- Bài viết này giải thích kỹ thuật làm mượt theo hàm mũ được dùng để khiến hoạt ảnh trông mượt và tự nhiên hơn. Kỹ thuật này góp phần cải thiện trải nghiệm người dùng và tăng tính trực quan của giao diện.
- Làm mượt theo hàm mũ cũng có thể hữu ích trong việc mô phỏng chuyển động vật lý, ví dụ trong phát triển game, nơi nó có thể được dùng để làm cho chuyển động nhân vật hoặc camera trở nên tự nhiên hơn.
- Kỹ thuật này đặc biệt hiệu quả khi các phần tử giao diện người dùng trải qua thay đổi trạng thái, vì nó thể hiện trực quan sự thay đổi đó rất tốt. Ví dụ, chuyển động của thanh trượt hoặc công tắc có thể được biểu diễn mượt mà hơn.
- Nếu nhìn từ góc độ phê bình, làm mượt theo hàm mũ có thể khiến việc kiểm soát chính xác tốc độ và thời lượng hoạt ảnh trở nên khó hơn. Đây có thể là một hạn chế khi nhà thiết kế muốn tinh chỉnh một hiệu ứng hoạt ảnh cụ thể một cách chính xác.
- Những thư viện hoặc framework hoạt ảnh khác cung cấp chức năng tương tự gồm có GreenSock Animation Platform(GSAP) hoặc anime.js, và chúng cho phép kiểm soát hoạt ảnh chi tiết hơn cùng nhiều hàm easing khác nhau.
- Khi áp dụng kỹ thuật làm mượt theo hàm mũ, cần tìm sự cân bằng giữa độ tự nhiên của hoạt ảnh và độ chính xác trong điều khiển. Lợi ích khi chọn kỹ thuật này là cải thiện trải nghiệm người dùng, còn nhược điểm là có thể khó căn chỉnh thời điểm hoạt ảnh thật chính xác.
1 bình luận
Ý kiến Hacker News
Tóm tắt bình luận thứ nhất:
smoothstep(), mà là một phương pháp phi trạng thái (stateless) xử lý nhất quán nhiều loại đầu vào khác nhau.Tóm tắt bình luận thứ hai:
Tóm tắt bình luận thứ ba:
sqrt) tốt hơn hàm bậc ba (cubic) cho công tắc gạt.Tóm tắt bình luận thứ tư:
Tóm tắt bình luận thứ năm:
Tóm tắt bình luận thứ sáu:
Tóm tắt bình luận thứ bảy:
Tóm tắt bình luận thứ tám:
Tóm tắt bình luận thứ chín:
Tóm tắt bình luận thứ mười: