Đóng gói sprite atlas: đệm, lũy thừa hai và lem màu

Atlas sprite có vẻ là ý tưởng đơn giản: đặt mọi sprite trên một texture lớn thay vì nhiều tệp nhỏ. Nhưng chi tiết đóng gói quyết định trò chơi hiển thị sạch hay xuất hiện viền màu mờ ở mọi cạnh sprite. Sau đây là việc atlas thực sự làm, lý do màu tràn qua đường nối và cài đặt cụ thể để ngăn nó.

Vì sao atlas tồn tại: lệnh vẽ

Mỗi lần GPU đổi từ texture này sang texture khác có chi phí, khái niệm là một “lệnh vẽ” mới. Cảnh có năm mươi sprite texture riêng, vẽ đơn giản có thể cần năm mươi lệnh dù mỗi sprite nhỏ. Đặc biệt trên di động, chi phí lệnh cộng nhanh đến mức ảnh hưởng fps trước khi GPU thực sự xử lý nhiều pixel.

Atlas sprite tránh vấn đề này bằng cách đặt nhiều sprite trên một texture chung. Miễn các hình trong một lượt đều lấy mẫu từ cùng texture, trình kết xuất có thể gộp thành ít lệnh hơn nhiều: trường hợp tốt nhất là một lệnh vẽ cho mọi sprite dùng cùng atlas và vật liệu. Vì vậy atlas quan trọng hơn với trò chơi có nhiều sprite nhỏ được vẽ lại thường xuyên, như hiệu ứng hạt, biểu tượng giao diện và tileset, so với vài ảnh nền lớn, nơi số lệnh vẽ chưa từng là nút thắt.

Bin packing: xếp sprite vào texture

Với bộ sprite khác kích thước, xếp vào một texture để ít phí nhất là biến thể của bài toán cổ điển bài toán đóng gói. Hầu hết công cụ tạo atlas dùng một biến thể của một trong hai cách:

  • Đóng gói kiểu kệ: sprite được đặt từ trái sang phải dọc một hàng (“kệ”) cho đến khi có sprite không vừa, rồi bắt đầu kệ mới bên dưới. Đơn giản và nhanh, nhưng lãng phí không gian khi chiều cao sprite khác nhau nhiều trong một kệ.
  • Hình chữ nhật cực đại / đóng gói guillotine: công cụ đóng gói theo dõi các vùng chữ nhật trống sau mỗi lần đặt và xếp sprite mới vào không gian còn lại tốt nhất, chia các hình chữ nhật trong quá trình đó. Đóng gói dày hơn, tính toán nhiều hơn cho mỗi sprite, và là cách tiêu chuẩn trong phần lớn công cụ atlas hiện đại (TexturePacker, Sprite Atlas của Unity, trình xuất Godot) khi số sprite không còn rất ít.

Xoay là tối ưu thêm ở một số bộ đóng gói: xoay sprite 90 độ có thể vừa vào dải trống mà khi không xoay không vừa. Đóng gói chặt hơn nhưng tăng phức tạp khi vẽ vì UV phải tính xoay, nên thường là tùy chọn bật thêm, không mặc định.

Bạn không cần tự triển khai những việc này — công cụ của engine và các trình đóng gói chuyên dụng sẽ xử lý. Điều đáng hiểu là mật độ đóng gói và phần đệm được nói đến tiếp theo có sự đánh đổi: đóng gói chặt hơn giảm diện tích kết cấu bị lãng phí, nhưng đệm giữa các sprite mới ngăn hiện tượng lem màu mô tả bên dưới. Vì thế, trình đóng gói được đặt quá chặt có thể khiến lỗi hình ảnh xuất hiện trở lại chỉ để tiết kiệm vài phần trăm diện tích kết cấu.

Lem texture: là gì, tại sao xảy ra

Lem texture là khi mép sprite được kết xuất hiện một vệt màu từ sprite đóng gói bên cạnh trong atlas — đường màu mảnh dọc một hoặc nhiều cạnh không liên quan đến hình vẽ của chính sprite. Đây là một trong những lỗi riêng của atlas phổ biến nhất, với hai nguyên nhân khác nhau thường bị nhầm lẫn.

Tràn màu do lọc. Khi sprite được vẽ ở tỷ lệ khác 1:1, hoặc camera đưa nó đến vị trí không thẳng hàng với pixel, bộ lọc texture của GPU lấy mẫu một vùng texel nhỏ quanh từng điểm lấy mẫu, chứ không chỉ texel gần nhất — ngay cả khi đặt bộ lọc point/nearest ở cấp texture, lọc và mipmapping ở giai đoạn lấy mẫu vẫn có thể vươn hơi ra ngoài hình chữ nhật UV của sprite. Nếu vùng atlas liền kề là sprite khác, màu bên cạnh đó sẽ tràn vào.

Tràn màu mipmap. Mipmap là bản giảm trước của texture dùng khi sprite nhỏ trên màn. Tạo mipmap pha các khối đầy đủ với nhau, nên màu sprite này vào mức mip của sprite khác trước khi vẽ riêng. Vì vậy đôi khi chỉ tràn ở xa hoặc nhỏ, còn zoom thì tốt.

Cả hai nguyên nhân cùng gốc: hai sprite không liên quan nằm sát nhau trong atlas, không có gì ở giữa hấp thụ lỗi lấy mẫu.

Trái: sprite sát cạnh trong atlas, ô phóng cho đường nối sai màu. Phải: cùng sprite tách bằng đệm trong suốt, cạnh sạch trong ô phóng
Chỗ phóng to thể hiện toàn vấn đề. Sát mép, bộ mẫu vượt nửa texel sẽ vào sprite bên và kéo màu qua đường nối.

Sửa tràn màu: đệm và mở rộng cạnh

Cách sửa chuẩn là đệm — để khoảng cách vài pixel trong suốt (hoặc lặp pixel mép) giữa mọi sprite trong atlas, để mẫu lệch rơi vào khoảng cách thay vì nội dung thật của sprite bên cạnh. Một đến hai pixel đệm đủ cho phần lớn trường hợp ở độ phân giải gốc; nếu sprite được phóng đáng kể khi kết xuất hoặc bật mipmap, đệm nhiều hơn cho biên độ sai số lớn hơn.

Đệm trong suốt đơn giản giải quyết việc rò màu sprite bên cạnh nhưng tạo vấn đề nhỏ hơn: ngay mép sprite, lọc có thể trộn màu mép với đệm trong suốt, gây tối nhẹ hoặc viền ngay ranh giới thay vì mép hoàn toàn sạch. Mở rộng cạnh (đôi khi gọi là mở rộng cạnh) khắc phục bằng cách lấp vùng đệm bằng bản sao các pixel cạnh của chính sprite, kéo dài ra ngoài, thay vì để trong suốt. Vấn đề với sprite lân cận vẫn được giải quyết bằng cách tương tự: vẫn có khoảng cách giữa các sprite khác nhau, nhưng ranh giới của sprite hòa với chính nó thay vì với vùng trong suốt trống.

Phần lớn công cụ atlas (TexturePacker, Sprite Atlas Unity, xuất atlas Godot) có kích thước đệm và bật mở rộng cạnh trực tiếp. Hiếm nên để đệm 0; một hai pixel mỗi cạnh ít tốn so với chẩn đoán nhiễu đường nối lúc có lúc không chỉ thấy vài mức zoom.

Tràn màu chỉ thấy trong bản phát hành, không trong trình chỉnh sửa. Thường mipmap/nén bật trong build nhưng tắt editor. Thử atlas dễ lem với cùng thiết lập build cuối, không chỉ editor.

Lũy thừa hai còn quan trọng không?

Trước đây GPU yêu cầu kích thước texture lũy thừa hai (256, 512, 1024, 2048…) để hỗ trợ mipmap và một số chế độ lặp hiệu quả; texture khác hoặc thất bại hoặc dùng đường kết xuất chậm. GPU và API hiện đại (OpenGL ES 3+, Metal, Vulkan, DirectX 11+) hỗ trợ kích thước không lũy thừa hai trực tiếp, không có phạt hiệu năng đáng kể cho 2D cơ bản, nên yêu cầu cứng phần lớn đã mất trên đích thế hệ hiện tại.

Vẫn quan trọng trong trường hợp cụ thể:

  • Dùng mipmap. Một số nền tảng và API cũ vẫn cần kích thước lũy thừa hai để tạo chuỗi mip đầy đủ sạch. Nếu atlas dùng mipmap, kiểm tra ràng buộc thực tế của nền tảng đích, đừng giả định.
  • Phần cứng cũ hoặc bị hạn chế. Thiết bị di động yếu, ngữ cảnh WebGL 1 và một số đích nhúng vẫn lợi hoặc cần lũy thừa hai.
  • Căn bộ nhớ và định dạng nén. Định dạng nén khối, dùng nhiều trên di động, nén theo khối kích thước cố định và đôi khi tự đệm kích thước không phù hợp lên mức hợp lệ tiếp theo, nên chọn lũy thừa hai từ đầu tránh vùng đệm âm thầm tốn bộ nhớ vô ích.

Thực tế: với desktop, console hoặc thiết bị di động hiện đại và quy trình 2D chuẩn, atlas không có kích thước lũy thừa hai vẫn ổn. Với WebGL 1, điện thoại cũ hay thiết bị nhúng, lũy thừa hai vẫn an toàn hơn và ít tốn kém; khi bật thiết lập, đa số bộ đóng gói tự làm tròn atlas lên lũy thừa hai kế tiếp.

Cắt gọn và pivot

Cắt gọn nghĩa là công cụ đóng gói cắt từng sprite sát nội dung đục thực tế trước khi đặt vào atlas, bỏ lề trong suốt xung quanh. Điều này hầu như luôn cải thiện mật độ đóng gói — sprite có nhiều đệm trong suốt quanh nhân vật nhỏ chiếm ít không gian atlas hơn nhiều khi cắt sát.

Điểm cần lưu ý là cắt sát thay đổi kích thước hiệu dụng và độ lệch sprite, điều quan trọng nếu logic game hoặc hệ thống hoạt ảnh phụ thuộc vào mọi khung có cùng kích thước và cùng điểm neo so với canvas gốc. Khung đi bộ nghiêng trái có ranh giới đục khác khung nghiêng phải; khi cắt riêng, hai khung có kích thước khác nhau, và đổi qua lại một cách đơn giản làm vị trí biểu kiến của nhân vật dịch giữa các khung.

Mọi công cụ atlas hỗ trợ cắt gọn đều ghi kích thước, độ lệch gốc chưa cắt cùng dữ liệu cắt để giải quyết điều này: engine vẽ sprite cắt tại vị trí đúng trong khung gốc, giữ điểm xoay nhất quán dù giảm không gian lưu pixel trong suốt. Sprite Atlas Unity và importer Godot xử lý tự động nếu cắt gọn bật qua công cụ thay vì làm tay trước đóng gói; tự cắt trước đóng gói làm mất siêu dữ liệu độ lệch engine cần để đặt đúng.

Một atlas hay nhiều atlas?

Nhét mọi thứ vào một atlas khổng lồ không tự động tốt hơn. Một số lý do thực tế để tách nhiều atlas:

  • Lưu thường trú trong bộ nhớ. Nếu một màn chỉ dùng phần sprite, tải một atlas mỗi màn/cảnh tránh giữ dữ liệu không dùng trong GPU khi người chơi chưa ở gần.
  • Giới hạn cỡ texture. Phần cứng giới hạn chiều texture tối đa, thường 4096 hoặc 8192 ở đích hiện đại, thấp hơn trên di động cũ. Bộ sprite đủ lớn không vừa một atlas dù đóng hiệu quả.
  • Tần suất cập nhật. Sprite đổi thường xuyên như UI lặp sửa nhanh nên có atlas riêng, tách hình ổn định ít đổi, để thay nhỏ không buộc dựng và tải lại toàn atlas.
  • Khác biệt vật liệu. Sprite cần shader hay chế độ hòa khác không thể batching cùng dù chung atlas, nên gộp không có lợi ích đóng gói.

Mặc định hợp lý là nhóm theo ngữ cảnh sử dụng: một atlas cho ô môi trường của màn, một cho nhân vật, một cho giao diện, và chỉ gộp thêm nếu đo hiệu năng cho thấy số lệnh vẽ thực sự là nút thắt trên phần cứng đích.

Câu hỏi thường gặp

Thực sự cần bao nhiêu đệm?

Một đến hai điểm ảnh đủ cho phần lớn trường hợp ở tỷ lệ hiển thị gốc và khi tắt mipmap. Nếu sprite được phóng lớn đáng kể lúc chạy, hoặc bật mipmap, hãy tăng lên — không có một con số đúng cho mọi trường hợp, và nên thử atlas cụ thể ở những tỷ lệ và mức thu phóng mà trò chơi thực sự dùng.

Vì sao màu tràn chỉ xuất hiện ở một số mức thu phóng?

Mẫu đó chỉ lem mipmap chứ không lọc ở độ phân giải nền: mức mip tại khoảng cách ấy được tạo từ trộn qua ranh giới sprite. Extrusion và đệm đủ giải quyết cả hai, nhưng nếu riêng mip, xác nhận đệm đủ rộng sống qua nhiều mức thu nhỏ, không chỉ texture gốc.

Tôi cần lo điều này nếu không dùng mipmap hay đổi tỷ lệ sprite không?

Ít hơn nhưng không bằng không. Ngay 1:1 pixel-perfect với lọc point, camera dưới pixel hoặc đặt sprite không nguyên khiến GPU lấy pixel phân số qua ranh giới. Đệm ít là bảo hiểm rẻ ngay với thiết lập 2D pixel-perfect thuần.

Cắt gọn ảnh hưởng hitbox hoặc hình va chạm không?

Không, nếu hình dạng va chạm được định nghĩa độc lập, như cách thiết lập phổ biến. Nếu dự án tự suy ra giới hạn va chạm từ kích thước sprite, hãy kiểm tra nó đọc kích thước đã cắt gọn hay kích thước gốc chưa cắt — dùng trực tiếp kích thước đã cắt gọn có thể khiến vùng va chạm thu nhỏ ngoài ý muốn ở những khung có nhiều lề trong suốt.

Đang tạo sprite để đóng gói vào atlas?

Sprite Gen gộp ảnh riêng vào một sheet theo hàng với đệm bạn đặt: kiểu kệ đã mô tả, không phải biến thể maximal rectangles, đủ cho đa số bộ biểu tượng và khung. Nếu sprite nguồn còn chung một sheet cần tách, Cắt sprite sheet tạo khung riêng. Cả hai hoàn toàn trong trình duyệt, không tải lên.

Mở Tạo sprite