2 điểm bởi GN⁺ 2025-02-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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 glslviewerffmpeg

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 glslviewerffmpeg
  • 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

 
GN⁺ 2025-02-08
Ý kiến trên Hacker News
  • Nếu muốn có biên khử răng cưa mượt mà mà không cần pass supersampling thứ hai, thì có thể dùng đạo hàm chuẩn trong SDF
    Về cơ bản là thay hàm step bằng aastep: https://github.com/glslify/glsl-aastep
  • Thật sự rất ngầu. Các lập trình viên shader như ở một chiều không gian khác
    Đâ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 float rồi nhấn shift-enter và thấy kết quả ngay lập tức khá là thỏa mãn
    • Không biết làm việc với JavaScript canvas hay các lớp trừu tượng như p5.js trên đó có mang lại cảm giác này không
      Khô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
  • Đây là một bài viết được tổ chức rất tốt để nhập môn GLSL
    Cũng có playlist YouTube liên quan của Inigo Quilez: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • Cách họ cho hình tham chiếu nhấp nháy phía trên phần xem trước để so sánh với bản gốc trong lúc chỉnh mã giống hệt cách làm trong hoạt hình vẽ tay
    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
  • Bố cục của bài này rất tốt để nhập môn GLSL
    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
    • Gần như y hệt. Vulkan và WebGL đều có thể dùng GLSL trực tiếp
      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 làm ra hoạt hình này mất 8 tháng cho thấy sự kiên trì đáng kinh ngạc
  • Tò mò không biết quá trình phát triển có phải là lặp đi lặp lại việc tinh chỉnh các giá trị nhỏ hay không, và họ đã dùng trình soạn thảo nào
    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
    • Chỉ dùng trình soạn thảo mã được nhúng ngay trên trang đó thôi
      Tìm kiếm nhị phân ngay cả khi làm bằng tay cũng khá nhanh
    • Khi làm kiểu này, người ta thường lấy một slider hoặc một giá trị đầu vào rồi nối nó với uniform
      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