Quản Lý Tài Nguyên Kết Cấu Trong Động Cơ 3D
Hôm nay, một đồng nghiệp đã đặt câu hỏi với tôi về vấn đề quản lý kết cấu trong dự án họ đang phát triển - một động cơ 3D do Ark Studio (thuộc NetEase) phát triển từ vài năm trước. Sau buổi trao đổi, họ đã quyết định hủy bỏ yêu cầu ban đầu. Điều này khiến tôi nhận ra tầm quan trọng của việc xác định rõ mục tiêu cốt lõi, tránh việc biến phương pháp thành mục đích cuối cùng.
Bài viết dưới đây không liên quan trực tiếp đến cuộc thảo luận hôm nay, mà là ghi chú cá nhân về cơ chế quản lý kết cấu trong hệ thống động cơ 3D của chúng tôi. Mặc dù quản lý tài nguyên là một mô-đun phức tạp, chúng tôi luôn cố gắng tối giản hóa thiết kế. Trước đây tôi đã từng ghi lại quá trình tiến hóa của thiết kế mô-đun tài nguyên, và dù vài tháng trôi qua với một số cải tiến nhỏ, bản chất vấn không thay đổi nhiều.
Tuy nhiên, kết cấu lại là trường hợp đặc biệt. Sau khi cân nhắc kỹ lưỡng, chúng tôi quyết định xử lý nó ở tầng cao hơn với cơ chế riêng biệt. Như các bậc tiền bối từng nhấn mạnh trong “The Elements of Programming Style” và “The Art of Unix Programming”: “Hãy đảm bảo các trường hợp ngoại lệ thực sự là ngoại lệ”.
Tại sao kết cấu lại đặc biệt?
Trong động cơ 3D, kết cấu thường chiếm dụng bộ nhớ đồ họa (VRAM) thay vì RAM thông thường. Giới hạn VRAM độc lập hoàn toàn với RAM, đồng thời chiến lược sử dụng và các ràng buộc API (như xử lý đa luồng) cũng phức tạp hơn nhiều.
Chưa kể, không phải lúc nào kết cấu cũng có thể tải trực tiếp từ ổ đĩa. Trong các hệ thống hỗ trợ thay đổi trang phục, nhiều kết cấu thực chất là sự kết hợp từ nhiều mảnh nhỏ. Ví dụ: áo khoác, thắt lưng, quần của nhân vật được lưu ở các kết cấu riêng, sau đó ghép thành một toàn cảnh khi render. Điều này xuất phát từ thực tế: các kết cấu rời rạc làm giảm hiệu năng nghiêm trọng trên GPU.
Việc quản lý kết cấu ghép hình tưởng chừng đơn giản, nhưng để thực hiện một cách tinh tế mà vẫn tối ưu hiệu năng lại không dễ dàng. Qua quan sát, các động cơ game 3D đang vận hành tại NetEase (như BigWorld dùng trong Thiên Hạ hay Đại Đường) vẫn còn hạn chế trong cơ chế này, dẫn đến hệ thống thay trang phục thiếu tinh tế. Trong khi đó, World of Warcraft cho phép thay đổi chi tiết nhỏ nhất trên ngoại hình nhân vật (dù chỉ là chi tiết nhỏ như khóa áo).
Giải pháp của chúng tôi: Nhóm kết cấu (Texture Bundle)
Chúng tôi thiết kế định dạng tệp dữ liệu “nhóm kết cấu” chứa nhiều kết cấu liên quan. Động cơ mặc định sử dụng kết cấu chính trong nhóm, nhưng có thể thay đổi trạng thái nhóm để chuyển đổi giữa các kết cấu cụ thể.
Lý do của thiết kế này xuất phát từ thực tế: Mô hình 3D do nghệ sĩ thiết kế thường chỉ tương thích với một số kết cấu đặc biệt. Cho phép người dùng tùy ý gán kết cấu bất kỳ sẽ dẫn đến lỗi hiển thị. Việc chuyển đổi trạng thái nhóm mang lại trải nghiệm sử dụng ổn định và an toàn hơn.
Trong trường hợp cần ghép kết cấu, nhóm kết cấu sẽ mô tả các vùng hình chữ nhật được chia theo quy chuẩn thống nhất. Mỗi kết cấu thành phần được thiết kế chỉ chứa phần hình ảnh tương ứng với một vùng cụ thể. Khi sử dụng, người dùng cung cấp một “mẫu” (pattern) dưới dạng chuỗi ký tự đơn để chỉ định cách kết hợp các vùng từ các kết cấu khác nhau.
Cơ chế quản lý kết cấu
Chúng tôi áp dụng cấu trúc dữ liệu tổ hợp gồm:
- Hai tầng bản đồ băm (hash map):
- Tầng 1: Tìm nạp khối dữ liệu nhóm kết cấu từ đối tượng tài nguyên
- Tầng 2: Cache các kết cấu được tạo ra từ các “mẫu” cụ thể
- Danh sách liên kết (linked list): Sắp xếp theo tần suất sử dụng theo nguyên tắc FIFO
Mỗi nhóm kết cấu được tham chiếu qua handle vĩnh viễn (vì tương ứng duy nhất với tệp dữ liệu cụ thể). Khi động cơ cần render, nó sẽ chọn pattern phù hợp từ handle. Cần lưu ý: Kết cấu được tạo ra chỉ tồn tại đến hết quá trình render khung hình hiện tại.
Thiết kế này mô phỏng cơ chế quản lý bộ nhớ vật lý trong hệ điều hành, cho phép giải phóng VRAM khi không sử dụng đồng thời lưu trữ đệm các pattern thường dùng. Để tối ưu hóa, chúng tôi tham khảo cơ chế quản lý glyph trong FreeType 2, sử dụng khái niệm “khe cắm” (slot) giúp người dùng truy cập dữ liệu dễ dàng mà không cần quan tâm vòng đời phức tạp.
Lưu ý đặc biệt với kết cấu nén DXT
Hiểu rõ nguyên lý nén DXT cho phép chúng ta chia tách và kết hợp các khối 4x4 pixel một cách trực tiếp. Tuy nhiên, mọi thao tác phải tuân thủ đơn vị nhỏ nhất là khối 4x4 pixel để đảm bảo tính toàn vẹn dữ liệu nén.