Bộ Mã Hóa EAC (Ericsson Texture Compression) Cho Bản Đồ Kết Cấu
Gần đây, tôi đang thực hiện công việc cho module UI (Giao diện người dùng) của một engine mới. Trong quá trình này, tôi nhận ra rằng font chữ tiếng Trung Quốc yêu cầu một bản đồ kết cấu có kích thước lớn, nhưng chỉ cần một kênh thông tin duy nhất. Vì vậy, tôi quyết định áp dụng kỹ thuật nén kết cấu. Trên các thiết bị di động, định dạng GL_COMPRESSED_R11_EAC
là một lựa chọn tối ưu cho trường hợp này.
EAC (Ericsson Texture Compression) là một phương án nén kênh đơn do Ericsson phát triển, hiện đã trở thành tiêu chuẩn chính thức của OpenGL. Trong thực tế, EAC thường được sử dụng kết hợp với ETC2, trong đó ETC2 xử lý kênh RGB và EAC phụ trách kênh Alpha. Tuy nhiên, EAC cũng có thể được áp dụng độc lập. Định dạng này giải mã mỗi điểm ảnh về giá trị nguyên trong khoảng [0,2047], đạt độ chính xác 11 bit, do đó có tên gọi R11_EAC
. Dù vậy, qua nghiên cứu tài liệu kỹ thuật, tôi nhận thấy rằng độ chính xác hiệu dụng thực tế chỉ là 8 bit, và dữ liệu thường được mã hóa từ nguồn 8 bit gốc. Sau khi nén, mỗi khối 4×4 điểm ảnh (tổng cộng 16 điểm) sẽ được mã hóa thành 64 bit, tương ứng tỷ lệ nén 2:1. Điều này giúp tiết kiệm 50% không gian bộ nhớ dành cho kết cấu, đặc biệt hữu ích với các font chữ có độ phân giải cao.
Hiện tại, bgfx chưa hỗ trợ EAC. Tôi đã gửi yêu cầu cải tiến đến tác giả của thư viện này, hy vọng sẽ có phiên bản chính thức tích hợp trong tương lai. Tuy nhiên, việc tùy chỉnh để thêm tính năng này không quá phức tạp. Công cụ mã hóa kết cấu texturec
của bgfx cũng chưa hỗ trợ EAC, dẫn đến việc không thể nén các kết cấu có kênh Alpha. Trên mạng hiện tại, etcpack - công cụ mã nguồn mở của Ericsson, là lựa chọn duy nhất để nén định dạng này. Ban đầu, tôi dự định tận dụng etcpack, nhưng sau khi phân tích mã nguồn, tôi nhận thấy chất lượng mã hóa thấp, dẫn đến quyết định tự xây dựng một codec dựa trên đặc tả định dạng.
Chi tiết kỹ thuật của EAC
Theo tài liệu chính thức của OpenGL, quá trình mã hóa EAC tuân theo các bước sau:
- Chia nhỏ ảnh: Ảnh được chia thành các khối 4×4 điểm ảnh độc lập.
- Mã hóa mỗi khối: 16 điểm ảnh 8 bit (tổng cộng 128 bit) sẽ được nén vào 64 bit (8 byte), trung bình 4 bit/trên mỗi điểm ảnh.
Cách tiếp cận thô sơ nhất là cắt bỏ 4 bit thấp, nhưng điều này gây mất mát thông tin nghiêm trọng. Một giải pháp phổ biến hơn là sử dụng 3 bit để lưu trữ các chỉ số (index), chiếm 48 bit cho 16 điểm ảnh, kết hợp 16 bit còn lại để lưu giá trị cơ sở (base value). Ví dụ, thuật toán S3 (DXT5) trên PC thường lưu hai byte cho giá trị min/max của khối, sau đó chia đều thành 8 mức màu hoặc xử lý đặc biệt cho các giá trị 0 và 255.
Tuy nhiên, phương pháp này hoạt động kém hiệu quả với các khối có độ phân bố xám không đồng đều. EAC cải tiến bằng cách chỉ lưu một byte giá trị cơ sở và một byte xác định bảng tra cứu (index table) được cố định trong GPU. Nhờ đó, 48 bit còn lại có thể biểu diễn 16 chỉ số dựa trên giá trị cơ sở và hệ số tỷ lệ (scale factor). Cụ thể:
- Có sẵn 16 bảng tra cứu khác nhau.
- Hệ số tỷ lệ nằm trong khoảng [1,15], tạo ra tổng cộng 240 tổ hợp (16×15).
Ví dụ về bảng tra cứu:
|
|
Giả sử giá trị cơ sở là 128, hệ số tỷ lệ là 2, và sử dụng Bảng 10. Một điểm ảnh có giá trị 130 sẽ được mã hóa thành chỉ số 4, theo công thức:
Giải mã = 128 + 2 × table[10][4].
Lưu ý rằng kết quả có thể vượt ngoài khoảng [0,255], do đó cần cắt bỏ giá trị ngoài ngưỡng. Điều này đặc biệt hiệu quả với các giá trị Alpha 0 hoặc 255 (thường gặp nhất). Bằng cách chọn hệ số tỷ lệ phù hợp, đảm bảo có thể mã hóa chính xác cả 0 và 255.
Cải tiến trong bộ mã hóa của tôi
Trong quá trình triển khai, tôi đã tối ưu hóa hai trường hợp đặc biệt:
- Khối có một giá trị Alpha duy nhất: Mã hóa không mất dữ liệu bằng cách đặt hệ số tỷ lệ bằng 0.
- Khối có hai giá trị Alpha (một trong hai là 0 hoặc 255): Mã hóa nhanh và chính xác.
Với các trường hợp khác, tôi sử dụng phương pháp vét cạn (brute-force) để tìm bảng tra cứu, giá trị cơ sở và hệ số tỷ lệ tối ưu, đảm bảo sai số giữa dữ liệu nén và gốc là nhỏ nhất. Để tăng tốc độ, tôi áp dụng các cải tiến sau:
- Sắp xếp điểm ảnh: Giảm độ phức tạp từ O(n²) xuống O(n) bằng cách gộp các giá trị trùng nhau.
- Giá trị cơ sở: Tính trung vị của min/max thay vì trung bình, giúp thu hẹp khoảng tìm kiếm.
- Hệ số tỷ lệ: Ước lượng dựa trên khoảng cách xa nhất giữa hai mức màu trong khối.
So với etcpack, mã nguồn của tôi được tối ưu về thuật toán cắt tỉa (pruning)