Câu trả lời ngắn:
Lấy mẫu quan trọng là một phương pháp để giảm phương sai trong Tích hợp Monte Carlo bằng cách chọn một công cụ ước tính gần với hình dạng của hàm thực tế.
PDF là tên viết tắt của Hàm xác suất mật độ . Một p df( x ) đưa ra xác suất của một mẫu ngẫu nhiên được tạo là x .
Câu trả lời dài:
Để bắt đầu, hãy xem lại Tích hợp Monte Carlo là gì và nó trông như thế nào về mặt toán học.
Tích hợp Monte Carlo là một kỹ thuật để ước tính giá trị của tích phân. Nó thường được sử dụng khi không có giải pháp dạng đóng cho tích phân. Nó trông như thế này:
∫f( x )dx ≈ 1NΣi = 1Nf( xtôi)p df( xtôi)
Trong tiếng Anh, điều này nói rằng bạn có thể tính gần đúng một tích phân bằng cách lấy trung bình các mẫu ngẫu nhiên liên tiếp từ hàm. Khi N trở nên lớn, phép tính gần đúng càng ngày càng gần với giải pháp. p df( xtôi) đại diện cho hàm mật độ xác suất của từng mẫu ngẫu nhiên.
Chúng ta hãy làm một ví dụ: Tính giá trị của tích phân tôi .
tôi= = ∫2π0e-xtội( x ) dx
Hãy sử dụng tích hợp Monte Carlo:
tôi≈ 1NΣi = 1Ne- xtội( xtôi)p df( xtôi)
Một chương trình python đơn giản để tính toán điều này là:
import random
import math
N = 200000
TwoPi = 2.0 * math.pi
sum = 0.0
for i in range(N):
x = random.uniform(0, TwoPi)
fx = math.exp(-x) * math.sin(x)
pdf = 1 / (TwoPi - 0.0)
sum += fx / pdf
I = (1 / N) * sum
print(I)
Nếu chúng tôi chạy chương trình, chúng tôi nhận được tôi= 0,4986941
Sử dụng phân tách theo các phần, chúng ta có thể có được giải pháp chính xác:
tôi= 12( 1 - e - 2 π ) = 0,4990663
Bạn sẽ nhận thấy rằng Giải pháp Monte Carlo không hoàn toàn chính xác. Điều này là bởi vì nó là một ước tính. Điều đó nói rằng, khi N đi đến vô cùng, ước tính sẽ ngày càng gần hơn với câu trả lời đúng. Đã ở N= 2000 một số lần chạy gần như giống hệt với câu trả lời đúng.
Lưu ý về PDF: Trong ví dụ đơn giản này, chúng tôi luôn lấy một mẫu ngẫu nhiên thống nhất. Một mẫu ngẫu nhiên thống nhất có nghĩa là mọi mẫu đều có cùng xác suất được chọn. Chúng tôi lấy mẫu trong phạm vi [ 0 , 2 π] vì vậy, p df( x ) = 1 / ( 2 π- 0 )
Lấy mẫu quan trọng hoạt động bằng cách không lấy mẫu thống nhất. Thay vào đó, chúng tôi cố gắng chọn nhiều mẫu đóng góp nhiều vào kết quả (quan trọng) và ít mẫu hơn chỉ đóng góp một ít vào kết quả (ít quan trọng hơn). Do đó tên, lấy mẫu quan trọng.
ff
Một ví dụ về lấy mẫu quan trọng trong Truy tìm đường dẫn là cách chọn hướng của tia sau khi chiếu vào bề mặt. Nếu bề mặt không hoàn toàn đặc biệt (ví dụ như gương hoặc kính), thì tia sáng đi ra có thể ở bất cứ đâu trong bán cầu.
Chúng ta có thể lấy mẫu đồng nhất bán cầu để tạo ra tia mới. Tuy nhiên, chúng ta có thể khai thác thực tế là phương trình kết xuất có yếu tố cosin trong đó:
Lo( P , ωo) = Le( P , ωo) + ∫Ωf( P , ωtôi, ωo) Ltôi( P , ωtôi) | cosθtôi| dωtôi
cos( x )
Để chống lại điều này, chúng tôi sử dụng lấy mẫu quan trọng. Nếu chúng ta tạo ra các tia theo một bán cầu có trọng số cosin, chúng tôi đảm bảo rằng nhiều tia được tạo ra tốt hơn phía trên đường chân trời và ít gần đường chân trời hơn. Điều này sẽ làm giảm phương sai và giảm tiếng ồn.
Trong trường hợp của bạn, bạn đã chỉ định rằng bạn sẽ sử dụng BRDF dựa trên Cook-Torrance, microfacet. Hình thức phổ biến là:
f( P , ωtôi, ωo) = F( ωtôi, h ) G ( ωtôi, ωo, h ) D ( h )4 cos( θtôi) cos( θo)
Ở đâu
F( ωtôi, h ) = Hàm FresnelG ( ωtôi, ωo, h ) = Chức năng Tạo hình và Tạo bóngD ( h ) = Hàm phân phối chuẩn
Blog "Ghi chú của một đồ họa" có một bài viết tuyệt vời về cách lấy mẫu BRDF của Cook-Torrance. Tôi sẽ giới thiệu bạn đến bài viết trên blog của anh ấy . Điều đó nói rằng, tôi sẽ cố gắng tạo ra một cái nhìn tổng quan ngắn gọn dưới đây:
NDF nói chung là phần chiếm ưu thế của BRDF Cook-Torrance, vì vậy nếu chúng ta sẽ lấy mẫu quan trọng, chúng ta nên lấy mẫu dựa trên NDF.
Cook-Torrance không chỉ định một NDF cụ thể để sử dụng; chúng tôi có thể tự do lựa chọn bất cứ ai phù hợp với ưa thích của chúng tôi. Điều đó nói rằng, có một vài NDF phổ biến:
Mỗi NDF có công thức riêng, do đó mỗi công thức phải được lấy mẫu khác nhau. Tôi sẽ chỉ hiển thị chức năng lấy mẫu cuối cùng cho mỗi. Nếu bạn muốn xem công thức có nguồn gốc như thế nào, hãy xem bài đăng trên blog.
GGX được định nghĩa là:
DG G X( m ) = α2π( ( α2- 1 ) cos2( θ ) + 1 )2
θ
θ = ArccOS( α2ξ1( α2- 1 ) + 1------------√)
ξ
φ
ϕ = ξ2
Beckmann được định nghĩa là:
DB e c k m a n n( m ) = 1πα2cos4( θ )e- tan2( θ )α2
Mà có thể được lấy mẫu với:
θ = ArccOS( 11 = α2ln( 1 - ξ1)--------------√)ϕ = ξ2
Cuối cùng, Blinn được định nghĩa là:
DB l i n n( m ) = α + 22 π( cos( θ ) )α
Mà có thể được lấy mẫu với:
θ = ArccOS( 1ξα + 11)ϕ = ξ2
Đưa nó vào thực tế
Chúng ta hãy xem xét một đường dẫn ngược cơ bản:
void RenderPixel(uint x, uint y, UniformSampler *sampler) {
Ray ray = m_scene->Camera.CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
// Bounce the ray around the scene
for (uint bounces = 0; bounces < 10; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.geomID == RTC_INVALID_GEOMETRY_ID) {
color += throughput * float3(0.846f, 0.933f, 0.949f);
break;
}
// We hit an object
// Fetch the material
Material *material = m_scene->GetMaterial(ray.geomID);
// The object might be emissive. If so, it will have a corresponding light
// Otherwise, GetLight will return nullptr
Light *light = m_scene->GetLight(ray.geomID);
// If we hit a light, add the emmisive light
if (light != nullptr) {
color += throughput * light->Le();
}
float3 normal = normalize(ray.Ng);
float3 wo = normalize(-ray.dir);
float3 surfacePos = ray.org + ray.dir * ray.tfar;
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.org = surfacePos;
// Reset the other ray properties
ray.dir = wi;
ray.tnear = 0.001f;
ray.tfar = embree::inf;
ray.geomID = RTC_INVALID_GEOMETRY_ID;
ray.primID = RTC_INVALID_GEOMETRY_ID;
ray.instID = RTC_INVALID_GEOMETRY_ID;
ray.mask = 0xFFFFFFFF;
ray.time = 0.0f;
}
m_scene->Camera.FrameBuffer.SplatPixel(x, y, color);
}
I E. chúng tôi tung lên xung quanh khung cảnh, tích lũy màu sắc và suy giảm ánh sáng khi chúng tôi đi. Ở mỗi lần nảy, chúng ta phải chọn một hướng mới cho tia. Như đã đề cập ở trên, chúng ta có thể lấy mẫu đồng nhất bán cầu để tạo ra tia mới. Tuy nhiên, mã thông minh hơn; tầm quan trọng của mẫu là hướng đi mới dựa trên BRDF. (Lưu ý: Đây là hướng đầu vào, bởi vì chúng tôi là người theo dõi đường dẫn ngược)
// Get the new ray direction
// Choose the direction based on the material
float3 wi = material->Sample(wo, normal, sampler);
float pdf = material->Pdf(wi, normal);
Mà có thể được thực hiện như:
void LambertBRDF::Sample(float3 outputDirection, float3 normal, UniformSampler *sampler) {
float rand = sampler->NextFloat();
float r = std::sqrtf(rand);
float theta = sampler->NextFloat() * 2.0f * M_PI;
float x = r * std::cosf(theta);
float y = r * std::sinf(theta);
// Project z up to the unit hemisphere
float z = std::sqrtf(1.0f - x * x - y * y);
return normalize(TransformToWorld(x, y, z, normal));
}
float3a TransformToWorld(float x, float y, float z, float3a &normal) {
// Find an axis that is not parallel to normal
float3a majorAxis;
if (abs(normal.x) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(1, 0, 0);
} else if (abs(normal.y) < 0.57735026919f /* 1 / sqrt(3) */) {
majorAxis = float3a(0, 1, 0);
} else {
majorAxis = float3a(0, 0, 1);
}
// Use majorAxis to create a coordinate system relative to world space
float3a u = normalize(cross(normal, majorAxis));
float3a v = cross(normal, u);
float3a w = normal;
// Transform from local coordinates to world coordinates
return u * x +
v * y +
w * z;
}
float LambertBRDF::Pdf(float3 inputDirection, float3 normal) {
return dot(inputDirection, normal) * M_1_PI;
}
Sau khi chúng tôi lấy mẫu inputDirection ('wi' trong mã), chúng tôi sử dụng điều đó để tính giá trị của BRDF. Và sau đó chúng tôi chia cho pdf theo công thức Monte Carlo:
// Accumulate the brdf attenuation
throughput = throughput * material->Eval(wi, wo, normal) / pdf;
Trong đó Eval () chỉ là hàm BRDF (Lambert, Blinn-Phong, Cook-Torrance, v.v.):
float3 LambertBRDF::Eval(float3 inputDirection, float3 outputDirection, float3 normal) const override {
return m_albedo * M_1_PI * dot(inputDirection, normal);
}