Bài Học Đầu Tiên Về Giảm Cân Mô Hình 3D: Ba Tuyệt Kỹ Nén Đỉnh Lưới
Dữ liệu đỉnh thực sự chiếm bao nhiêu byte? Lượng tử hóa, MeshOpt và Draco, công cụ nào phù hợp với từng kịch bản? Câu trả lời không phải là 'Draco là số một', mà là 'đúng công cụ cho đúng việc'.
Trong bài viết trước, chúng ta đã mở tệp GLB và nhận thấy các bản đồ kết cấu ngốn tới 80% dung lượng, trong khi các đỉnh chỉ chiếm từ 10-20%. Vậy phải chăng nén đỉnh là việc không cần thiết?
Hoàn toàn không phải vậy. Khi các bức ảnh của mô hình đã được nén tối ưu sang KTX2 và mật độ đỉnh của mô hình rất dày đặc, thì 20% còn lại chính là dữ liệu đỉnh, và 20% này hoàn toàn có thể cắt giảm một nửa hoặc thậm chí giảm tới 90%. Quan trọng hơn, nén đỉnh là một trong số ít những kỹ thuật tối ưu hóa gần như không tốn chi phí và mang lại hiệu quả ngay tức thì: chỉ cần thêm vài dòng lệnh, nạp một bộ giải mã nhỏ là tệp mô hình đã giảm cân rõ rệt.
Bài viết này làm rõ ba nội dung chính: cấu trúc dữ liệu đỉnh thực tế trông như thế nào, đặc tính kỹ thuật của Lượng tử hóa, MeshOpt và Draco, cuối cùng là một kết luận giúp bạn tránh khỏi những sai lầm phổ biến: Không có giải pháp tốt nhất, chỉ có giải pháp phù hợp nhất.
Một Đỉnh Mô Hình Chiếm Bao Nhiêu Bộ Nhớ?
Trước tiên hãy cùng mổ xẻ bên trong một đỉnh. Trong định dạng glTF, mỗi đỉnh được cấu thành từ nhiều thuộc tính hình học:
| Thuộc tính | Mục đích sử dụng | Độ chính xác mặc định | Số byte trên mỗi đỉnh |
|---|---|---|---|
| position | Tọa độ vị trí trong không gian | 3 × float32 | 12 byte |
| normal | Xác định hướng chiếu sáng (vector pháp tuyến) | 3 × float32 | 12 byte |
| tangent | Dùng cho tính toán bản đồ pháp tuyến (tiếp tuyến) | 4 × float32 | 16 byte |
| texcoord_0 (UV) | Tọa độ lấy mẫu kết cấu | 2 × float32 | 8 byte |
| color (đỉnh) | Đổ bóng màu sắc cấp độ đỉnh | 4 × float32 | 16 byte |
Một đỉnh mang đầy đủ các thuộc tính PBR có thể chiếm từ 48 đến 64 byte dữ liệu hình học. Một mô hình với 100.000 đỉnh đồng nghĩa với việc bạn mất từ 5 đến 6MB chỉ riêng cho phần đỉnh.
Hãy để ý rằng hầu như mọi thuộc tính đều mặc định dùng kiểu dữ liệu float32 (số thực dấu phẩy động 32-bit). Đây chính là cơ hội vàng để nén đỉnh, bởi vì đa số các thuộc tính không hề đòi hỏi mức độ chính xác toán học cao đến như vậy.
Tuyệt Kỹ 1: Lượng Tử Hóa
Lượng tử hóa là nguyên lý nền tảng của mọi thuật toán nén đỉnh; cả Draco lẫn MeshOpt đều áp dụng kỹ thuật này bên trong lõi xử lý.
Lượng tử hóa (ánh xạ các số thực dấu phẩy động độ chính xác cao thành các số nguyên độ chính xác thấp hơn) về bản chất là: bạn không cần ghi nhớ số 3.14159265, chỉ cần nhớ 3.14 là đã đủ dùng. Đối với một tập hợp tọa độ trong một phạm vi không gian nhất định, thay vì ghi lại từng chữ số thập phân bằng 32-bit, chúng ta dùng một số nguyên có dải giá trị nhỏ hơn.
Dữ liệu gốc: position.x = 1.234567 (float32, chiếm 4 byte)
Sau lượng tử: position.x = 1234 (int16, chiếm 2 byte) + kèm tỷ lệ scale/offset để tái tạo lại
So sánh trước và sau khi lượng tử hóa:
| Thuộc tính | Số byte float32 | Sau lượng tử hóa (16-bit) | Tỷ lệ tiết kiệm |
|---|---|---|---|
| position | 12 | 6 | Giảm 50% |
| normal | 12 | 6 (hoặc 4, nếu dùng int8 + octahedral) | Giảm 50-67% |
| tangent | 16 | 4-8 | Giảm 50-75% |
| texcoord | 8 | 4 | Giảm 50% |
Đối với một đỉnh ban đầu chiếm từ 48-64 byte, lượng tử hóa thường có thể rút gọn xuống chỉ còn 16-24 byte, cắt giảm một nửa hoặc hơn tổng dung lượng dữ liệu hình học.
Khi Nào Nên Dùng Lượng Tử Hóa
- Bạn chỉ muốn giảm bớt dung lượng tệp mà không đòi hỏi tỷ lệ nén quá cực đoan.
- Bạn muốn hoàn toàn không phụ thuộc vào bộ giải mã ngoài, tệp glTF sau lượng tử hóa sử dụng phần mở rộng chuẩn
KHR_mesh_quantization, và các engine đồ họa lớn đều hỗ trợ gốc mà không cần nạp thêm bất kỳ thư viện JS/WASM nào. - Nền tảng mục tiêu rất khắt khe về kích thước gói tải (như các ứng dụng mini app, nơi việc thêm bộ giải mã Draco ngốn hàng chục KB quý giá).
Khi Nào Không Nên Dùng
- Mô hình có kích thước vật lý rất nhỏ và chi tiết siêu mịn là điểm nhấn chính (như linh kiện cơ khí chuẩn milimét, trang sức đá quý). Sai số lượng tử hóa có thể lộ rõ trên các mô hình siêu nhỏ, tọa độ đỉnh lệch 0.1mm có thể nhìn thấy bằng mắt thường khi soi cận cảnh.
Bài học thực tế về mất mát độ chính xác: Trong một dự án trưng bày trang sức, mô hình nhẫn sau khi lượng tử hóa 16-bit xuất hiện các vết răng cưa gãy khúc trên bề mặt kim loại khi soi phóng to. Nguyên nhân không phải do thiếu đỉnh, mà do không gian tọa độ thế giới quá nhỏ khiến bước nhảy 16-bit không đủ độ mịn. Cách khắc phục: thu nhỏ hộp bao tọa độ hoặc dùng độ sâu bit cao hơn cho các vật thể nhỏ.
Tuyệt Kỹ 2: MeshOpt
MeshOpt tương ứng với phần mở rộng chính thức của glTF: EXT_meshopt_compression. Định vị của nó là "tỷ lệ nén rất tốt, tốc độ giải mã nhanh như chớp".
Cơ chế hoạt động: trước tiên lượng tử hóa các thuộc tính (tương tự như trên), sau đó áp dụng thuật toán mã hóa entropy không mất dữ liệu để nén tiếp các số nguyên đã lượng tử hóa. Nói cách khác: Lượng tử hóa có mất mát + Mã hóa entropy không mất mát = Dung lượng tệp nhỏ hơn đáng kể nhưng chất lượng hình ảnh giữ nguyên tương đương lượng tử hóa.
- Tỷ lệ nén: Nhỏ hơn từ 30-50% so với lượng tử hóa thuần
- Tốc độ giải mã: Cực kỳ nhanh, mã nguồn C/JS tối ưu cao, giải mã hàng chục triệu đỉnh mỗi giây chỉ trên một luồng đơn
- Dung lượng bộ giải mã: Siêu nhỏ gọn (chỉ khoảng 20-30KB sau khi nén gzip)
- Độ tương thích: Được hỗ trợ gốc hoàn hảo trong Three.js, Babylon.js, trở thành tiêu chuẩn trên nền Web
Khi Nào Nên Dùng MeshOpt
- Bạn cần tỷ lệ nén cao hơn lượng tử hóa nhưng không thể chấp nhận tốc độ giải mã chậm chạp của Draco.
- Các dự án chạy trên Web, thiết bị di động hoặc WebXR, nơi tốc độ giải mã ảnh hưởng trực tiếp đến trải nghiệm mở màn hình đầu tiên.
- Các mô hình cần được nạp và giải nén liên tục trong quá trình chạy (như màn chơi được tải động).
Khi Nào Không Nên Dùng
- Nền tảng mục tiêu của bạn không hỗ trợ phần mở rộng
EXT_meshopt_compression(rất hiếm, chỉ xảy ra trên các engine nội bộ quá cũ). - Bạn chỉ cần giải pháp đơn giản nhất có thể và không bận tâm đến mức chênh lệch 30% dung lượng, khi đó lượng tử hóa thuần sẽ bớt đi một thư viện phụ thuộc.
Tuyệt Kỹ 3: Draco
Draco là giải pháp nén do Google phát triển, được định vị ở phân khúc "tỷ lệ nén cực hạn".
Sự khác biệt cốt lõi nhất của Draco so với hai phương pháp trên: Draco can thiệp làm thay đổi cấu trúc liên kết đỉnh. Lượng tử hóa chỉ thay đổi giá trị số học của đỉnh, MeshOpt nén dữ liệu số nguyên bên trên, còn Draco tái tổ chức lại toàn bộ lưới tam giác để diễn đạt thông tin "những đỉnh nào ghép lại thành mặt tam giác" theo cách cô đọng nhất.
- Tỷ lệ nén: Cao nhất trong cả ba phương pháp; các mô hình có mật độ đỉnh dày đặc thường đạt mức giảm trên 90%
- Tốc độ giải mã: Chậm nhất trong ba phương pháp, tuy vẫn nhanh về mặt số học nhưng chậm hơn rõ rệt khi so với MeshOpt
- Dung lượng bộ giải mã: Khá nặng (khoảng 100-200KB, thường phải tải tệp WASM riêng biệt)
- Chất lượng thị giác: Có thể tùy biến mức nén, tuy nhiên tỷ lệ nén quá cao có thể gây méo mó hình học
Khi Nào Nên Dùng Draco
- Các mô hình có dung lượng khổng lồ với số lượng đỉnh siêu dày đặc (mô hình quét 3D hàng triệu đa giác, sa bàn địa hình).
- Nạp một lần duy nhất và người dùng lưu lại trải nghiệm lâu (chấp nhận thời gian chờ giải mã ban đầu).
- Dung lượng bộ giải mã không phải là rào cản, nhưng băng thông tải mạng là nút thắt chính.
Khi Nào Không Nên Dùng
- Thiết bị di động yêu cầu mở màn hình đầu tiên tức thì, việc tải đồng thời cả bộ giải mã WASM lẫn tệp mô hình thực chất có thể làm chậm thời gian hiển thị.
- Môi trường hạn chế nghiêm ngặt về kích thước gói tải (như mini app).
- Mô hình có hoạt ảnh gắn xương hoặc hình dạng biến dạng, Draco hỗ trợ các tính năng này hạn chế hơn và dễ phát sinh lỗi nếu cấu hình sai.
Đặt Tất Cả Lên Bàn Cân: Bảng So Sánh Toàn Diện
Các tỷ lệ nén dưới đây dựa trên số liệu đo lường thực tế từ cộng đồng công nghệ:
| Phương án | Tỷ lệ nén (so với float32) | Tốc độ giải mã | Kích thước bộ giải mã | Nén có mất mát? | Phần mở rộng glTF |
|---|---|---|---|---|---|
| Lượng tử hóa thuần | ~50% | Tức thì, không cần bộ giải mã | 0 KB | Có (về độ chính xác) | KHR_mesh_quantization |
| MeshOpt | ~25-35% | Cực kỳ nhanh | ~25 KB | Có (về độ chính xác) | EXT_meshopt_compression |
| Draco | ~10-20% | Nhanh (chậm nhất trong ba) | ~100-200 KB | Có (chính xác + topology) | KHR_draco_mesh_compression |
Khả năng tương thích nền tảng và bộ giải mã:
| Nền tảng | Lượng tử hóa thuần | MeshOpt | Draco |
|---|---|---|---|
| Web Máy tính để bàn | Hỗ trợ gốc | Hỗ trợ gốc | Cần bộ giải mã ngoài |
| Web Di động | Hỗ trợ gốc | Hỗ trợ gốc | Bộ giải mã khá nặng |
| WebXR / Kính VR | Hỗ trợ gốc | Khuyên dùng | Cần cân nhắc kỹ |
| Ứng dụng Mini App | Rất khuyên dùng | Rất khuyên dùng | Nên tránh sử dụng |
Đúc kết trong một câu: Cần sự tiện lợi tuyệt đối không phụ thuộc thư viện → Chọn Lượng tử hóa thuần; cần sự cân bằng hoàn hảo trên Web → Chọn MeshOpt; cần tỷ lệ nén nhỏ nhất bằng mọi giá và chấp nhận chờ giải mã → Chọn Draco.
Thực Hành: Lượng Tử Hóa Và MeshOpt Với gltfpack
gltfpack là công cụ chính thức xuất sắc cho phép bạn thực hiện cả lượng tử hóa lẫn nén MeshOpt chỉ trong một dòng lệnh duy nhất.
Cài đặt trước (bản nhị phân có tại trang phát hành gltfpack):
# Lượng tử hóa mô hình model.glb sang 16-bit và áp dụng nén MeshOpt
gltfpack -i model.glb -o model-packed.glb -cc
# Cờ -cc kích hoạt chế độ nén (bổ sung EXT_meshopt_compression bên trên lượng tử hóa mặc định)
Các tham số phổ biến trong thực tế:
# Chỉ lượng tử hóa thuần, không dùng MeshOpt (nhẹ nhất, không phụ thuộc bộ giải mã)
# gltfpack mặc định lượng tử hóa đỉnh về 16-bit (KHR_mesh_quantization), không cần thêm cờ
gltfpack -i model.glb -o model-quant.glb
# Lượng tử hóa kèm bật nén MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc
# Khi số lượng đỉnh quá lớn, đồng thời đơn giản hóa hình học (giảm bớt đỉnh đa giác)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 tương đương với việc giữ lại khoảng 50% số đỉnh
Lưu ý về cờ
-cc: đây là cờ kích hoạt nénEXT_meshopt_compression. Nếu không truyền-cc, gltfpack mặc định sẽ thực hiện lượng tử hóa thuần, điều đó có nghĩa là lệnhgltfpack -i in.glb -o out.glbđã là phiên bản lượng tử hóa thuần không cần bộ giải mã ngoài.
Kết quả đo đạc thực tế (trên một mô hình PBR 120.000 đỉnh dung lượng 5MB):
| Thao tác xử lý | Dung lượng tệp | Đánh giá thực tế |
|---|---|---|
| Bản gốc (float32) | 5.0MB | Mốc so sánh ban đầu |
| Chỉ lượng tử hóa (mặc định) | 2.6MB | Giảm một nửa, hầu như không khác biệt bằng mắt |
Kèm MeshOpt (-cc) | 1.7MB | Tiết kiệm thêm 35%, nạp mượt mà hơn |
Cảnh báo: Tham số
-siđơn giản hóa là thao tác làm thay đổi hình học thực tế của mô hình, khác hoàn toàn với nén dữ liệu. Nén dữ liệu bảo toàn độ trung thực thị giác tối đa, còn đơn giản hóa chủ động gọt bớt các chi tiết đa giác. Bạn có thể kết hợp cả hai, nhưng hãy đảm bảo yêu cầu của dự án cho phép điều đó.
Các Bẫy Kỹ Thuật Thường Gặp
- Vector pháp tuyến bị đổi hướng sau khi lượng tử hóa: Thường do đặt độ chính xác quá thấp. Hãy dùng ít nhất 16-bit cho vector pháp tuyến, hoặc mã hóa 8-bit dạng octahedral.
- Mất vật liệu sau khi giải nén Draco: Draco chỉ nén phần hình học lưới; vật liệu và kết cấu phải được xử lý độc lập. Hãy đảm bảo cả bộ giải mã Draco lẫn các phần mở rộng KHR đều được cấu hình chuẩn khi nạp mô hình.
- Draco không chạy được trong mini app: Bộ giải mã WASM có thể bị hạn chế quyền thực thi trong môi trường sandbox của một số ứng dụng. Chuyển sang MeshOpt thường giải quyết dứt điểm vấn đề này.
- Mô hình bị lệch tọa độ hoặc méo mó: Khi mô hình nằm quá xa gốc tọa độ (0, 0, 0), dải giá trị 16-bit không thể bao quát được tọa độ lớn trong khi vẫn duy trì chi tiết vi mô. Giải pháp: đưa tâm mô hình về gốc tọa độ trước khi nén, hoặc tăng độ sâu bit.
Bước Tiếp Theo
Đỉnh lưới đã được tối ưu thon gọn, nhưng chớ vội mừng vì như đã nói ở phần đầu, các bức ảnh kết cấu mới là thành phần ngốn tới 80% dung lượng. Trong bài viết tiếp theo, chúng ta sẽ chuyển hướng sang chiến trường mới: khám phá lý do tại sao ảnh PNG/JPG truyền thống lại là những kẻ ngốn bộ nhớ VRAM khủng khiếp trên GPU, và cách các định dạng kết cấu gốc GPU giải quyết triệt để bài toán này.