- Chỉ với 240 dòng mã GLSL, có thể tạo hoạt hình Rick; không cần hình ảnh hay thư viện, shader GPU sẽ tính màu của từng pixel theo thời gian
- Cốt lõi là
color_for_pixel(pixel, time) được chạy trên mọi pixel, và trường khoảng cách có dấu (SDF) biểu diễn bên trong/bên ngoài cùng đường viền của hình bằng giá trị khoảng cách
- Khuôn mặt được tạo bằng cách kết hợp các hình SDF như
round_rect(), circle(), star(), bezier(), parabola() với các phép tịnh tiến, thay đổi tỉ lệ, đối xứng, lặp lại và hợp nhất
- Gợn sóng tóc, lặp răng, viền môi, chớp mắt, di chuyển con ngươi, lắc đầu được thêm dần bằng domain warping,
mod(), sin(time), noise()
- Bản hoàn chỉnh còn có cả nền cổng không gian, nhưng trình biên tập không có chức năng xuất video, nên cần một quy trình macOS riêng dùng
glslviewer và ffmpeg
Cấu trúc shader tính màu theo từng pixel
- Hoạt hình được viết bằng OpenGL Shading Language (GLSL)
- Có thể chạy và chỉnh sửa ví dụ ngay trong trình live coding trên trang
- Điểm vào cơ bản là hàm
color_for_pixel(vec2 pixel, float time)
- GPU chạy hàm này cho mọi pixel trong bản xem trước
- Câu hỏi mà hàm trả lời là: “Pixel này ở thời điểm này nên có màu gì?”
- Ví dụ đơn giản đưa các giá trị
pixel.x, pixel.y, time vào các kênh màu để trực quan hóa vị trí pixel và sự thay đổi theo thời gian
time là số giây đã trôi qua kể từ lần chỉnh sửa cuối cùng và liên tục tăng
Cách vẽ hình bằng SDF
- Hình tròn có thể được vẽ bằng cách lấy khoảng cách từ gốc tọa độ đến pixel bằng
length(pixel), rồi so sánh với bán kính
- Hàm
circle() không chỉ trả về bool cho biết bên trong hay bên ngoài, mà trả về khoảng cách đến chu vi
- Bên trong hình là giá trị âm
- Bên ngoài hình là giá trị dương
- Biên là giá trị gần 0
- Loại hàm này là hàm trường khoảng cách có dấu (SDF)
- Dùng giá trị khoảng cách cho phép xử lý phần tô và đường viền theo cùng một cách
dist < -0.01 thì dùng màu bên trong
dist < 0.0 thì dùng viền màu đen
- Nhiều hình SDF có thể được gộp bằng
min()
- Nếu xử lý riêng viền đầu và tai của Rick thì sẽ xuất hiện đường thừa giữa chúng
- Gộp hai giá trị khoảng cách bằng
min() sẽ chỉ giữ lại đường viền của phần hợp giữa đầu và tai
- Còn có các cách kết hợp khác; tài liệu tham khảo có bài 2D distance functions của Inigo Quilez và tài liệu về kết hợp primitive
Các hình tạo nên khuôn mặt Rick
- Hình đầu được tạo bằng
round_rect(), và tai cũng được thêm bằng một round_rect() riêng
- Tác giả chồng hình Rick từ poster mùa 1 lên bản xem trước theo kiểu nhấp nháy để căn chỉnh thông số
- Nhiều con số được tìm ra bằng quá trình thử sai này
- Giá trị màu được lấy bằng công cụ eyedropper của trình chỉnh sửa ảnh
- Mắt được tạo từ SDF hình tròn và
star() 6 cánh
- Nhãn cầu được kéo hơi dài theo chiều dọc bằng biến đổi tọa độ như
pixel.y *= .93
- Con ngươi được làm tròn các góc bằng cách trừ một giá trị khoảng cách nhỏ khỏi SDF ngôi sao 6 cánh
- Hai mắt được tạo bằng đối xứng trái-phải với
pixel.x = abs(pixel.x) thay vì sao chép mã
- Mũi, miệng, lông mày được vẽ bằng
bezier()
- Tóc bắt đầu từ một
star() 11 cánh được kéo dài theo chiều dọc
- Thứ tự của mã vẽ hình cũng ảnh hưởng đến kết quả
- Màu được trả về trước sẽ là hình nằm phía trước
- Răng và lưỡi chỉ được vẽ bên trong câu lệnh điều kiện của hình miệng nên không lộ ra ngoài miệng
Kỹ thuật tinh chỉnh tóc, răng và các đường trang trí
- Mái tóc hình sao cứng được biến thành dạng gợn sóng bằng domain warping
- Domain warping là cách làm rung tọa độ bằng các độ lệch ngẫu nhiên dựa trên vị trí pixel
- Cùng một vị trí sẽ nhận cùng một độ lệch, nên có thể tạo ra biến dạng nhất quán theo thời gian
- Tài liệu liên quan được dẫn là bài domain warping của Inigo Quilez
- Răng được tạo từ một SDF
parabola() rồi lặp ngang bằng mod()
mod(pixel.x, width) khiến tọa độ x quay lại từ 0 sau mỗi khoảng cố định, để lặp lại cùng một hình
pixel.y = abs(pixel.y)-.06 dùng để đối xứng răng trên và răng dưới
- Độ lệch y dựa trên
pow(pixel.x, 2.) giúp khớp răng theo đường cong của nụ cười
- Các điều kiện như
abs(pixel.x+.06) < .194 được dùng để giới hạn vòng lặp vô hạn
- Đường viền môi và đường dưới mắt được vẽ bằng cách đẩy đường viền SDF ra phía ngoài
- Viền thông thường là
abs(distance_to_shape) < thickness
- Viền đẩy ra ngoài là
abs(distance_to_shape - outset) < thickness
- Đường dưới mắt còn được thêm điều kiện vị trí để chỉ hiện ở một vùng nhất định dưới mắt
Cách tạo chuyển động bằng thời gian
- Kiểu hoạt hình đơn giản nhất là đưa
sin(time) vào mã
sin() bọc giá trị time tăng liên tục vào khoảng từ -1 đến 1 để tạo hoạt hình lặp lại
- Phạm vi được điều chỉnh bằng scale và offset như
sin(time)*.5 + .5
- Chuyển động xoay đầu, xoay lưỡi và độ cao lông mày của Rick được tạo theo cách này
- Phép tính xoay có thêm hàm
rotateAt()
- Chớp mắt được triển khai bằng thay đổi đối tượng được vẽ theo thời gian
- Nếu
mod(time, 2.) < .09 thì vẽ mắt nhắm
- Nếu không thì vẽ mắt mở và con ngươi
- Chuyển động bất quy tắc của con ngươi dùng
noise()
- Để mắt không di chuyển mượt liên tục,
round(time) được áp dụng trước rồi truyền vào noise()
- Tóc chuyển động mềm hơn nhờ time domain warping
- Thay vì làm méo không gian, nó làm trễ giá trị thời gian theo vị trí
- Càng gần ngọn tóc thì độ trễ thời gian càng khác nhau, nên toàn bộ mái tóc không xoay cứng mà uốn cong
Nền cổng không gian và phần kết
- Phiên bản cuối cùng được hoàn thiện bằng cách thêm hiệu ứng cổng không gian phía sau Rick
- Theo chú thích, hiệu ứng cổng dựa trên một hiệu ứng do người dùng ShaderToy valena tạo ra, và là phiên bản đã được rút gọn bởi FabriceNeyret2
- Mã nguồn ưu tiên tính dễ đọc hơn hiệu năng
- Toàn bộ kết quả gói gọn các kỹ thuật kết hợp hình, SDF, warping, lặp, chuyển đổi theo thời gian và chuyển động dựa trên noise cho hoạt hình shader 2D trong một ví dụ duy nhất
Quy trình export video
- Trình biên tập trên trang hiện chưa thể xuất hoạt hình thành video
- Một cách tạm thời được đề xuất là quy trình macOS dùng glslviewer và ffmpeg
- Ví dụ cài đặt dependency như sau
brew install glslviewer ffmpeg
- Script export tạo thư mục tạm và chạy
glslViewer ở chế độ headless
- Độ phân giải:
1920x1080
- Chuỗi thời gian: từ
0 giây đến 7 giây
- Tốc độ khung hình:
60
- Tệp kết quả:
animation.mp4
- Ví dụ live coding cục bộ dùng lệnh sau
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0
Supersampling và điểm khởi đầu của bài viết
- Lý do các cạnh của hình trông mượt là vì phía sau có áp dụng supersampling
- Supersampling hoạt động bằng cách gọi
color_for_pixel() tại 9 vị trí bên trong một pixel màn hình rồi hiển thị giá trị trung bình
- Khi dùng
#version 300 es, chế độ “pro” của trình biên tập sẽ được bật và tính năng supersampling tự động bị loại bỏ
- Điểm khởi đầu của bài viết là video I Made a 3D Modeler, in C, in a Week được công bố 8 tháng trước
- Video đó có phần hoạt hình giải thích thuật toán marching cubes
- Tác giả cảm thấy rất khó làm chính xác và nhanh bằng phần mềm hoạt hình thông thường nên bắt đầu làm bằng mã
- Về sau nhiều người hỏi cách tạo hoạt hình, nên nội dung được tổng hợp thành bài viết này
1 bình luận
Ý kiến trên Hacker News
Về cơ bản là thay hàm
stepbằngaastep: https://github.com/glslify/glsl-aastepĐây là kiểu làm việc dày đặc và lặp đi lặp lại, rất khác với phát triển web, giao thức hay ứng dụng mà nhiều người quen thuộc
Việc chỉnh một
floatrồi nhấnshift-entervà thấy kết quả ngay lập tức khá là thỏa mãncanvashay các lớp trừu tượng như p5.js trên đó có mang lại cảm giác này khôngKhông rõ là đang nói về lập trình đồ họa nói chung hay cụ thể hơn là làm shader trên GPU
Cũng có playlist YouTube liên quan của Inigo Quilez: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
Lập trình shader thật sự là một thế giới khác, và bài viết cũng rất tuyệt
Tò mò không biết làm việc này trong Vulkan hay WebGPU/WebGL sẽ có cảm giác như thế nào
Chính xác hơn thì Vulkan là luồng từ GLSL sang SPIR-V
WebGPU chạy trong trình duyệt thì về mặt kỹ thuật không dùng GLSL trực tiếp, nhưng các triển khai WebGPU native có thể nhận GLSL và cũng có thể chuyển đổi
Hoặc cứ dùng WGSL, về cơ bản nó gần giống GLSL, chỉ khác là cú pháp lấy cảm hứng từ Rust thay vì kiểu C
Việc tạo ra 240 dòng với các giá trị thập phân phù hợp bằng thử-sai có vẻ như sẽ tốn khá nhiều thời gian
Tìm kiếm nhị phân ngay cả khi làm bằng tay cũng khá nhanh
Uniform được truyền vào shader và có thể cập nhật mà không cần biên dịch lại