- Tổng kết trải nghiệm tự tay hiện thực một game engine nhỏ kèm hai game demo trong 3 tháng học Vulkan
- Từng bước vượt qua độ phức tạp của Vulkan dựa trên kinh nghiệm OpenGL sẵn có, hiện thực các tính năng cốt lõi như tải glTF, skinning và shadow mapping
- Engine có tên EDBR (Elias Daler’s Bikeshed Engine), gồm khoảng 19 nghìn dòng mã, sử dụng các kỹ thuật đồ họa hiện đại như bindless descriptor, PVP, BDA
- Bài viết chia sẻ các chi tiết triển khai thực tế như những thư viện thiết yếu vk-bootstrap, VMA, volk cùng mẫu pipeline, tự động hóa build shader, quản lý đồng bộ hóa
- Việc chuyển sang Vulkan mang lại loại bỏ trạng thái toàn cục, kiểm soát tường minh, môi trường debug tốt hơn, tính nhất quán giữa các GPU, và có kế hoạch bổ sung render graph, font SDF, hiệu ứng thể tích trong tương lai
Tổng quan về việc học Vulkan và phát triển engine
- Tác giả bắt đầu học lập trình đồ họa theo kiểu tự học và đã từng viết một engine 3D bằng OpenGL cách đây khoảng một năm rưỡi
- Engine dựa trên Vulkan phù hợp với game nhỏ theo level, tập trung vào mục tiêu học hỏi và thử nghiệm hơn là tối ưu hiệu năng
- Ban đầu tác giả làm một game 3D đơn giản, rồi tách các phần có thể tái sử dụng để dần engine hóa
- Lý do có thể hoàn thành trong 3 tháng là vì giới hạn nó thành một engine cho mục đích cụ thể, chứ không phải engine đa dụng
Lộ trình học lập trình đồ họa
- Với người mới bắt đầu, nên học từ OpenGL để làm quen với hiển thị model có texture, ánh sáng Blinn-Phong, shadow mapping, v.v.
- Các tài liệu được khuyến nghị gồm learnopengl.com, Anton’s OpenGL 4 Tutorials, bài giảng của Thorsten Thormählen
- Bài viết cũng nhấn mạnh tầm quan trọng của việc hiểu đại số tuyến tính (vector, ma trận, quaternion) cùng với tài liệu thực hành OpenGL 4.6 hiện đại
Lời khuyên để tránh bike-shedding
- Tránh thiết kế quá mức và trừu tượng hóa không cần thiết, giữ nguyên tắc “chỉ hiện thực thứ cần ngay lúc này”
- Khuyến khích cách tiếp cận “cứ làm cho chạy trước rồi cải thiện sau”
- Hoàn thành một game nhỏ trước sẽ hiệu quả hơn là nhắm ngay đến engine đa dụng
- Đừng sao chép nguyên xi cấu trúc hay mã nguồn phức tạp của người khác, hãy bắt đầu từ cấu trúc đơn giản
Lý do chọn Vulkan
- Với game AAA thường dùng DirectX, trên macOS/iOS là Metal, còn web chủ yếu dùng WebGPU/WebGL
- Tác giả ưu tiên mã nguồn mở và công nghệ tiêu chuẩn, đồng thời chọn Vulkan vì phù hợp với mục tiêu phát triển game 3D nhỏ trên desktop
- OpenGL không còn được phát triển thêm và đã bị loại bỏ trên macOS
- WebGPU tuy gọn hơn nhưng vẫn có các hạn chế như độ ổn định chưa cao, giới hạn tính năng, chưa hỗ trợ bindless và push constant
Quá trình học Vulkan
- Ban đầu nó khó đến mức có cảm giác như đang “tự viết driver đồ họa”,
nhưng nhờ sự xuất hiện của dynamic rendering, vk-bootstrap, vkguide mà việc tiếp cận đã dễ hơn
- Các tài liệu học chính:
- vkguide.dev (thực hành từ cơ bản)
- TU Wien Vulkan Lecture Series
- 3D Graphics Rendering Cookbook, Mastering Graphics Programming with Vulkan
- Trong tháng đầu tiên, tác giả đã hoàn thành việc tải glTF, compute skinning, frustum culling và shadow mapping
Cấu trúc engine EDBR và xử lý khung hình
- Mã nguồn engine khoảng 19.000 dòng, game 3D khoảng 4.600 dòng, game platform 2D khoảng 1.200 dòng
- Các bước render chính:
- Compute skinning → Cascaded Shadow Mapping (4096×4096) → geometry shading dựa trên PBR
- Depth Resolve → Post FX (sương mù dựa trên độ sâu) → render UI
- Toàn bộ hệ thống đồ họa được viết lại hoàn toàn dành riêng cho Vulkan, không trộn với mã OpenGL cũ
Mẹo thực chiến khi phát triển với Vulkan
Thư viện khuyến nghị
- vk-bootstrap: đơn giản hóa khởi tạo và cấu hình swapchain
- Vulkan Memory Allocator (VMA): tự động hóa quản lý bộ nhớ
- volk: đơn giản hóa việc nạp các hàm mở rộng
Trừu tượng hóa GfxDevice
- Quản lý
VkDevice, VkQueue, VmaAllocator trong một đối tượng duy nhất
- Phụ trách bắt đầu/kết thúc frame, tạo image·buffer, quản lý bindless descriptor
Quản lý shader
- Sử dụng GLSL, tiền biên dịch sang SPIR-V bằng
glslc tại thời điểm build
- Dùng CMake
DEPFILE để tự động build lại khi shader thay đổi
Mẫu pipeline
- Tách từng giai đoạn render thành pipeline cấp lớp (
init, cleanup, draw)
- Dùng
VK_KHR_dynamic_rendering để bỏ qua render pass·subpass, giữ cấu trúc đơn giản hơn
Programmable Vertex Pulling + Buffer Device Address
- Xử lý mọi mesh bằng một struct Vertex duy nhất
- Truy cập trực tiếp từ shader bằng buffer_reference, không cần VAO
Bindless Descriptor
- Dùng mảng texture toàn cục (
textures[], samplers[]) để sample theo texture ID
- Lưu texture ID trong struct material và truyền qua push constant
Tải dữ liệu động lên GPU
- Truyền dữ liệu bằng cách thay buffer GPU theo từng frame hoặc dùng buffer staging phía CPU
- Quản lý cấu trúc frame-in-flight bằng lớp
NBuffer
Dọn dẹp tài nguyên và đồng bộ hóa
- Dùng hàm cleanup tường minh, quản lý thủ công thay vì trông chờ destructor tự dọn dẹp
- Thực hiện đồng bộ bộ nhớ giữa các pass bằng
vkCmdPipelineBarrier2
- Render Graph dự kiến sẽ được hiện thực trong tương lai
Các ví dụ chi tiết trong hiện thực
Render sprite
- Dùng bindless texture và instancing để render hàng nghìn sprite trong một lần
- Tải struct
SpriteDrawCommand lên buffer GPU rồi gọi vkCmdDraw(6, N)
- Render 10.000 sprite trong 315μs
Compute skinning
- Thực hiện biến đổi vertex dựa trên ma trận xương và trọng số trong compute shader
- Tạo buffer đầu ra riêng cho từng instance, sau đó xử lý như nhau ở bước render
Tách biệt game và renderer
- Logic game dùng entt ECS, còn renderer chỉ xử lý vector DrawCommand
- Sinh lệnh render thông qua các lời gọi
drawMesh, drawSkinnedMesh
Tải scene và prefab
- Dựng level trong Blender rồi xuất sang glTF, tự động spawn prefab theo quy ước tên node
- Prefab được định nghĩa bằng JSON và tham chiếu glTF bên ngoài
MSAA, UI, ImGui
- Áp dụng MSAA x8 dựa trên forward rendering
- Hiện thực hệ thống layout tự động lấy cảm hứng từ Roblox UI API
- Tự viết backend Vulkan riêng để xử lý vấn đề sRGB của Dear ImGui
Các thành phần khác
- Dùng Jolt Physics cho vật lý, entt cho ECS, OpenAL-soft cho âm thanh, Tracy cho profiler
Lợi ích của việc chuyển sang Vulkan
- Loại bỏ trạng thái toàn cục để có cấu trúc mã tường minh và mô-đun hơn
- Validation layer và debug bằng RenderDoc giúp truy vết lỗi dễ dàng hơn
- Tính nhất quán cao hơn giữa GPU·OS, hành vi dễ dự đoán hơn so với OpenGL
- Mở ra khả năng mở rộng như ngôn ngữ shading mới (slang, shady)
- Ít trừu tượng hơn, kiểm soát pipeline rõ ràng hơn nên dễ bảo trì hơn
Kế hoạch sắp tới
- Dự kiến bổ sung font SDF, tải ảnh song song và tạo mipmap, Bloom, volumetric fog, animation blending, render graph, AO
- Học Vulkan tuy khó nhưng mang lại nhiều giá trị trong việc hiểu API đồ họa hiện đại và nâng cao năng lực thiết kế engine
1 bình luận
Ý kiến trên Hacker News
Kể từ bài viết của tôi đăng cách đây 1 năm, suy nghĩ của tôi về Vulkan vẫn không thay đổi nhiều
Nó có thể thú vị với những ai muốn kiểm soát đồ họa ở mức thấp, nhưng với tôi thì đó thực sự là một API rất khổ sở để sử dụng
Tôi vẫn muốn tự làm một game engine, nhưng ngay cả phần thiết lập ban đầu của Vulkan đến giờ vẫn khiến tôi e ngại
Điều tôi muốn là một phiên bản 3D của cách SDL xử lý đồ họa 2D
Nếu muốn làm 3D với SDL thì cuối cùng vẫn phải hạ xuống OpenGL, mà đó không phải mức trừu tượng tôi mong muốn
Có lẽ WebGPU sẽ là một lựa chọn thay thế mà tôi có thể dùng một cách thoải mái hơn
Tôi cũng đã làm một engine với nó, nhưng cuối cùng lại quay về engine dựa trên Vulkan vì muốn có thêm quyền kiểm soát và hiệu năng
Dù vậy, tôi đã học được các mẫu đồng bộ hóa từ mã SDL GPU, và điều đó giúp ích rất nhiều cho engine Vulkan của tôi
wgpucủa Rust là một điểm trung gian cung cấp mức trừu tượng kiểu WebGPUNó mạnh hơn OpenGL, nhưng không cần tự xử lý trực tiếp những chi tiết như resource barrier hay layout transition
Nó đảm nhận một phần bookkeeping ở runtime, và có những giới hạn như chỉ hỗ trợ một hàng đợi duy nhất
Vulkan đúng là khó, nhưng nếu dùng các extension được những nhà cung cấp lớn hỗ trợ thì cũng cải thiện khá nhiều
Tuy nhiên, nó vẫn tồn tại độ phức tạp không cần thiết, như yêu cầu các thiết lập chi tiết mà driver lại bỏ qua
Hiện giờ, API trung gian giữa game engine cấp cao và Vulkan/Metal cấp thấp gần như đã biến mất
Nếu người mới muốn học đồ họa 3D, họ cần một API đơn giản ở mức “vẽ tam giác”, nơi không cần biết đến các khái niệm như shader hay buffer
Sự kiểm soát tỉ mỉ của Vulkan chỉ cần thiết cho số rất ít nhà phát triển engine, còn với đa số thì mức OpenGL là đã đủ
3D có nhiều thành phần có thể kết hợp hơn 2D rất nhiều, nên một API đồ họa đơn giản khó mà gánh nổi
OpenGL ban đầu cũng nhắm tới mục tiêu đó, nhưng cuối cùng vẫn trở nên phức tạp
‘bike shedding’ nghĩa là ám ảnh vào chuyện nhỏ nhặt mà bỏ lỡ phần quan trọng
Điều được mô tả trong bài gốc thực ra gần với feature creep hoặc over-engineering hơn
Nó chỉ việc tập trung vào niềm vui cá nhân đến mức cản trở tiến độ dự án
bike shedding thường được giải thích là “chọn màu nhà để xe đạp trước cả khi xây xong ngôi nhà”
Có người nói “không nên bắt đầu phát triển engine bằng một bản clone multiplayer của Minecraft”,
nhưng thật ra rất nhiều người làm game kiểu Minecraft như dự án engine đầu tiên
Với voxel engine, đó gần như là một kiểu “Hello, world”
Bài đăng vào thời điểm (2024) đã đạt 625 điểm và 260 bình luận — liên kết gốc
Vulkan là công nghệ khó nhất mà tôi từng học
Nó quá thiếu trực quan và có quá nhiều việc lặp đi lặp lại, đến mức làm mất đi niềm vui lập trình
Nếu muốn bắt đầu đơn giản hơn thì nên dùng OpenGL, đặc biệt là các phiên bản trước khi shader được đưa vào
Nhưng ngành này đang dần gạt OpenGL ra ngoài
Tôi chỉ làm theo tutorial và chép mã, chứ không thực sự hiểu khái niệm
Vì thế tôi đã chuyển sang WebGPU (Google Dawn), và nó đơn giản hơn Vulkan rất nhiều
Nhờ các ràng buộc của WebGPU mà sau khi nắm được khái niệm rồi quay lại học Vulkan, mọi thứ trở nên dễ hơn hẳn
WebGPU không có push constant và có vấn đề pipeline explosion, nhưng Vulkan thì khó hơn ở phần đồng bộ hóa và quản lý bộ nhớ
SDL_GPU cũng là API ở mức tương tự nên rất phù hợp để nhập môn
Vulkan là một API bị thiết kế quá đà
Trong CUDA, việc cấp phát bộ nhớ GPU có thể làm chỉ với một dòng, còn trong Vulkan lại cần vô số boilerplate
Vulkan hiện đại đã cải thiện khá nhiều, nhưng vẫn còn một chặng đường dài
Tôi hy vọng SDL3 hay wgpu sẽ trở thành lớp trừu tượng giúp giảm bớt sự phức tạp này
Vì Valve đang hỗ trợ SDL3 nên tôi nghĩ hướng đó có nhiều triển vọng
Trước tiên phải tự hỏi: “Có cần xử lý đồ họa theo kiểu đa luồng không?”
Nếu không thì chẳng có lý do gì để dùng Vulkan/DX12 cả
Trước khi gặp vấn đề hiệu năng, dùng OpenGL, DX11 hoặc một game engine sẽ tốt hơn nhiều
Tôi rất mê lập trình 3D/game và thường xem vài YouTuber làm game
Nhưng so với web app hay DevOps thì đây là một thế giới phức tạp hơn nhiều
Nào là pixel shader, compute shader, hình học, đại số tuyến tính, thậm chí cả PDE
Kênh YouTube TokyoSpliff
Thật tốt khi dạo này việc tự làm game engine như một sở thích được xem là điều ngầu
Tôi cũng đã phát triển engine cá nhân của mình suốt 10 năm, và đó là một trải nghiệm rất đáng giá
Nếu mới bắt đầu với lập trình đồ họa thì nên bắt đầu từ OpenGL
Tôi đã đọc tutorial OpenGL của NeHe từ 23 năm trước, và đến giờ vẫn thấy đó là một trong những tài liệu học được tổ chức tốt nhất
Nhân tiện, tôi không phải tác giả bài gốc, tôi chỉ giữ lại tiêu đề mà thôi