Nâng Cao Hiệu Suất Đóng Gói 2D Sprite Thông Qua Phép Biến Đổi Nghiêng - nói dối e blog

Nâng Cao Hiệu Suất Đóng Gói 2D Sprite Thông Qua Phép Biến Đổi Nghiêng

Trong ngành công nghiệp game 2D hiện đại, tầng đồ họa hầu hết đều được xây dựng dựa trên nền tảng các API 3D. Để tối ưu hiệu suất rendering, các yếu tố đồ họa (sprite) thường được đóng gói vào cùng một texture atlas. Quy trình này có thể thực hiện thủ công ngoại tuyến hoặc tự động hóa theo thời gian chạy.

TexturePacker là một công cụ thương mại xuất sắc trong lĩnh vực này, nhưng theo quan điểm của tôi, việc tích hợp nó vào chuỗi công cụ phát triển vẫn tồn tại một số điểm cần hoàn thiện. Quá trình đóng gói sprite và tổng hợp texture atlas từ kết quả nên được tách biệt thay vì kết hợp. Trong quy trình phát triển lặp, khi chỉ sửa đổi một số lượng nhỏ sprite và kích thước chúng không thay đổi, việc tránh chạy lại thuật toán đóng gói sẽ tiết kiệm tài nguyên đáng kể.

Khi cập nhật sprite, quá trình tổng hợp texture có thể được tối ưu nếu giữ nguyên các sprite chưa thay đổi. Đặc biệt với các định dạng nén như ETC - vốn chia texture thành các khối 4x4 độc lập - ta có thể tiến hành nén từng sprite riêng lẻ trước khi tổng hợp. Điều này cho phép tái sử dụng cache của các sprite chưa sửa đổi mà không cần xử lý lại.

Trong một số trường hợp, thay vì tổng hợp texture, chỉ lưu trữ dữ liệu đóng gói cũng mang lại nhiều lợi thế. Hệ thống runtime có thể tải các sprite rời rạc vào texture dựa trên thông tin atlas. Cách tiếp cận này rất phù hợp với việc cập nhật nội dung game định kỳ.

Về tối ưu không gian, TexturePacker đã áp dụng nhiều giải pháp sáng tạo. Với các sprite có nhiều khoảng trống ở góc, việc sử dụng hộp bao hình chữ nhật truyền thống thường gây lãng phí. Phiên bản mới nhất của công cụ này đã hỗ trợ đóng gói đa giác, cắt bỏ tối đa các vùng trống. Dù giải pháp này làm tăng số lượng đa giác rendering (tuy nhiên không đáng kể với game 2D), nhưng lại tạo thêm không gian cho các sprite nhỏ khác.

Tuy nhiên, tôi tin rằng vẫn còn nhiều phương án khác để cải tiến. Trong quá trình phát triển công cụ đóng gói cho dự án 2D mới của công ty, tôi đã nghiên cứu và phát triển một phương pháp biến đổi hình học độc đáo.

Chúng tôi nhận ra rằng nhiều sprite có các vùng trống đáng kể ở góc, dù không làm tăng kích thước cài đặt sau nén, nhưng lại gây lãng phí diện tích texture (ảnh hưởng đến bộ nhớ runtime). Bằng cách áp dụng phép biến đổi hình học hợp lý, ta có thể thu gọn diện tích bao bọc mà không làm mất thông tin. Chẳng hạn trong dự án trước, một sprite vũ khí được vẽ theo góc phối cảnh nghiêng (hình trái), tạo ra các khoảng trống lớn ở hai góc. Sau khi áp dụng phép biến đổi nghiêng, diện tích sprite giảm từ 358x305 xuống còn 149x284 (giảm 61.2%).

!weapon.png !weapon.trans.png
Do hiện tượng lọc tuyến tính, hình ảnh sau biến đổi có thể bị mờ. Ta có thể áp dụng kỹ thuật làm sắc nét để cải thiện: !weapon.sharpen.png

Ý tưởng này không mới, trước đây team nghệ thuật thường thực hiện thủ công: dùng phần mềm đồ họa biến đổi hình dạng rồi phục hồi trong công cụ chỉnh sửa. Quá trình này hoàn toàn có thể tự động hóa, vấn đề nằm ở việc tìm kiếm thuật toán tối ưu. Trong công cụ đóng gói mới phát triển, tôi sử dụng thuật toán độ phức tạp O(n²) để tìm giải pháp tối ưu. Tuy nhiên tôi tin rằng có thể cải tiến xuống độ phức tạp O(log N). Với số lượng sprite thông thường, thời gian xử lý chấp nhận được, đặc biệt kết quả có thể cache để tránh xử lý lại khi sprite không thay đổi.

Thuật toán thực hiện như sau: Đầu tiên thực hiện phép nghiêng theo phương ngang, dịch chuyển từng dòng quét theo tỷ lệ xác định, biến hình chữ nhật thành hình bình hành. Toàn bộ pixel được giữ nguyên, lượng thông tin không thay đổi. Sau đó tiếp tục thực hiện tương tự theo phương dọc. Bằng cách thử nghiệm các tổ hợp biến đổi, ta xác định tham số tối ưu dựa trên kích thước AABB. Vì đây là phép biến đổi tuyến tính, không cần thử hết toàn bộ tổ hợp.

Công cụ của tôi đã xử lý thành công một sprite khác trong game, kết quả gần như khớp với bản xử lý thủ công của team nghệ thuật, thậm chí còn tốt hơn chút ít:

!box.png !box.trans.png

Tuy nhiên không phải hình ảnh nào cũng phù hợp. Các tile vẽ tay dạng kim cương khi biến đổi sẽ bị mờ nghiêm trọng do lỗi sampling, như trường hợp này:

!tile.png !tile.trans.png

Để giải quyết, ta có thể đánh dấu các sprite không cần xử lý bằng hậu tố đặc biệt trong tên file. Nếu runtime không yêu cầu scale, nên sử dụng phương thức sampling GL_NEAREST thay vì GL_LINEAR.

Hệ thống ejoy2d được thiết kế linh hoạt với định dạng texture cho phép xử lý này. Mỗi sprite trong atlas không chỉ lưu trữ offset mà còn mô tả đầy đủ tọa độ 4 đỉnh trên texture và vị trí tương ứng trên màn hình. Điều này cho phép dễ dàng tính toán ngược để xác định vị trí rendering chính xác.

Code của thuật toán đang được phát triển như một module C cho công cụ đóng gói, có thể tích hợp dễ dàng vào hệ thống build. Phiên bản hiện tại ưu tiên tính rõ ràng thay vì tối ưu tốc độ. (Lưu ý: Công cụ vẫn đang trong giai đoạn phát triển)

Một hướng tối ưu khác là cắt sprite thành các phần nhỏ. Chẳng hạn sprite 100x100 có thể chia làm hai phần 50 pixel, mỗi phần nhỏ hơn nguyên bản. Hệ thống ejoy2d hỗ trợ rendering sprite đa phần tự nhiên. Tuy nhiên cần lưu ý khi chia, mỗi phần cần mở rộng thêm 1 pixel ở cạnh cắt để tránh khe hở. Vị trí cắt tối ưu (không nhất thiết ở chính giữa

0%