You are currently viewing Sprite Fusion Sprite Normals tạo normal map cho pixel art bằng Rust

Sprite Fusion Sprite Normals tạo normal map cho pixel art bằng Rust

Công cụ này làm gì

Bạn có một sprite pixel art 2D. Bạn muốn nó phản ứng với ánh sáng động trong engine: đèn pin quét qua nhân vật, ngọn lửa bên cạnh hắt bóng, mặt trời di chuyển theo thời gian. Muốn vậy sprite cần có normal map đi kèm. Vẽ tay từng frame normal map cho một animation 8 hướng thì rất mệt. Repo này sinh normal map tự động từ chính file sprite.

Cụ thể hơn: nó đọc silhouette và phần shading có sẵn trong ảnh, dựng lại độ nổi bề mặt, rồi xuất ra một PNG normal map giữ nguyên độ phân giải pixel gốc và kênh alpha. Không scale, không nội suy mịn, không làm mất cái cảm giác pixel cứng. Đầu vào nhận PNG sprite đơn lẻ, PNG sprite sheet, GIF hoặc APNG. Với animation, nó xuất thêm bản color atlas và file JSON chứa metadata (kích thước atlas, kích thước frame, số cột, tổng số frame, timing).

Viết bằng Rust, đóng gói hai dạng: CLI chạy local và thư viện WebAssembly cho trình duyệt. Cùng một bộ generator và decoder animation ở cả hai phía, nên kết quả CLI và WASM khớp nhau.

Vì sao đang được chú ý

560 sao sau khoảng một ngày kể từ lúc tạo repo (2026-10-19, cập nhật 2026-10-20). Con số đó không đến từ marketing, nó đến từ một lỗ hổng khá phổ biến trong pipeline game 2.5D: artist vẽ sprite xong, lập trình viên muốn gắn lighting động, và giữa hai bên là một khoảng trống không ai muốn làm. Normal map cho pixel art không phải bài toán mới, nhưng phần lớn workflow hiện có là vẽ tay trong Aseprite hoặc dùng plugin của engine, và cả hai đều vỡ khi số lượng frame tăng lên.

Điểm khác biệt nằm ở chỗ công cụ xử lý được animated sprite sheet như một đơn vị, không bắt bạn cắt frame ra từng file rồi ghép lại. Thêm nữa, tham số được phơi ra rõ ràng (strength, detail, shape) nên bạn tinh chỉnh được thay vì nhận một output đen hộp. Có bản online, có API, có cả MCP, tức là nếu bạn muốn nhét nó vào pipeline AI agent thì cũng có đường.

Và nó miễn phí, MIT, chạy local. Với dân làm game indie thì ba chữ đó gần như đủ.

Cài và chạy thử

Yêu cầu Rust 1.85 trở lên. Build từ source:

cargo build --release --locked

Binary nằm ở target/release/spritefusion-sprite-normals. Muốn cài vào PATH:

cargo install --path . --locked

Dùng cơ bản nhất, một sprite PNG ra một normal PNG:

spritefusion-sprite-normals sprite.png -o sprite_n.png

Batch cả thư mục:

spritefusion-sprite-normals sprites/*.png --output-dir normals

Tăng độ nổi:

spritefusion-sprite-normals sprite.png -o sprite_n.png --normal-strength 2

Sprite sheet có ô đều nhau, chỉ định frame size:

spritefusion-sprite-normals sheet.png --frame-size 32x32 -o sheet_n.png

Animation GIF:

spritefusion-sprite-normals walk.gif -o walk_n.png

Không truyền output path thì nó ghi file cạnh input với hậu tố _n.png. Các tham số tinh chỉnh có default khá hợp lý: --normal-strength 1.5, --detail-strength 6, --detail-scale 0.6, --shape-strength 1.15. Cờ --normal-format directx sẽ đảo kênh green, mặc định là OpenGL. Cờ --force cho phép ghi đè output, còn file input thì được bảo vệ.

Với GIF hoặc APNG, bạn nhận ba file: walk_n.png (normal map atlas lossless), walk_n.color.png (color atlas cùng layout), walk_n.frames.json (metadata kèm timing). Frame xếp theo row-major, delays_ms chứa các cặp [tử số, mẫu số] tính bằng ms. PNG sheet giữ nguyên layout và nhận delay mặc định 100 ms. Loop count của file gốc không được xuất ra, đây là điểm cần biết nếu bạn phụ thuộc vào nó.

Bản WASM build bằng:

cargo build --release --target wasm32-unknown-unknown --no-default-features --locked
wasm-bindgen target/wasm32-unknown-unknown/release/spritefusion_sprite_normals.wasm --target web --out-dir pkg

Rồi gọi trong JS:

import init, { generate_normal_map } from "./pkg/spritefusion_sprite_normals.js";

await init();
const result = generate_normal_map(inputBytes, JSON.stringify({ strength: 1.5 }));
try {
 const normalPng = result.normal_png;
 const colorPng = result.color_png;
 const metadata = JSON.parse(result.metadata);
} finally {
 result.free();
}

Lưu ý là các mảng byte trả về vẫn dùng được sau khi gọi free(). Giới hạn đầu vào: 64 MiB và 4.194.304 pixel (tính cả padding), animation thêm trần 256 frame và 4.194.304 pixel decode tổng cộng. Đủ cho hầu hết sprite pixel art, nhưng không dành cho ảnh độ phân giải cao.

Ai nên dùng, dùng thế nào

Rõ ràng nhất là dev làm game 2D có ánh sáng động, kiểu top-down hoặc platformer mà đèn, lửa, đạn, mặt trời di chuyển liên tục. Nếu bạn đã có normal lighting trong engine (Godot, Unity URP 2D, custom shader) thì đây là mảnh ghép còn thiếu.

Nhóm thứ hai là người làm asset pipeline. CLI chạy được trong script, nhận glob, xuất ra thư mục riêng, có --force. Bạn có thể gắn nó vào bước build sau khi export sprite từ Aseprite.

Nhóm thứ ba là tool web. Bản WASM cho phép xử lý ngay trong trình duyệt, không upload ảnh lên server. Nếu bạn đang viết một editor pixel art nhỏ, đây là cách thêm tính năng normal map mà không phải viết lại thuật toán.

Nhóm cuối, hơi khác: người chơi với AI-generated pixel art. Sprite sinh từ model thường lệch grid, shading lộn xộn, và một normal map dựng từ silhouette đôi khi làm lộ rõ vấn đề đó. Đó là điểm trừ, nhưng cũng là công cụ để kiểm tra chất lượng sprite đầu vào.

Những điểm cần cân nhắc

Điểm mạnh đã nói: giữ nguyên độ phân giải pixel, xử lý animation như một khối, không phụ thuộc dịch vụ ngoài, MIT. Pipeline đơn giản, một lệnh là xong.

Hạn chế thật thì có vài cái. Thứ nhất, đây là công cụ suy luận: nó đoán độ nổi từ màu và silhouette, không biết hình dạng thật của vật thể. Highlight cọ vẽ tay có thể bị hiểu thành chi tiết bề mặt. Với sprite có shading phức tạp hoặc ánh sáng bake sẵn theo một hướng cố định, kết quả sẽ lệch. README nói thẳng điều này, đáng ghi nhận.

Thứ hai, không export loop count từ GIF/APNG. Nếu animation của bạn dựa vào loop vô hạn so với loop một lần, phải tự xử lý phía engine.

Thứ ba, repo mới. Tạo ngày 2026-10-19, mới một ngày tuổi tại thời điểm này. API có thể đổi, format JSON có thể đổi, chưa có hệ sinh thái plugin cho các engine lớn. Đừng đưa vào pipeline production mà không pin version và đọc changelog.

Thứ tư, normal map là data texture: bạn phải tắt sRGB sampling trong engine khi bind nó, nếu không ánh sáng sẽ sai. Đây là lỗi kinh điển, README có nhắc nhưng dễ bỏ qua.

Tài liệu: README khá đầy đủ cho CLI và WASM, có trang docs riêng cho API và MCP. Không có hướng dẫn cụ thể cho từng engine, nên bạn phải tự biết cách import normal map vào Godot hay Unity.

Về so sánh, tôi không tiện đưa ra danh sách công cụ tương đương vì phần lớn lựa chọn khác là plugin gắn với một engine cụ thể hoặc công cụ trong Aseprite, và tôi chưa kiểm chứng đủ để nói cái nào hơn cái nào ở đây.

Kết

Nếu bạn đang làm game 2.5D với pixel art và chưa có normal map, thử một lệnh là biết ngay có hợp không: spritefusion-sprite-normals walk.gif -o walk_n.png. Một repo 560 sao trong ngày đầu, viết bằng Rust, MIT, cho ra ba file từ một GIF. Đáng để bỏ 10 phút cài thử, miễn là bạn nhớ nó mới một ngày tuổi và đừng vội đưa vào build production.

Repo: Hugo-Dz/spritefusion-sprite-normals. Bản online ở spritefusion.com/sprite-normals nếu bạn muốn thử trước khi build.

Đăng ngày 22/10/2026
Giang Na
Về tác giả: Giang Na
Người lập ra blog từ 2015 với những bài về Blogspot. Giờ tập trung vào cách đưa AI vào việc thường ngày, theo hướng thực dụng và dễ làm theo.

Giang Na

Người lập ra blog từ 2015 với những bài về Blogspot. Giờ tập trung vào cách đưa AI vào việc thường ngày, theo hướng thực dụng và dễ làm theo.