Nhiễu Perlin: Thuật toán khiến sinh thủ tục trông đẹp mắt
Một bài phân tích sâu về thuật toán nhiễu Perlin, từ nền tảng của nhiễu kết hợp đến chi tiết triển khai vector gradient và bảng hoán vị.
Sinh thủ tục chẳng là gì nếu không có nhiễu, và hàm nhiễu nổi tiếng nhất là nhiễu Perlin. Bài viết này hướng dẫn từng bước thuật toán, bắt đầu từ khái niệm cơ bản về hàm nhiễu cho đến triển khai đầy đủ.
Nhiễu là gì?
Về cốt lõi, nhiễu chỉ là một tập hợp các giá trị ngẫu nhiên — một hàm nhận N tham số và trả về một giá trị theo một số quy tắc. Với nhiễu 2D, bạn có thể hình dung nó như một kết cấu xám: độ sáng của mỗi pixel tương ứng với giá trị nhiễu tại tọa độ đó.
Cách tiếp cận ngây thơ — dùng một hàm ngẫu nhiên đơn giản như rand(x + y) — tạo ra nhiễu trắng: mỗi pixel độc lập, không có mẫu hình rõ rệt. Điều đó không hữu ích lắm để tạo địa hình, mây hay kết cấu.
Nhiễu kết hợp
Thứ bạn thực sự cần là nhiễu kết hợp, có ba thuộc tính:
- Cùng đầu vào luôn cho cùng đầu ra
- Thay đổi nhỏ ở đầu vào tạo ra thay đổi nhỏ ở đầu ra
- Thay đổi lớn ở đầu vào tạo ra thay đổi ngẫu nhiên ở đầu ra
Điều này mang lại các mẫu hình mượt mà, hữu cơ nhưng vẫn có cảm giác ngẫu nhiên tổng thể.
Thuật toán của Perlin
Ken Perlin phát triển thuật toán này vào năm 1983 khi làm việc cho bộ phim Tron của Disney, thất vọng với vẻ ngoài máy móc của CGI. Ông đã giành giải Oscar cho nó vào năm 1997.
Ý tưởng cốt lõi: với bất kỳ điểm nào trên mặt phẳng 2D, bạn tìm bốn góc nguyên của ô vuông chứa nó. Mỗi góc có một vector đơn vị ngẫu nhiên được tính trước (gradient). Bạn tính các vector từ mỗi góc đến điểm, lấy tích vô hướng của từng cặp, sau đó nội suy bốn tích vô hướng.
Gradient và bảng hoán vị
Vì không thể tính trước vô số gradient, Perlin dùng một bộ cố định 256 vector đơn vị ngẫu nhiên và một bảng hoán vị 256 phần tử. Bảng hoán vị được xáo trộn bằng kiểu hoán đổi Fisher-Yates, sau đó nhân đôi lên 512 mục để tránh tràn bộ đệm.
Đây là mã thiết lập:
for (int i = 0; i < B; i++) {
permutation[i] = i;
gradients[i] = Vector2.Random();
gradients[i] = Vector2.Normalize(gradients[i]);
}
for (int i = 0, j = 0; i < B; i++) {
int k = permutation[i];
permutation[i] = permutation[j = random() % B];
permutation[j] = k;
}
for (i = 0; i < B + 2; i++) {
permutation[B + i] = permutation[i];
gradients[B + i] = gradients[i];
}Trong đó B là 256. Bảng hoán vị làm cho nhiễu có tính xác định nhưng có vẻ ngẫu nhiên — cùng một tọa độ nguyên luôn ánh xạ đến cùng một gradient, nhưng các tọa độ liền kề nhận được các gradient khác nhau.
Tính giá trị nhiễu
Với một điểm (x, y) cho trước, bạn tính các vector tương đối đến bốn góc và tra cứu gradient qua bảng hoán vị. Sau đó lấy tích vô hướng và nội suy bằng hàm smoothstep (thường là 6t⁵ - 15t⁴ + 10t³) để tránh các hiện vật lưới nhìn thấy được.
Triển khai gốc dùng phép AND bit với 255 để bọc tọa độ — thực chất là phép modulo giữ cho nhiễu tuần hoàn mỗi 256 đơn vị. Đây là một hạn chế thực tế: ngoài phạm vi đó, mẫu hình lặp lại.
Ken Perlin phát triển nhiễu Perlin vào năm 1983 do thất vọng với vẻ ngoài 'máy móc' của hình ảnh do máy tính tạo ra vào thời điểm đó.