Giới thiệu về mô phỏng quang học
Trong đồ họa máy tính 3D, kỹ thuật Ray Tracing (dò tia) là phương pháp mô phỏng hành vật lý của ánh sáng để tạo ra hình ảnh chân thực. Khác với kỹ thuật Rasterization truyền thống chỉ xử lý các điểm trên bề mặt vật thể dựa trên góc nhìn của camera, Ray Tracing tính toán sự tương tác của ánh sáng khi nó di chuyển, phản xạ và khúc xạ qua các vật thể trong cảnh.
Cơ sở toán học của Tia sáng
Một tia sáng (Ray) trong không gian 3D được định nghĩa bằng một điểm gốc $O$ và một vectơ phương hướng đơn vị $D$. Mọi điểm $P$ nằm trên tia sáng có thể được biểu diễn theo tham số $t$:
$P(t) = O + tD$
với $t \ge 0$. Giá trị $t$ đại diện cho khoảng cách từ điểm gốc đến điểm $P$.
Tương tác Tia - Mặt phẳng và Mặt cầu
Để xác định xem một tia sáng có chạm vật thể hay không, ta cần giải phương trình giao điểm.
Đối với Mặt cầu có tâm $C$ và bán kính $R$, phương trình là $|P - C|^2 = R^2$. Thay thế $P(t)$ vào phương trình mặt cầu ta được một phương trình bậc hai. Số lượng nghiệm thực của phương trình này sẽ cho biết số điểm giao (0, 1 hoặc 2 điểm).
Đối với Hình tam giác, ta thường sử dụng phương pháp Möller–Trumbore. Ta tìm giao điểm giữa tia và mặt phẳng chứa tam giác, sau đó sử dụng tọa độ trọng tâm (barycentric coordinates) để kiểm tra xem điểm đó có nằm bên trong tam giác hay không.
Cấu trúc tăng tốc (Bounding Volume Hierarchy - BVH)
Việc kiểm tra giao điểm với từng tam giác một cách riêng lẻ là vô cùng tốn kém về mặt tính toán. Để tối ưu hóa, ta sử dụng các cấu trúc dữ liệu không gian như AABB (Axis-Aligned Bounding Box) và BVH.
AABB là một hộp giới hạn các cạnh song song với các trục tọa độ. Nếu tia sáng không giao với hộp bao này, ta có thể bỏ qua toàn bộ các vật thể bên trong nó. Thuật toán kiểm tra giao AABB sử dụng phương pháp "slabs", trong đó ta tính toán khoảng nhập ($t_{enter}$) và khoảng xuất ($t_{exit}$) của tia trên từng cặp mặt phẳng song song. Tia chỉ đi qua hộp khi $t_{enter} < t_{exit}$ và $t_{exit} > 0$.
BVH là một cây nhị phân trong đó mỗi nút lá chứa một nhóm tam giác và các nút nội bộ chứa hộp bao của các nút con. Việc phân chia cây thường dựa trên heuristic như SAH (Surface Area Heuristic) để giảm thiểu diện tích bề mặt của các nút cha, từ đó tối đa hóa khả năng loại bỏ nhánh tính toán sớm.
Giải thuật duyệt BVH
Dưới đây là mã giả tối giản cho việc đệ quy duyệt cây BVH để tìm điểm giao gần nhất:
function traverseBVH(ray, node):
// Nếu tia không cắt hộp bao của nút hiện tại, dừng lại
if not node.bbox.intersect(ray):
return no_hit
// Nếu là nút lá, kiểm tra từng tam giác trong nút
if node.is_leaf:
closest_hit = no_hit
for triangle in node.triangles:
hit = triangle.intersect(ray)
if hit.distance < closest_hit.distance:
closest_hit = hit
return closest_hit
// Nếu là nút nội bộ, đệ quy vào hai nhánh con
hit_left = traverseBVH(ray, node.left_child)
hit_right = traverseBVH(ray, node.right_child)
// Trả về điểm giao gần hơn
return closer(hit_left, hit_right)
Hệ thống đo lường bức xạ (Radiometry)
Để tính toán ánh sáng chính xác, ta sử dụng các đơn vị vật lý:
- Radiant Energy (Q): Năng lượng bức xạ (Joules).
- Radiant Flux ($\Phi$): Công suất, tức là năng lượng trên một đơn vị thời gian (Watts hoặc Lumens).
- Radiant Intensity (I): Công suất trên một đơn vị góc khối (Steradians - sr).
- Ir-radiance (E): Công suất nhận được trên một đơn vị diện tích bề mặt ($W/m^2$). Đây là đại lượng mô tả ánh sáng chiếu tới một điểm.
- Radiance (L): Công suất trên một đơn vị góc khối và một đơn vị diện tích chiếu (projected area). Đây là đại lượng cơ bản nhất trong Ray Tracing, mô tả ánh sáng đi theo một hướng cụ thể tại một điểm.
Hàm phản xạ BRDF và Phương trình Render
BRDF (Bidirectional Reflectance Distribution Function) xác định tỷ lệ ánh sáng bị phản xạ từ hướng chiếu tới ($\omega_i$) sang hướng quan sát ($\omega_o$) tại một điểm trên bề mặt.
Phương trình Render toàn cảnh mô tả cân bằng năng lượng ánh sáng tại một điểm $p$:
$L_o(p, \omega_o) = L_e(p, \omega_o) + \int_{\Omega} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) (n \cdot \omega_i) d\omega_i$
Trong đó:
- $L_o$: Ánh sáng đi ra (outgoing radiance).
- $L_e$: Ánh sáng phát ra (emitted radiance).
- $L_i$: Ánh sáng tới (incoming radiance).
- $f_r$: Hàm BRDF.
Path Tracing và Tích phân Monte Carlo
Phương trình Render là một tích phân khó giải tích phân chính xác. Ta sử dụng phương pháp Monte Carlo Integration để ước lượng giá trị của nó bằng cách lấy mẫu ngẫu nhiên.
Thuật toán Path Tracing hoạt động bằng cách bắn các tia từ camera. Khi tia chạm vật thể, ta tiếp tục bắn các tia thứ cấp (secondary rays) để lấy mẫu ánh sáng từ môi trường xung quanh (Global Illumination). Quá trình này được lặp lại đệ quy.
Triển khai Path Tracing tối giản
Dưới đây là mã giả cho hàm tính toán độ sáng tại một điểm sử dụng Path Tracing cơ bản:
function calculateRadiance(scene, ray, depth):
if depth >= MAX_DEPTH:
return Color(0, 0, 0) // Dừng đệ quy nếu quá sâu
hit = scene.findClosestIntersection(ray)
if not hit:
return scene.environmentColor // Nếu không chạm gì, trả màu môi trường
// 1. Tính toán ánh sáng phát ra từ chính vật thể (nếu có)
radiance = hit.object.emission
// 2. Tính toán ánh sáng phản xạ (Sử dụng Monte Carlo)
// Lấy mẫu hướng ngẫu nhiên trên bán cầu theo BRDF
sample_direction = sampleBRDFDirection(hit.material)
pdf = hit.material.pdf(sample_direction) // Probability Density Function
// Tạo tia mới từ điểm giao
secondary_ray = Ray(hit.point, sample_direction)
// Đệ quy tính toán ánh sáng tới từ hướng mới
indirect_radiance = calculateRadiance(scene, secondary_ray, depth + 1)
// Công thức Monte Carlo: (Li * fr * cos_theta) / pdf
cos_theta = dot(hit.normal, sample_direction)
brdf_value = hit.material.brdf(ray.direction, sample_direction)
radiance += indirect_radiance * brdf_value * cos_theta / pdf
return radiance
Kỹ thuật Russian Roulette
Một vấn đề của Path Tracing là số lượng tia tăng theo cấp số nhân theo độ sâu, dẫn đến không thể kết thúc. Để giải quyết, ta sử dụng Russian Roulette: tại mỗi bước phản xạ, ta có xác suất $P$ để dừng việc dò tia. Nếu tiếp tục, ta phải chia giá trị kết quả cho $P$ để đảm bảo giá trị kỳ vọng không bị lệch.
Cập nhật mã giả để thêm cơ chế này:
// Bên trong hàm calculateRadiance
if depth > MIN_DEPTH:
termination_prob = 0.5 // Ví dụ 50% khả năng dừng
if random() < termination_prob:
return Color(0, 0, 0)
// Nếu tiếp tục, lấy mẫu và chia cho xác suất sống sót (1 - P)
// ...
radiance += indirect_radiance * ... / (pdf * (1.0 - termination_prob))
Tối ưu hóa ánh sáng trực tiếp (Next Event Estimation)
Thay vì chỉ lấy mẫu ngẫu nhiên theo BRDF (thường gây nhiễu nếu nguồn sáng nhỏ), ta có thể kết hợp phương pháp lấy mẫu trực tiếp từ nguồn sáng. Ta bắn tia trực tiếp từ điểm giao đến các nguồn sáng (Light Rays) để tính toán đóng góp của ánh sáng trực tiếp (Direct Lighting), còn phần còn lại dùng lấy mẫu BRDF để tính ánh sáng gián tiếp (Indirect Lighting). Kỹ thuật này giúp giảm nhiễu đáng kể trong vùng bóng.