Có nhiều khu vực trong theo dõi đường dẫn có thể được lấy mẫu quan trọng. Ngoài ra, mỗi khu vực đó cũng có thể sử dụng Lấy mẫu nhiều lần quan trọng, lần đầu tiên được đề xuất trong bài báo năm 1995 của Veach và Guibas . Để giải thích rõ hơn, chúng ta hãy nhìn vào một đường dẫn ngược:
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);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// 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 emission
if (light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
Bằng tiếng Anh:
- Bắn một tia qua cảnh
- Kiểm tra nếu chúng tôi nhấn bất cứ điều gì. Nếu không chúng tôi trả lại màu skybox và phá vỡ.
- Kiểm tra nếu chúng ta nhấn một ánh sáng. Nếu vậy, chúng ta thêm sự phát xạ ánh sáng vào sự tích lũy màu của chúng ta
- Chọn một hướng mới cho tia tiếp theo. Chúng tôi có thể làm điều này thống nhất, hoặc mẫu quan trọng dựa trên BRDF
- Đánh giá BRDF và tích lũy nó. Ở đây chúng ta phải chia theo pdf theo hướng đã chọn, để tuân theo Thuật toán Monte Carlo.
- Tạo một tia mới dựa trên hướng đã chọn của chúng tôi và nơi chúng tôi vừa đến
- [Tùy chọn] Sử dụng Roulette Nga để chọn xem chúng tôi có nên chấm dứt tia không
- Đi 1
Với mã này, chúng ta chỉ nhận được màu nếu tia cuối cùng phát sáng. Ngoài ra, nó không hỗ trợ các nguồn sáng đúng giờ, vì chúng không có khu vực.
Để khắc phục điều này, chúng tôi lấy mẫu đèn trực tiếp tại mỗi lần nảy. Chúng tôi phải thực hiện một vài thay đổi nhỏ:
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);
SurfaceInteraction interaction;
// Bounce the ray around the scene
const uint maxBounces = 15;
for (uint bounces = 0; bounces < maxBounces; ++bounces) {
m_scene->Intersect(ray);
// The ray missed. Return the background color
if (ray.GeomID == INVALID_GEOMETRY_ID) {
color += throughput * m_scene->BackgroundColor;
break;
}
// 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 this is the first bounce or if we just had a specular bounce,
// we need to add the emmisive light
if ((bounces == 0 || (interaction.SampledLobe & BSDFLobe::Specular) != 0) && light != nullptr) {
color += throughput * light->Le();
}
interaction.Position = ray.Origin + ray.Direction * ray.TFar;
interaction.Normal = normalize(m_scene->InterpolateNormal(ray.GeomID, ray.PrimID, ray.U, ray.V));
interaction.OutputDirection = normalize(-ray.Direction);
// Calculate the direct lighting
color += throughput * SampleLights(sampler, interaction, material->bsdf, light);
// Get the new ray direction
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float pdf = material->bsdf->Pdf(interaction);
// Accumulate the weight
throughput = throughput * material->bsdf->Eval(interaction) / pdf;
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
ray.TNear = 0.001f;
ray.TFar = infinity;
// Russian Roulette
if (bounces > 3) {
float p = std::max(throughput.x, std::max(throughput.y, throughput.z));
if (sampler->NextFloat() > p) {
break;
}
throughput *= 1 / p;
}
}
m_scene->Camera->FrameBufferData.SplatPixel(x, y, color);
}
Đầu tiên, chúng tôi thêm "color + = thông lượng * SampleLights (...)". Tôi sẽ đi vào chi tiết về SampleLights () một chút. Nhưng, về cơ bản, nó lặp qua tất cả các đèn và trả lại sự đóng góp của chúng cho màu sắc, bị suy giảm bởi BSDF.
Điều này thật tuyệt, nhưng chúng ta cần thực hiện thêm một thay đổi để làm cho nó chính xác; cụ thể, những gì xảy ra khi chúng ta nhấn một ánh sáng. Trong mã cũ, chúng tôi đã thêm sự phát xạ của ánh sáng vào sự tích lũy màu. Nhưng bây giờ chúng tôi trực tiếp lấy mẫu ánh sáng mỗi lần nảy, vì vậy nếu chúng tôi thêm phát xạ của ánh sáng, chúng tôi sẽ "nhúng đôi". Do đó, điều chính xác cần làm là ... không có gì; chúng ta bỏ qua việc tích lũy phát xạ của ánh sáng.
Tuy nhiên, có hai trường hợp góc:
- Tia đầu tiên
- Bounce hoàn toàn đặc biệt (còn gọi là gương)
Nếu tia đầu tiên chiếu vào ánh sáng, bạn sẽ thấy sự phát xạ của ánh sáng trực tiếp. Vì vậy, nếu chúng ta bỏ qua nó, tất cả các đèn sẽ hiển thị màu đen, mặc dù các bề mặt xung quanh chúng được thắp sáng.
Khi bạn chạm vào một bề mặt đặc biệt hoàn hảo, bạn không thể lấy mẫu trực tiếp ánh sáng, vì một tia đầu vào chỉ có một đầu ra. Chà, về mặt kỹ thuật, chúng ta có thể kiểm tra xem tia đầu vào có bị sáng hay không, nhưng không có điểm nào; vòng lặp Path Trace chính sẽ làm điều đó. Do đó, nếu chúng ta đánh một ánh sáng ngay sau khi chúng ta chạm vào một bề mặt cụ thể, chúng ta cần tích lũy màu sắc. Nếu chúng ta không, đèn sẽ có màu đen trong gương.
Bây giờ, hãy đi sâu vào SampleLights ():
float3 SampleLights(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
float3 L(0.0f);
for (uint i = 0; i < numLights; ++i) {
Light *light = &m_scene->Lights[i];
// Don't let a light contribute light to itself
if (light == hitLight) {
continue;
}
L = L + EstimateDirect(light, sampler, interaction, bsdf);
}
return L;
}
Bằng tiếng Anh:
- Vòng qua tất cả các đèn
- Bỏ qua ánh sáng nếu chúng ta nhấn nó
- Tích lũy ánh sáng trực tiếp từ tất cả các đèn
- Trả lại ánh sáng trực tiếp
B SD F( P , ωTôi, ωo) LTôi( P , ωTôi)
Đối với các nguồn sáng đúng giờ, điều này đơn giản như:
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
return float3(0.0f);
}
interaction.InputDirection = normalize(light->Origin - interaction.Position);
return bsdf->Eval(interaction) * light->Li;
}
Tuy nhiên, nếu chúng ta muốn đèn có diện tích, trước tiên chúng ta cần lấy mẫu một điểm trên ánh sáng. Do đó, định nghĩa đầy đủ là:
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float pdf;
float3 Li = light->SampleLi(sampler, m_scene, interaction, &pdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (pdf != 0.0f && !all(Li)) {
directLighting += bsdf->Eval(interaction) * Li / pdf;
}
}
return directLighting;
}
Chúng tôi có thể thực hiện ánh sáng-> SampleLi theo cách chúng tôi muốn; chúng ta có thể chọn điểm đồng nhất, hoặc mẫu quan trọng. Trong cả hai trường hợp, chúng tôi chia độ phóng xạ cho pdf khi chọn điểm. Một lần nữa, để đáp ứng các yêu cầu của Monte Carlo.
Nếu BRDF phụ thuộc nhiều vào chế độ xem, có thể tốt hơn là chọn một điểm dựa trên BRDF, thay vì một điểm ngẫu nhiên trên ánh sáng. Nhưng làm thế nào để chúng ta chọn? Mẫu dựa trên ánh sáng, hoặc dựa trên BRDF?
B SD F( P , ωTôi, ωo) LTôi( P , ωTôi)
float3 EstimateDirect(Light *light, UniformSampler *sampler, SurfaceInteraction &interaction, BSDF *bsdf) const {
float3 directLighting = float3(0.0f);
float3 f;
float lightPdf, scatteringPdf;
// Sample lighting with multiple importance sampling
// Only sample if the BRDF is non-specular
if ((bsdf->SupportedLobes & ~BSDFLobe::Specular) != 0) {
float3 Li = light->SampleLi(sampler, m_scene, interaction, &lightPdf);
// Make sure the pdf isn't zero and the radiance isn't black
if (lightPdf != 0.0f && !all(Li)) {
// Calculate the brdf value
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
float weight = PowerHeuristic(1, lightPdf, 1, scatteringPdf);
directLighting += f * Li * weight / lightPdf;
}
}
}
// Sample brdf with multiple importance sampling
bsdf->Sample(interaction, sampler);
f = bsdf->Eval(interaction);
scatteringPdf = bsdf->Pdf(interaction);
if (scatteringPdf != 0.0f && !all(f)) {
lightPdf = light->PdfLi(m_scene, interaction);
if (lightPdf == 0.0f) {
// We didn't hit anything, so ignore the brdf sample
return directLighting;
}
float weight = PowerHeuristic(1, scatteringPdf, 1, lightPdf);
float3 Li = light->Le();
directLighting += f * Li * weight / scatteringPdf;
}
return directLighting;
}
Bằng tiếng Anh:
- Đầu tiên, chúng tôi lấy mẫu ánh sáng
- Điều này cập nhật tương tác.InputDirection
- Cung cấp cho chúng tôi Li cho ánh sáng
- Và pdf của việc chọn điểm đó trên ánh sáng
- Kiểm tra xem pdf có hợp lệ không và độ sáng là khác không
- Đánh giá BSDF bằng cách sử dụng InputDirection được lấy mẫu
- Tính toán pdf cho BSDF được đưa vào InputDirection được lấy mẫu
- Về cơ bản, mẫu này có khả năng như thế nào, nếu chúng ta lấy mẫu bằng BSDF, thay vì ánh sáng
- Tính trọng lượng, sử dụng pdf ánh sáng và pdf BSDF
- Veach và Guibas định nghĩa một vài cách khác nhau để tính trọng lượng. Thực nghiệm, họ đã tìm thấy sức mạnh heuristic với sức mạnh là 2 để hoạt động tốt nhất trong hầu hết các trường hợp. Tôi giới thiệu bạn đến bài báo để biết thêm chi tiết. Việc thực hiện dưới đây
- Nhân trọng lượng với tính toán ánh sáng trực tiếp và chia cho ánh sáng pdf. (Đối với Monte Carlo) Và thêm vào sự tích lũy ánh sáng trực tiếp.
- Sau đó, chúng tôi lấy mẫu BRDF
- Điều này cập nhật tương tác.InputDirection
- Đánh giá BRDF
- Nhận pdf để chọn hướng này dựa trên BRDF
- Tính pdf ánh sáng, được đưa vào InputDirection được lấy mẫu
- Đây là tấm gương của trước đây. Hướng này có khả năng như thế nào, nếu chúng ta lấy mẫu ánh sáng
- Nếu lightPdf == 0,0f, thì tia bị mất ánh sáng, vì vậy chỉ cần trả lại ánh sáng trực tiếp từ mẫu ánh sáng.
- Mặt khác, tính toán trọng lượng và thêm ánh sáng trực tiếp BSDF vào tích lũy
- Cuối cùng, trả lại ánh sáng trực tiếp tích lũy
.
inline float PowerHeuristic(uint numf, float fPdf, uint numg, float gPdf) {
float f = numf * fPdf;
float g = numg * gPdf;
return (f * f) / (f * f + g * g);
}
Có một số tối ưu hóa / cải tiến bạn có thể thực hiện trong các chức năng này, nhưng tôi đã phân tích chúng để cố gắng làm cho chúng dễ hiểu hơn. Nếu bạn muốn, tôi có thể chia sẻ một số cải tiến này.
Chỉ lấy mẫu một ánh sáng
Trong SampleLights () chúng tôi lặp qua tất cả các đèn và nhận được sự đóng góp của chúng. Đối với một số lượng nhỏ đèn, điều này là tốt, nhưng đối với hàng trăm hoặc hàng ngàn đèn, điều này trở nên đắt đỏ. May mắn thay, chúng ta có thể khai thác thực tế rằng Monte Carlo Integration là một mức trung bình khổng lồ. Thí dụ:
Hãy xác định
h ( x ) = f( x ) + g( x )
h ( x )
h ( x ) = 1NΣi = 1Nf( xTôi) + g( xTôi)
f( x )g( x )
h ( x ) = 1NΣi = 1Nr ( ζ, x )p df
ζr ( ζ, x )
r ( ζ, x ) = { f( x ) ,g( x ) ,0.0 ≤ ζ< 0,50,5 ≤ ζ< 1.0
p df= 12
Bằng tiếng Anh:
- f( x )g( x )
- 12
- Trung bình cộng
Khi N trở nên lớn, ước tính sẽ hội tụ đến giải pháp chính xác.
Chúng ta có thể áp dụng nguyên tắc tương tự này để lấy mẫu ánh sáng. Thay vì lấy mẫu mỗi ánh sáng, chúng tôi chọn ngẫu nhiên một và nhân kết quả với số lượng đèn (Điều này giống như chia cho pdf phân số):
float3 SampleOneLight(UniformSampler *sampler, SurfaceInteraction interaction, BSDF *bsdf, Light *hitLight) const {
std::size_t numLights = m_scene->NumLights();
// Return black if there are no lights
// And don't let a light contribute light to itself
// Aka, if we hit a light
// This is the special case where there is only 1 light
if (numLights == 0 || numLights == 1 && hitLight != nullptr) {
return float3(0.0f);
}
// Don't let a light contribute light to itself
// Choose another one
Light *light;
do {
light = m_scene->RandomOneLight(sampler);
} while (light == hitLight);
return numLights * EstimateDirect(light, sampler, interaction, bsdf);
}
1chữ số
Nhiều tầm quan trọng Lấy mẫu theo hướng "Tia mới"
Mã hiện tại chỉ quan trọng lấy mẫu theo hướng "Tia mới" dựa trên BSDF. Điều gì sẽ xảy ra nếu chúng ta muốn lấy mẫu quan trọng dựa trên vị trí của đèn?
Lấy từ những gì chúng ta đã học ở trên, một phương pháp sẽ là bắn hai tia "mới" và trọng lượng mỗi tia dựa trên pdf của chúng. Tuy nhiên, điều này vừa tốn kém về mặt tính toán, vừa khó thực hiện mà không cần đệ quy.
Để khắc phục điều này, chúng ta có thể áp dụng các nguyên tắc tương tự mà chúng ta đã học bằng cách chỉ lấy một ánh sáng. Đó là, chọn ngẫu nhiên một mẫu để lấy mẫu và chia cho pdf chọn nó.
// Get the new ray direction
// Randomly (uniform) choose whether to sample based on the BSDF or the Lights
float p = sampler->NextFloat();
Light *light = m_scene->RandomLight();
if (p < 0.5f) {
// Choose the direction based on the bsdf
material->bsdf->Sample(interaction, sampler);
float bsdfPdf = material->bsdf->Pdf(interaction);
float lightPdf = light->PdfLi(m_scene, interaction);
float weight = PowerHeuristic(1, bsdfPdf, 1, lightPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / bsdfPdf;
} else {
// Choose the direction based on a light
float lightPdf;
light->SampleLi(sampler, m_scene, interaction, &lightPdf);
float bsdfPdf = material->bsdf->Pdf(interaction);
float weight = PowerHeuristic(1, lightPdf, 1, bsdfPdf);
// Accumulate the throughput
throughput = throughput * weight * material->bsdf->Eval(interaction) / lightPdf;
}
Như đã nói, chúng ta có thực sự muốn coi trọng hướng "Tia mới" dựa trên ánh sáng không? Đối với ánh sáng trực tiếp , độ phóng xạ bị ảnh hưởng bởi cả BSDF của bề mặt và hướng của ánh sáng. Nhưng đối với ánh sáng gián tiếp , độ phóng xạ hầu như chỉ được xác định bởi BSDF của bề mặt trước đó. Vì vậy, việc thêm mẫu quan trọng nhẹ không cung cấp cho chúng tôi bất cứ điều gì.
Do đó, thông thường chỉ lấy mẫu quan trọng "Hướng mới" với BSDF, nhưng áp dụng Lấy mẫu nhiều lần quan trọng cho chiếu sáng trực tiếp.