Kết Cấu: Kẻ Nuốt Chửng Bộ Nhớ VRAM Mà Bạn Chưa Từng Ngờ Tới
Vì sao tệp JPG 1.5MB lại phình thành 87MB trong VRAM? Tìm hiểu vì sao PNG/JPG thất thế trên GPU và cách Basis Universal cùng KTX2 giải quyết triệt để.
Trong bài viết trước, chúng ta đã cắt giảm một nửa số lượng đỉnh lưới và giúp mô hình trở nên thon gọn hơn nhiều, nhưng đó vẫn chưa phải là tất cả. Thành phần cồng kềnh nhất vẫn đang nằm sờ sờ ngay trước mắt: các bản đồ kết cấu. Đối với một mô hình PBR tiêu chuẩn, kết cấu chiếm tới hơn 80% dung lượng tệp tin, và chúng cũng chính là thủ phạm phình to dữ dội nhất một khi được nạp vào bộ nhớ card đồ họa VRAM.
Bài viết này sinh ra để xử lý dứt điểm "quái vật nuốt VRAM" này. Chúng ta sẽ cùng làm rõ ba vấn đề cốt lõi: tại sao PNG và JPG lại có khiếm khuyết trí mạng trong mắt GPU, định dạng kết cấu gốc của GPU hoạt động như thế nào và vì sao không thể áp dụng trực tiếp, cuối cùng là cách sự kết hợp giữa Basis Universal và KTX2 giải quyết triệt để rào cản đó.
Nhắc Lại Nhanh: Vì Sao JPG Lại Khiến VRAM Phát Nổ
Chúng ta đã gặp công thức toán học này trong bài trước:
Chiếm dụng VRAM = Chiều rộng * Chiều cao * 4 byte (RGBA) * 1.333 (kèm mipmaps)
Một bức ảnh kết cấu kích thước 4096×4096, bất kể nó chỉ nặng 1.5MB dạng JPG hay 8MB dạng PNG khi lưu trên ổ cứng, một khi đã nạp lên bộ nhớ đồ họa VRAM sẽ ngốn tới khoảng 87MB. Lý do duy nhất đằng sau điều này: GPU hoàn toàn không hiểu định dạng JPG hay PNG.
Khối lấy mẫu kết cấu (texture sampler) của phần cứng GPU chỉ biết làm đúng một việc: dựa vào tọa độ UV, đọc giá trị màu sắc từ một khối điểm ảnh có kích thước cố định trong bộ nhớ. Nó bắt buộc các bức ảnh phải được lưu trữ trong VRAM dưới dạng "điểm ảnh thô trải phẳng". Do đó, trước khi trình duyệt có thể đẩy một tệp JPG lên GPU, CPU bắt buộc phải giải nén toàn bộ tệp nén đó thành ma trận điểm ảnh RGBA thô, sau đó mới nhồi nhét toàn bộ khối dữ liệu khổng lồ đó vào VRAM.
Quy trình này gây ra ba vấn đề kỹ thuật nghiêm trọng:
- Bùng nổ VRAM: Các điểm ảnh thô sau khi giải nén chiếm một lượng dung lượng khổng lồ. Con số 87MB không hề phóng đại, đó là phép tính toán học tất yếu.
- Nghẽn đường truyền GPU: Việc truyền tải các khối điểm ảnh thô đồ sộ từ RAM máy tính sang VRAM của card đồ họa diễn ra chậm chạp và có thể làm đóng băng khung hình đầu tiên.
- Gánh nặng giải nén của CPU: Quá trình giải nén các bức ảnh lớn bằng CPU rất tốn thời gian, đặc biệt trên các dòng điện thoại di động cấu hình hạn chế.
Ví von như hình ảnh "miếng bọt biển nén": PNG và JPG là những miếng bọt biển được vắt kiệt nước, rất tiện lợi khi vận chuyển trên mạng. Nhưng ngay khoảnh khắc chúng chạm vào GPU, chúng hút no nước và nở bung trở lại kích thước khổng lồ ban đầu. Tốc độ tải tệp qua mạng có thể nhanh hơn, nhưng lượng VRAM tiết kiệm được bằng đúng số không tròn trĩnh.
Định Dạng Kết Cấu Gốc Của GPU: Nén Trực Tiếp Ngay Trong VRAM
Vì GPU không thể đọc trực tiếp các thuật toán nén như PNG, vậy điều gì sẽ xảy ra nếu chúng ta giữ các bức ảnh ở trạng thái nén ngay cả khi đã nằm trong VRAM? Khi đó GPU có thể giải mã trực tiếp từng khối điểm ảnh nhỏ trong tích tắc ngay khi lấy mẫu, với chi phí tài nguyên gần như bằng không.
Đó chính xác là cách thức vận hành của các định dạng kết cấu gốc của GPU. Dưới đây là các họ định dạng phổ biến nhất:
| Họ định dạng | Tên đầy đủ | Nền tảng chính | Đặc điểm kỹ thuật |
|---|---|---|---|
| BC1-7 | Block Compression | Máy tính để bàn (PC, Mac) | Tiêu chuẩn gạo cội, nén theo từng khối 4×4 điểm ảnh |
| ETC1/2 | Ericsson Texture Compression | Di động (Android, iOS cũ) | Tiêu chuẩn di động đời đầu |
| ASTC | Adaptive Scalable Texture Compression | Di động, VR (thiết bị đời mới) | Linh hoạt, chất lượng cao nhất, điều chỉnh kích cỡ khối tùy biến |
| PVRTC | PowerVR | Thiết bị iOS đời cũ | Đang dần bị thay thế hoàn toàn bởi ASTC |
Điểm chung cốt lõi của các định dạng này: kết cấu được nén thành các khối nhỏ 4×4 điểm ảnh, và phần cứng GPU chỉ giải nén đúng khối điểm ảnh đó khi có yêu cầu lấy mẫu. Nó không giải nén từng điểm ảnh đơn lẻ, nó giải mã cả khối phần cứng. Nhờ vậy, dung lượng VRAM chiếm dụng sẽ thu nhỏ lại theo một tỷ lệ cố định, bất kể nội dung bức ảnh phức tạp hay đơn giản.
Hãy cùng đặt lên bàn cân so sánh:
| Tiêu chí | PNG / JPG (Truyền thống) | Định dạng nén gốc của GPU |
|---|---|---|
| Dung lượng lưu ổ đĩa | Rất nhỏ (đặc biệt là JPG) | Trung bình (nén khối, bitrate cố định) |
| Chiếm dụng VRAM | Khổng lồ (điểm ảnh thô sau giải nén) | Siêu nhỏ (duy trì dạng nén trong bộ nhớ) |
| Tốc độ nạp lên GPU | Chậm (giải nén CPU + nạp dữ liệu lớn) | Rất nhanh (truyền thẳng, không cần CPU giải nén) |
| Tốc độ lấy mẫu đồ họa | Nhanh (vốn là điểm ảnh thô) | Rất nhanh (phần cứng GPU giải mã thời gian thực) |
Định dạng gốc GPU nghe như một giải pháp trong mơ. Vậy tại sao chúng ta không thể sử dụng trực tiếp chúng ngay từ đầu?
Bẫy Kỹ Thuật: Sự Phân Mảnh Phần Cứng Giữa Các Thiết Bị
Đây chính là nỗi đau lớn nhất của các định dạng kết cấu GPU: sự phân mảnh trầm trọng.
- Máy tính PC để bàn hỗ trợ họ định dạng BC1-7, không hỗ trợ ASTC
- Điện thoại Android hỗ trợ ETC2/ASTC, hầu hết không hỗ trợ BC
- Thiết bị iOS (từ chip A7 trở lên) hỗ trợ ASTC; các máy đời cũ hỗ trợ PVRTC
- WebGPU và WebGL về bản chất cũng phụ thuộc vào phần cứng bên dưới của thiết bị người dùng
Nếu bạn muốn một bức ảnh "tồn tại ở định dạng GPU gốc trên mọi thiết bị", bạn bắt buộc phải chuẩn bị nhiều phiên bản riêng biệt cho từng nền tảng. Để ứng dụng chạy được trên cả PC + Android + iOS, bạn phải nén 3 bản sao của cùng một bức ảnh: BC + ETC2/ASTC. Dung lượng gói cài đặt tăng gấp ba lần, và khối lượng công việc cũng tăng gấp ba lần.
Tệ hơn nữa trên nền tảng Web, bạn hoàn toàn không thể biết trước người dùng sẽ mở trang web bằng thiết bị nào. Việc tạo sẵn hàng loạt định dạng tệp trên máy chủ vừa cồng kềnh vừa gây lãng phí tài nguyên lớn.
Basis Universal: Mã Hóa Một Lần, Chuyển Mã Mọi Nơi
Basis Universal (thường gọi tắt là Basis) ra đời nhằm giải quyết dứt điểm sự phân mảnh này. Ý tưởng cốt lõi của nó gói gọn trong một câu:
Trước tiên hãy nén ảnh thành một "định dạng trung gian", sau đó tại thời điểm ứng dụng chạy, chuyển mã ảnh đó sang đúng định dạng gốc mà phần cứng GPU của thiết bị người dùng hỗ trợ.
Quy trình chuyển mã được mô tả trực quan như sau:
Ảnh nguồn ban đầu (PNG/JPG)
│ Mã hóa ngoại tuyến một lần duy nhất (quá trình chậm nhưng chỉ làm một lần)
▼
Định dạng trung gian Basis (ETC1S hoặc UASTC)
│ Đóng gói vào vỏ bọc KTX2
▼
Phát hành lên Web ──┬── GPU Máy tính PC ──→ Chuyển mã tại runtime → BC1/3/7
├── Thiết bị Android ──→ Chuyển mã tại runtime → ETC2
└── Thiết bị iOS/VR ───→ Chuyển mã tại runtime → ASTC
Các điểm cốt lõi cần nhớ:
- Quá trình mã hóa ngoại tuyến chỉ thực hiện một lần duy nhất, tạo ra một tệp dữ liệu trung gian nhỏ gọn
- Quá trình chuyển mã tại thời điểm chạy diễn ra cực kỳ nhanh chóng (tính toán toán học thuần túy trong vài mili-giây), và nó chuyển đổi trực tiếp giữa các định dạng khối mà không cần CPU giải nén thành điểm ảnh thô
- Sau khi chuyển mã, dữ liệu được đẩy vào VRAM là định dạng phần cứng gốc đích thực, do đó mức chiếm dụng VRAM hoàn toàn tuân theo công thức nén khối, tương đương với tệp gốc của GPU
Basis cung cấp hai chế độ mã hóa trung gian. Chúng ta sẽ tìm hiểu sâu ở bài viết sau, hiện tại hãy ghi nhớ hai cái tên:
- ETC1S: Tỷ lệ nén cực kỳ cao; rất phù hợp cho bản đồ màu sắc như diffuse và albedo
- UASTC: Chất lượng hiển thị cao hơn; tối ưu cho các bản đồ nhạy cảm về độ chính xác như normal map
KTX2: Vỏ Bọc Chuẩn Mực Cho Kết Cấu GPU
Vẫn còn một bài toán kỹ thuật: dữ liệu mã hóa của Basis sẽ được lưu trữ ở đâu, gắn nhãn ra sao và kết nối vào tệp glTF bằng cách nào? Câu trả lời chính là KTX2.
KTX2 (Khronos Texture 2) không phải là một thuật toán nén ảnh mới, nó là một định dạng vỏ bọc. Tương tự như tệp nén .zip không phân biệt bên trong chứa tài liệu hay ảnh, KTX2 chịu trách nhiệm đóng gói dữ liệu kết cấu GPU (bao gồm cả dữ liệu mã hóa Basis) theo một cấu trúc chuẩn hóa, đi kèm đầy đủ siêu dữ liệu (định dạng, cấp độ mipmap, không gian màu...).
Trong mô hình glTF, KTX2 được tích hợp thông qua phần mở rộng KHR_texture_basisu: bức ảnh trong mô hình không còn là tệp PNG thông thường mà là tệp KTX2 chứa dữ liệu Basis. Khi nạp mô hình, engine đồ họa sẽ tự động phát hiện năng lực của thiết bị và chuyển mã sang chuẩn BC/ETC/ASTC tương ứng.
Hãy phân biệt rõ vai trò của bộ ba này, tránh nhầm lẫn:
| Tên gọi | Vai trò kỹ thuật | Ví von dễ hiểu |
|---|---|---|
| Basis Universal | Thuật toán mã hóa (nén ảnh thành định dạng trung gian) | "Công thức nén" |
| KTX2 | Định dạng vỏ bọc (đóng gói dữ liệu đã nén) | "Chiếc hộp chứa" |
| KHR_texture_basisu | Phần mở rộng của glTF (báo cho engine biết đây là kết cấu Basis) | "Chiếc nhãn dán" |
Một tệp KTX2 có thể chứa dữ liệu mã hóa Basis (hỗ trợ đa nền tảng) hoặc chứa trực tiếp định dạng gốc của một GPU cụ thể (như BC7). Trên nền Web, 99% trường hợp chúng ta đều dùng Basis, vì mục tiêu hướng đến luôn là "mã hóa một lần, chuyển mã ở mọi nơi".
So Sánh Chiếm Dụng VRAM Thực Tế: Thử Nghiệm Với Ảnh 4096
Hãy áp dụng công thức tính toán vào thực tế để thấy rõ mức chiếm dụng bộ nhớ VRAM của một bức ảnh 4096×4096 RGBA giữa các giải pháp khác nhau:
| Phương án | Dung lượng tệp trên ổ đĩa | Chiếm dụng VRAM (kèm mipmap) | Tốc độ nạp GPU | Hỗ trợ đa nền tảng |
|---|---|---|---|---|
| PNG | ~8MB | ~87MB | Chậm (cần giải nén) | Đạt chuẩn |
| JPG | ~1.5MB | ~87MB | Chậm (cần giải nén) | Đạt chuẩn |
| WebP | ~2MB | ~87MB | Chậm (cần giải nén) | Đạt chuẩn |
| KTX2 (ETC1S) | ~2 đến 3MB | ~11 đến 14MB | Rất nhanh | Đạt chuẩn (qua chuyển mã) |
| KTX2 (UASTC) | ~6 đến 8MB | ~22MB | Rất nhanh | Đạt chuẩn (qua chuyển mã) |
Cách tính thông số VRAM: Nén khối GPU thường chạy ở mức 4bpp (bit trên mỗi điểm ảnh) hoặc 8bpp. Một bức ảnh 4096×4096 ở mức 4bpp có dung lượng khoảng 8MB; khi tính thêm mipmap nhân với 1.333 sẽ ra xấp xỉ 11MB. Chuẩn UASTC chủ yếu chuyển mã sang định dạng 8bpp, tương đương khoảng 22MB.
Ý nghĩa cốt lõi không nằm ở con số chính xác của từng dòng, mà nằm ở hai kết luận mang tính bước ngoặt:
- Các định dạng truyền thống (PNG/JPG/WebP) có mức chiếm dụng VRAM giống hệt nhau, chúng đều bị giải nén thành điểm ảnh thô lên tới 87MB. Tệp tải về nhẹ hơn không giúp bạn tiết kiệm được dù chỉ một byte VRAM nào.
- KTX2 cắt giảm dung lượng VRAM xuống chỉ còn 1/4 hoặc thậm chí 1/8, trong khi kích thước tải về qua mạng vẫn cực kỳ nhỏ gọn.
Đây chính là lý do vì sao các dự án Web thực tế ảo VR và Web di động gần như bắt buộc phải dùng KTX2: một chiếc điện thoại với 2GB VRAM có thể gánh được bao nhiêu bức ảnh 87MB trước khi sập ứng dụng? Nhưng nếu là các bức ảnh 11MB, nó có thể gánh mượt mà hàng chục bức ảnh cùng lúc.
Ma Trận Hỗ Trợ Nền Tảng Của Các Dòng GPU
Mặc dù Basis đã che giấu sự phức tạp cho chúng ta, việc hiểu rõ bức tranh phần cứng bên dưới sẽ giúp ích rất nhiều cho quá trình gỡ lỗi:
| Nền tảng / Thiết bị | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| Máy tính để bàn PC (DirectX, Vulkan, WebGPU) | Hỗ trợ | Không | Hỗ trợ một phần (GPU đời mới) | Không |
| macOS (Metal) | Hỗ trợ (máy đời mới) | Không | Hỗ trợ | Không |
| Android (phổ thông) | Không | Hỗ trợ | Hỗ trợ | Không |
| iOS (từ chip A8 trở lên) | Không | Hỗ trợ | Hỗ trợ | Hỗ trợ (thiết bị cũ) |
| WebGL 2 | Tùy tiện ích mở rộng | Hỗ trợ | Hỗ trợ một phần | Không |
| WebGPU | Hỗ trợ (máy tính) | Hỗ trợ | Tùy thiết bị | Không |
Thư viện Basis sẽ tự động phát hiện các năng lực phần cứng này tại thời điểm chạy và chuyển đổi cùng một tệp dữ liệu trung gian sang định dạng tương thích tối ưu nhất. Đó là lý do tại sao Basis là giải pháp không thể thay thế trên Web: bạn không thể nào biết trước thiết bị của người dùng trước khi xuất bản trang.
So Sánh Luồng Nạp Dữ Liệu Lên GPU
Hãy cùng đúc kết sự khác biệt qua sơ đồ đường đi của dữ liệu.
Định dạng PNG/JPG truyền thống:
Tệp PNG ──Tải về──> RAM máy tính ──CPU giải nén (chậm)──> Mảng điểm ảnh RGBA thô ──Nạp lên GPU (rất nặng, chậm)──> VRAM (87MB)
Định dạng KTX2 + Basis:
Tệp KTX2 ──Tải về──> RAM máy tính ──Chuyển mã tại runtime (cực nhanh)──> Định dạng khối GPU ──Nạp lên GPU (nhẹ, nhanh)──> VRAM (11MB)
Quy trình KTX2 loại bỏ hoàn toàn bước "CPU giải nén từng điểm ảnh thô", và khối lượng dữ liệu đẩy lên card đồ họa giảm đi gần một bậc độ lớn. Khung hình đầu tiên hiển thị nhanh hơn và bộ nhớ VRAM được giải phóng tối đa, đó chính là giá trị không thể chối từ của công nghệ này.
Nội Dung Tiếp Theo
Lý thuyết đã hoàn chỉnh, trong bài viết tiếp theo chúng ta sẽ bắt tay vào thực hành. Chúng ta sẽ sử dụng công cụ toktx và gltf-transform để nén các bức ảnh thực tế sang KTX2, nạp chúng vào Three.js và Babylon.js, đồng thời tìm hiểu sâu cách chọn giữa ETC1S và UASTC cũng như phương pháp tinh chỉnh các thông số nén tối ưu nhất.