Tóm tắt: Normal Map là kỹ thuật "lừa mắt" ánh sáng để bề mặt phẳng trông như có chi tiết lồi lõm, mà không cần thêm polygon. Bài viết này sẽ giúp bạn hiểu rõ Normal Map hoạt động ra sao, cách dùng trong Unity, và những sai lầm hay gặp.
--
1. Normal Map là gì?
Hãy tưởng tượng bạn có một bức tường phẳng trong game. Nếu muốn nó trông như gạch lồi lõm, bạn có 2 cách:
Cách 1: Tạo model 3D với hàng nghìn polygon cho từng viên gạch → nặng, tốn hiệu năng.
Cách 2: Dùng một tấm ảnh đặc biệt (Normal Map) để "nói dối" ánh sáng rằng bề mặt không phẳng → nhẹ, hiệu quả.
Normal Map chính là cách 2. Nó là một texture mà mỗi pixel lưu trữ hướng (direction) của bề mặt tại điểm đó, thay vì màu sắc. Khi ánh sáng chiếu vào, shader sẽ dùng thông tin hướng này để tính toán bóng đổ, phản chiếu — tạo ảo giác chi tiết 3D.
Tại sao Normal Map thường có màu xanh tím?
Mỗi pixel trong Normal Map mã hoá 3 giá trị (X, Y, Z) vào 3 kênh màu (R, G, B):
Kênh
Trục
Ý nghĩa
R (Đỏ)
X
Hướng trái/phải
G (Xanh lá)
Y
Hướng lên/xuống
B (Xanh dương)
Z
Hướng ra ngoài (vuông góc bề mặt)
Phần lớn bề mặt hướng thẳng ra ngoài (Z = 1), nên kênh B luôn gần max → ảnh có tông xanh dương/tím đặc trưng.
2. Cách hoạt động (đơn giản)
Không có Normal Map:
Ánh sáng → Bề mặt phẳng → Bóng đổ đều → Trông phẳng lì
Có Normal Map:
Ánh sáng → Đọc hướng từ Normal Map → Mỗi pixel phản xạ khác nhau → Trông lồi lõm
Bản chất, Normal Map không thay đổi hình dạng thật của mesh. Nó chỉ thay đổi cách ánh sáng tương tác với bề mặt. Nếu bạn nhìn mép (edge) của object, bạn sẽ thấy nó vẫn phẳng.
3. Tangent Space vs Object Space vs World Space
Tangent Space (phổ biến nhất — 90% trường hợp)
Hướng được tính tương đối với bề mặt mesh.
Có thể dùng lại cho nhiều object khác nhau.
Đây là loại mặc định trong Unity.
Ảnh có tông xanh tím đặc trưng.
Object Space
Hướng được tính theo toạ độ của object.
Ảnh có nhiều màu sắc hơn (đỏ, xanh lá, xanh dương đều rõ).
Không thể dùng lại nếu mesh bị biến dạng (skinned mesh, animation).
Dùng cho vật thể tĩnh, không animate.
World Space
Hướng theo toạ độ thế giới.
Hiếm dùng, chủ yếu cho terrain hoặc hiệu ứng đặc biệt.
Kết luận: Trong Unity, 99% bạn sẽ dùng Tangent Space. Chỉ cần nhớ loại này.
4. Cách sử dụng Normal Map trong Unity
Bước 1: Import texture
Kéo file ảnh Normal Map vào project. Unity sẽ tự nhận diện nếu tên file chứa _Normal hoặc _N.
Bước 2: Cài đặt Import Settings
Chọn texture trong Inspector:
Texture Type: Normal map ← BẮT BUỘC phải chọn đúng
(nếu để Default, kết quả sẽ sai)
Create from Grayscale: ☐ ← Tick nếu ảnh input là ảnh xám (height map)
(Unity sẽ tự chuyển thành Normal Map)
Bumpiness: 0.0 - 10.0 ← Chỉ hiện khi tick "Create from Grayscale"
Độ mạnh của hiệu ứng lồi lõm
Filter Mode: Bilinear / Trilinear
Max Size: Tuỳ nhu cầu (512, 1024, 2048...)
Compression: Normal Quality
Lưu ý quan trọng: Khi bạn chọn Texture Type = Normal map, Unity sẽ tự động pack texture theo format tối ưu cho từng platform (ví dụ: DXT5nm trên PC, ASTC trên mobile). Đây là lý do bạn phải chọn đúng type.
Bước 3: Gán vào Material
Trong material (Standard, URP/Lit, HDRP/Lit...), tìm slot Normal Map và kéo texture vào:
Material Inspector:
├── Albedo (Base Map) ← Màu sắc
├── Metallic / Smoothness ← Chất liệu
├── Normal Map ← KÉO VÀO ĐÂY
│ └── Strength: 1.0 ← Điều chỉnh độ mạnh (0 = tắt, >1 = cường điều)
└── ...
Ví dụ cụ thể — Tường gạch
1. Model: Một plane phẳng (2 triangle)
2. Albedo: Ảnh tường gạch có màu sắc
3. Normal Map: Ảnh normal map của gạch (tông xanh tím)
4. Kết quả: Tường trông lồi lõm, có bóng giữa các viên gạch
→ Chỉ tốn 2 triangle thay vì hàng nghìn!
5. Normal Map Strength (Cường độ)
Trong material, bạn sẽ thấy thanh trượt Strength (hoặc Normal Scale trong URP/HDRP):
Giá trị
Hiệu ứng
0
Tắt hoàn toàn, bề mặt phẳng lì
0.5
Hiệu ứng nhẹ, subtle
1.0
Mặc định, đúng như artist thiết kế
2.0+
Cường điệu, chi tiết rõ hơn nhưng có thể trông giả
Trong Shader Graph / code:
// Trong shader, bạn điều chỉnh strength bằng cách lerp với (0,0,1)
float3 normal = UnpackNormal(tex2D(_BumpMap, uv));
normal.xy *= _BumpScale; // _BumpScale = strength
normal.z = sqrt(1.0 - saturate(dot(normal.xy, normal.xy)));
6. Tạo Normal Map từ Height Map (Grayscale)
Nếu bạn chỉ có ảnh xám (trắng = cao, đen = thấp), Unity có thể tự tạo Normal Map:
Import Settings:
Texture Type: Normal map
☑ Create from Grayscale ← Tick vào
Bumpiness: 0.5 ← Điều chỉnh độ mạnh
Filtering: Sharp / Smooth
Cách này tiện nhưng chất lượng kém hơn Normal Map được tạo từ phần mềm chuyên dụng (Substance, xNormal, Blender).
7. Normal Map trong từng Render Pipeline
Built-in Render Pipeline
Material: Standard Shader
Slot: Normal Map (với _BumpMap)
Property trong code: _BumpMap, _BumpScale
URP (Universal Render Pipeline)
Material: Lit Shader
Slot: Normal Map (với _BumpMap)
Property trong code: _BumpMap, _BumpScale
Lưu ý: Cần bật keyword _NORMALMAP trong shader
HDRP (High Definition Render Pipeline)
Material: Lit Shader
Slot: Normal Map
Hỗ trợ thêm: Detail Normal Map (lớp chi tiết thứ 2)
Bent Normal Map (cho ambient occlusion chính xác hơn)
Shader Graph
Dùng node Normal Map hoặc Sample Texture 2D rồi nối vào output Normal (Tangent Space):
Unity hỗ trợ Detail Normal Map — một Normal Map thứ 2 được tile (lặp lại) nhiều lần hơn để thêm chi tiết nhỏ.
Ví dụ:
Normal Map chính: Vân gỗ lớn
Detail Normal Map: Sớ gỗ nhỏ li ti, lặp lại 10 lần
Material Inspector:
├── Normal Map ← Chi tiết lớn
├── Detail Normal Map ← Chi tiết nhỏ (tile cao hơn)
│ └── Tiling: 10x10
└── Detail Mask ← Ảnh xám quyết định vùng nào hiện detail
Kết quả: Bề mặt vừa có cấu trúc lớn, vừa có chi tiết nhỏ — rất realistic.
9. Blending Normal Maps (Trộn nhiều Normal Map)
Khi cần trộn 2+ Normal Map (ví dụ: terrain với nhiều lớp), có 3 phương pháp:
Khuyến nghị: Dùng RNM nếu cần chất lượng. Trong Shader Graph, dùng node Normal Blend (chọn mode Reoriented).
10. Normal Map và Mobile — Tối ưu hiệu năng
Compression
Platform
Format khuyên dùng
Ghi chú
Android
ASTC 6x6
Cân bằng chất lượng/dung lượng
iOS
ASTC 4x4 hoặc 6x6
ASTC được hỗ trợ tốt
PC
DXT5nm (BC5)
Chất lượng cao nhất
Giảm kích thước
Normal Map thường chấp nhận được ở 512x512 hoặc 1024x1024 trên mobile.
Không cần thiết dùng 2048 cho Normal Map trừ khi object rất lớn và camera rất gần.
Tắt Normal Map khi không cần
// Tắt Normal Map ở runtime để tiết kiệm hiệu năng trên thiết bị yếu
material.DisableKeyword("_NORMALMAP");
material.SetTexture("_BumpMap", null);
11. Các lỗi thường gặp và cách fix
❌ Lỗi 1: Normal Map trông sai, bề mặt lồi thành lõm
Nguyên nhân: Sai hệ toạ độ. DirectX dùng Y+ hướng xuống, OpenGL dùng Y+ hướng lên.
Fix:
Import Settings → Tick "Flip Green Channel"
Hoặc: Trong phần mềm tạo Normal Map, export đúng format cho Unity (OpenGL).
Mẹo nhớ: Unity dùng OpenGL style (Y+ hướng lên). Nếu Normal Map từ Unreal/DirectX, bạn cần flip kênh Green.
❌ Lỗi 2: Texture Type để sai thành "Default"
Triệu chứng: Bề mặt trông kỳ lạ, có màu xanh tím thay vì lồi lõm.
Fix: Đổi Texture Type thành Normal map trong Import Settings.
❌ Lỗi 3: UV seam (đường nối) rõ trên model
Nguyên nhân: Tại các đường nối UV, tangent space bị gián đoạn.
Fix:
Đảm bảo model export với tangent và binormal.
Trong Unity: Import Settings → Normals: Import (không để Calculate).
Sử dụng Mikk tangent space (mặc định của Unity) khi bake trong phần mềm 3D.
❌ Lỗi 4: Normal Map không có tác dụng
Nguyên nhân có thể:
Không có light trong scene.
Shader không hỗ trợ Normal Map.
Keyword _NORMALMAP chưa được bật.
Fix:
// Kiểm tra keyword
Debug.Log(material.IsKeywordEnabled("_NORMALMAP"));
// Bật thủ công
material.EnableKeyword("_NORMALMAP");
❌ Lỗi 5: Hiệu ứng quá mạnh / quá yếu
Fix: Điều chỉnh thanh Normal Strength trong material (giá trị 0 → 2).
12. Bake Normal Map từ High-poly sang Low-poly
Đây là quy trình phổ biến nhất để tạo Normal Map chất lượng cao:
Quy trình:
1. Tạo model High-poly (hàng triệu polygon) → Chi tiết đầy đủ
2. Tạo model Low-poly (vài trăm/nghìn polygon) → Dùng trong game
3. Bake: "Chuyển" chi tiết từ high-poly sang normal map cho low-poly
Công cụ phổ biến:
- Substance Painter / Designer
- xNormal (miễn phí)
- Blender (tích hợp sẵn)
- Marmoset Toolbag
Lưu ý khi bake:
Low-poly và high-poly phải chồng khớp vị trí.
Dùng cage (lồng bao) để kiểm soát khoảng cách ray casting.
// Sample normal map
float4 normalSample = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, uv);
float3 normalTS = UnpackNormalScale(normalSample, _BumpScale);
// Chuyển từ tangent space sang world space
float3 normalWS = TransformTangentToWorld(normalTS,
float3x3(input.tangentWS, input.bitangentWS, input.normalWS));
14. So sánh Normal Map với các kỹ thuật khác
Kỹ thuật
Chi tiết
Hiệu năng
Thay đổi silhouette?
Dùng khi
Normal Map
Trung bình
Rẻ
❌ Không
Hầu hết mọi thứ
Bump Map
Thấp
Rất rẻ
❌ Không
Hiệu ứng đơn giản
Parallax Map
Cao
Trung bình
❌ Không (nhưng có ảo giác)
Sàn, tường nhìn gần
Displacement Map
Rất cao
Đắt
✅ Có
Terrain, cinematic
Tessellation + Displacement
Rất cao
Rất đắt
✅ Có
AAA, next-gen
Giải thích thêm
Bump Map: Phiên bản đời đầu, chỉ dùng 1 kênh (grayscale). Normal Map là bản nâng cấp.
Parallax Map: Thêm hiệu ứng dịch UV theo góc nhìn, tạo cảm giác "sâu" hơn Normal Map.
Displacement Map: Thực sự dịch chuyển vertex → silhouette thay đổi thật, nhưng cần nhiều polygon.
15. Điểm mạnh & Điểm yếu
✅ Điểm mạnh
Hiệu năng rẻ: Gần như không tốn thêm chi phí so với không dùng (chỉ thêm 1 texture sample).
Chất lượng cao: Tạo ảo giác chi tiết 3D rất thuyết phục ở hầu hết góc nhìn.
Linh hoạt: Dùng được cho mọi loại object — nhân vật, môi trường, props.
Tiết kiệm polygon: Giảm đáng kể số polygon cần thiết → game chạy mượt hơn.
Dễ dùng: Unity hỗ trợ sẵn, chỉ cần kéo thả.
❌ Điểm yếu
Không thay đổi silhouette: Nhìn từ cạnh, object vẫn phẳng. Đây là hạn chế lớn nhất.
Góc nhìn cạn: Khi camera nhìn gần song song bề mặt, hiệu ứng bị "bẹp".
Không tự đổ bóng: Các chi tiết normal map không tạo bóng lên chính nó (self-shadowing). Cần thêm kỹ thuật khác.
Phụ thuộc ánh sáng: Trong môi trường không có light (unlit shader), Normal Map vô dụng.
UV dependency: Chất lượng phụ thuộc vào UV unwrap tốt.
16. Tips & Best Practices
Cho Artist
Luôn export OpenGL format cho Unity (Y+ hướng lên).
Dùng Mikk tangent space khi bake — khớp với Unity.
Đừng dùng JPEG cho Normal Map — compression artifact sẽ tạo noise. Dùng PNG hoặc TGA.
Padding UV islands ít nhất 4-8 pixel để tránh seam khi mipmap.
Cho Programmer
Luôn set Texture Type = Normal Map khi import.
DùngUnpackNormal()hoặcUnpackNormalScale() — đừng tự đọc RGB rồi chuyển đổi thủ công.
Enable keyword_NORMALMAP nếu set texture bằng code.
Giảm size Normal Map trên mobile — 512 thường đủ cho hầu hết object.
Cho cả hai
Kiểm tra với directional light xoay xung quanh — đây là cách tốt nhất để thấy Normal Map hoạt động.
Đừng set strength quá cao — giá trị 0.8 - 1.2 thường là ngon nhất.
Kết hợp với AO map (Ambient Occlusion) để tăng chiều sâu.
17. Script ví dụ: Thay đổi Normal Map lúc runtime
using UnityEngine;
public class NormalMapSwitcher : MonoBehaviour
{
[Header("Normal Maps")]
[SerializeField] private Texture2D normalMapDry;
[SerializeField] private Texture2D normalMapWet;
[Header("Settings")]
[SerializeField, Range(0f, 2f)] private float normalStrength = 1f;
private Material _material;
private void Start()
{
// Lấy material instance (tạo bản copy để không ảnh hưởng asset gốc)
_material = GetComponent<Renderer>().material;
}
/// <summary>
/// Chuyển sang Normal Map ướt (ví dụ: khi trời mưa)
/// </summary>
public void SetWet()
{
SetNormalMap(normalMapWet);
}
/// <summary>
/// Chuyển sang Normal Map khô
/// </summary>
public void SetDry()
{
SetNormalMap(normalMapDry);
}
private void SetNormalMap(Texture2D normalMap)
{
if (normalMap == null)
{
// Tắt Normal Map
_material.DisableKeyword("_NORMALMAP");
_material.SetTexture("_BumpMap", null);
}
else
{
// Bật Normal Map
_material.EnableKeyword("_NORMALMAP");
_material.SetTexture("_BumpMap", normalMap);
_material.SetFloat("_BumpScale", normalStrength);
}
}
private void OnDestroy()
{
// Dọn dẹp material instance tránh memory leak
if (_material != null)
Destroy(_material);
}
}
18. Packed Texture — Normal Map kết hợp với texture khác
Trong game AAA và mobile tối ưu, người ta hay pack nhiều thông tin vào 1 texture:
Ví dụ format phổ biến:
R: Metallic
G: AO (Ambient Occlusion)
B: không dùng / Detail Mask
A: Smoothness
→ Normal Map để riêng (vì cần format compression đặc biệt)
Unity URP/HDRP mặc định dùng Metallic-Smoothness packed (Smoothness ở kênh Alpha của Metallic map). Normal Map luôn để riêng vì cần format nén chuyên biệt (BC5/ASTC).
19. Checklist nhanh khi dùng Normal Map
☐ Texture Type đã set thành "Normal map"?
☐ File ảnh là PNG/TGA (không phải JPEG)?
☐ Đúng hệ toạ độ OpenGL (Y+ hướng lên)?
☐ Material có slot Normal Map và đã gán?
☐ Scene có ít nhất 1 light source?
☐ Shader hỗ trợ Normal Map?
☐ Kích thước texture phù hợp (mobile: 512, PC: 1024-2048)?
☐ Normal Strength ở mức hợp lý (0.8 - 1.2)?
☐ Không bị seam ở đường nối UV?
Tổng kết
Normal Map là công cụ không thể thiếu trong pipeline đồ hoạ game hiện đại. Nó giúp bạn:
Thêm chi tiết 3D mà gần như miễn phí về hiệu năng.
Giảm polygon count đáng kể.
Nâng chất lượng visual lên nhiều bậc.
Hạn chế chính là không thay đổi silhouette và phụ thuộc ánh sáng. Nhưng với hầu hết game (đặc biệt mobile), Normal Map là lựa chọn tối ưu nhất giữa chất lượng và hiệu năng.
Rule of thumb: Nếu chi tiết đó nhỏ hơn 1-2cm trong game → dùng Normal Map. Nếu lớn hơn và ảnh hưởng silhouette → cần geometry thật.