Như đã đề cập trong các ý kiến, tôi rất muốn đề xuất bắt đầu với Phân tán toàn phần. Đây là hai lần:
- Vì bạn đang thực hiện theo dõi đường dẫn, việc thêm thể tích không quá khó.
- Hiểu đầy đủ về cách hoạt động của tán xạ thể tích đầy đủ sẽ là cơ sở tuyệt vời để hiểu các ước tính. Ngoài ra, nó có thể cung cấp "tài liệu tham khảo" tuyệt vời để xem liệu ước tính của bạn có hoạt động tốt / chính xác hay không.
Với ý nghĩ đó, dưới đây là phần giới thiệu cơ bản về cách triển khai Phân tán thể tích toàn phần trong trình theo dõi đường đi ngược.
Để bắt đầu, chúng ta hãy xem mã để theo dõi đường đi ngược chỉ có phản xạ, không truyền / khúc xạ:
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);
}
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ỡ.
- Nếu đây là tia đầu tiên, hoặc chúng ta vừa bật ra khỏi bề mặt đầu cơ, hãy kiểm tra xem chúng ta có chạm đèn không. Nếu vậy, chúng tôi thêm sự phát xạ ánh sáng để tích lũy màu sắc của chúng tôi.
- Mẫu ánh sáng trực tiếp.
- 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ó.
- 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 nếu chúng ta nên chấm dứt tia.
- Đi 1.
Để biết thêm chi tiết về Lấy mẫu ánh sáng trực tiếp, xem câu trả lời này .
BSDF
Để thêm truyền, trước tiên chúng tôi cần nâng cấp một số tài liệu BRDF của chúng tôi lên BSDF đầy đủ, với chức năng BTDF.
Ví dụ: BSDF cho một chất điện môi đặc trưng lý tưởng (nghĩa là thủy tinh) là:
class IdealSpecularDielectric : public BSDF {
public:
IdealSpecularDielectric(float3 albedo, float ior)
: BSDF(BSDFLobe::Specular, albedo),
m_ior(ior) {
}
private:
float m_ior;
public:
float3 Eval(SurfaceInteraction &interaction) const override {
return m_albedo;
}
void Sample(SurfaceInteraction &interaction, UniformSampler *sampler) const override {
float VdotN = dot(interaction.OutputDirection, interaction.Normal);
float IORo = m_ior;
if (VdotN < 0.0f) {
IORo = 1.0f;
interaction.Normal = -interaction.Normal;
VdotN = -VdotN;
}
float eta = interaction.IORi / IORo;
float sinSquaredThetaT = SinSquaredThetaT(VdotN, eta);
float fresnel = Fresnel(interaction.IORi, IORo, VdotN, sinSquaredThetaT);
float rand = sampler->NextFloat();
if (rand <= fresnel) {
// Reflect
interaction.InputDirection = reflect(interaction.OutputDirection, interaction.Normal);
interaction.SampledLobe = BSDFLobe::SpecularReflection;
interaction.IORo = interaction.IORi;
} else {
// Refract
interaction.InputDirection = refract(interaction.OutputDirection, interaction.Normal, VdotN, eta, sinSquaredThetaT);
interaction.SampledLobe = BSDFLobe::SpecularTransmission;
interaction.IORo = IORo;
}
if (AnyNan(interaction.InputDirection)) {
printf("nan");
}
}
float Pdf(SurfaceInteraction &interaction) const override {
return 1.0f;
}
};
Phần thú vị của mã là Sample()
. Đây là nơi một tia sẽ chọn phản xạ hoặc khúc xạ. Làm thế nào chúng ta làm điều này thực sự là tùy thuộc vào chúng ta, như thể hiện trong câu trả lời này . Tuy nhiên, một lựa chọn rõ ràng sẽ là lấy mẫu dựa trên các phương trình Fresnel .
Trong trường hợp của một điện môi Specular lý tưởng, chỉ có hai hướng đầu ra tiềm năng: khúc xạ hoàn hảo hoặc phản xạ hoàn hảo. Vì vậy, chúng tôi đánh giá Fresnel và chọn ngẫu nhiên để khúc xạ / phản xạ với tỷ lệ bằng với Fresnel.
Phương tiện truyền thông
Tiếp theo, chúng ta cần nói về phương tiện truyền thông, và cách chúng ảnh hưởng đến ánh sáng dội xung quanh. Như lời giải thích của Nathan Reed trong câu trả lời này :
Cách tôi muốn nghĩ về tán xạ âm lượng là một photon truyền qua môi trường có xác suất nhất định trên mỗi đơn vị độ dài tương tác (bị tán xạ hoặc bị hấp thụ). Miễn là nó không tương tác, nó chỉ đi theo một đường thẳng không bị cản trở và không mất năng lượng. Khoảng cách càng lớn, xác suất mà nó tương tác ở đâu đó trong khoảng cách đó càng lớn. Xác suất tương tác trên mỗi đơn vị độ dài là hệ số mà bạn thấy trong các phương trình. Chúng tôi thường có các hệ số riêng biệt để phân tán và xác suất hấp thụ, vì vậyσσ= =σS+σmột
Xác suất này trên mỗi đơn vị độ dài chính xác là nguồn gốc của luật Bia-Lambert. Cắt một đoạn tia thành các khoảng vô hạn, coi mỗi khoảng là một nơi độc lập có thể tương tác, sau đó tích hợp dọc theo tia; bạn nhận được phân phối theo cấp số nhân (với tham số tỷ lệ σσ) cho xác suất tương tác là một hàm của khoảng cách.
Về mặt kỹ thuật, bạn có thể chọn khoảng cách giữa các sự kiện theo ý muốn, miễn là bạn cân chính xác đường dẫn cho xác suất để một photon có thể thực hiện giữa hai sự kiện liền kề mà không tương tác với phương tiện. Nói cách khác, mỗi phân đoạn đường dẫn trong phương tiện đóng góp hệ số trọng số của , trong đó là độ dài của phân khúc.e- σxx
Vì điều này, một lựa chọn tốt cho khoảng cách là lấy mẫu quan trọng từ phân bố mũ. Nói cách khác, bạn đặtx = - ( lnξ) / Σ
Tóm tắt:
- Một tia đi qua một phương tiện có một số xác suất để:
- Vì đây là Tích hợp Monte Carlo, chúng tôi có thể tự do chọn khoảng cách tương tác theo ý muốn. Tuy nhiên, một lựa chọn tốt là lấy mẫu quan trọng từ phân phối theo cấp số nhân tương tự như Bia-Lambert.
Tôi đã chọn thực hiện điều này với hệ số tán xạ và để Bia-Lambert chăm sóc hệ số hấp thụ.
class Medium {
public:
Medium(float3 absorptionColor, float absorptionAtDistance)
: m_absorptionCoefficient(-log(absorptionColor) / absorptionAtDistance) {
// This method for calculating the absorption coefficient is borrowed from Burley's 2015 Siggraph Course Notes "Extending the Disney BRDF to a BSDF with Integrated Subsurface Scattering"
// It's much more intutive to specify a color and a distance, then back-calculate the coefficient
}
virtual ~Medium() {
}
protected:
const float3a m_absorptionCoefficient;
public:
virtual float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const = 0;
virtual float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const = 0;
virtual float ScatterDirectionPdf(float3a &wi, float3a &wo) const = 0;
virtual float3 Transmission(float distance) const = 0;
};
Môi trường không tán xạ là môi trường mà một photon truyền thẳng qua, nhưng nó bị suy giảm dọc đường theo Bia-Lambert:
class NonScatteringMedium : public Medium {
public:
NonScatteringMedium(float3 color, float atDistance)
: Medium(color, atDistance) {
}
public:
float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const override {
*pdf = 1.0f;
return tFar;
}
float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const override {
return wo;
}
float ScatterDirectionPdf(float3a &wi, float3a &wo) const override {
return 1.0f;
}
float3 Transmission(float distance) const override {
return exp(-m_absorptionCoefficient * distance);
}
};
Môi trường tán xạ đẳng hướng cho phép tia có các sự kiện tán xạ (phản xạ) trong khi tia đi qua nó. Tia có thể được phản xạ theo bất kỳ hướng nào trong quả cầu xung quanh điểm tương tác (do đó là đẳng hướng).
Khoảng cách giữa các phản xạ là ngẫu nhiên, dựa trên xác suất "phân tán" theo cấp số nhân.
class IsotropicScatteringMedium : public Medium {
public:
IsotropicScatteringMedium(float3 absorptionColor, float absorptionAtDistance, float scatteringDistance)
: Medium(absorptionColor, absorptionAtDistance),
m_scatteringCoefficient(1 / scatteringDistance) {
}
private:
float m_scatteringCoefficient;
public:
float SampleDistance(UniformSampler *sampler, float tFar, float *weight, float *pdf) const override {
float distance = -logf(sampler->NextFloat()) / m_scatteringCoefficient;
// If we sample a distance farther than the next intersecting surface, clamp to the surface distance
if (distance >= tFar) {
*pdf = 1.0f;
return tFar;
}
*pdf = std::exp(-m_scatteringCoefficient * distance);
return distance;
}
float3a SampleScatterDirection(UniformSampler *sampler, float3a &wo, float *pdf) const override {
*pdf = 0.25f * M_1_PI; // 1 / (4 * PI)
return UniformSampleSphere(sampler);
}
float ScatterDirectionPdf(float3a &wi, float3a &wo) const override {
return 0.25f * M_1_PI; // 1 / (4 * PI)
}
float3 Transmission(float distance) const override {
return exp(-m_absorptionCoefficient * distance);
}
};
Ở khoảng cách tán xạ cao, vật liệu này hoạt động gần giống như NonScatteringMedium, bởi vì nó có xác suất tán xạ rất nhỏ trước khi truyền qua môi trường.
Ở khoảng cách tán xạ thấp, vật liệu này hoạt động như sáp hoặc ngọc bích.
Khi khoảng cách tán xạ càng ngày càng thấp, chúng ta có cơ hội cao hơn và cao hơn để tán xạ khi đi qua phương tiện. Điều này trình bày một vài vấn đề:
- Số lượng nảy trên mỗi tia sẽ phát nổ.
- Nếu Russian Roulette giết chết tia sáng hoặc nếu bạn đi qua "maxBounces", bạn sẽ "mất" mọi đóng góp màu sắc từ bên trong phương tiện.
Do đó, bạn cần đặt maxBounces lên một số cao và dựa vào Roulette Nga để chấm dứt các tia trong trường hợp chung. Ngoài ra, IsotropicScatteringMedium cực kỳ kém hiệu quả khi đại diện cho các vật liệu chủ yếu mờ đục. Để có được hiệu suất tốt hơn, chúng ta nên sử dụng hàm pha không đẳng hướng, như Henyey-Greenstein hoặc Schlick . Những sai lệch sự tán xạ theo một hướng nhất định. Vì vậy, chúng ta có thể đặt chúng để có độ tán xạ ngược cao, do đó làm giảm bớt vấn đề tia "bị mất".
Để tất cả chúng cùng nhau
Vì vậy, với thông tin mới này, làm thế nào để chúng tôi sửa đổi nhà tích hợp để hiểu BSDF và phương tiện truyền thông?
Trước tiên, chúng ta cần bắt đầu theo dõi phương tiện chúng ta hiện đang ở và Chỉ số khúc xạ (IOR) của nó.
SurfaceInteraction interaction;
interaction.IORi = 1.0f; // Vacuum
Medium *medium = nullptr; // nullptr == vacuum
Sau đó, chúng tôi chia bộ tích hợp thành hai phần: truyền và tương tác vật liệu.
void RenderPixel(uint x, uint y, UniformSampler *sampler) const {
Ray ray = m_scene->Camera->CalculateRayFromPixel(x, y, sampler);
float3 color(0.0f);
float3 throughput(1.0f);
SurfaceInteraction interaction;
interaction.IORi = 1.0f; // Air
Medium *medium = nullptr;
bool hitSurface = false;
// Bounce the ray around the scene
uint bounces = 0;
const uint maxBounces = 1500;
for (; 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;
}
// We hit an object
hitSurface = true;
// Calculate any transmission
if (medium != nullptr) {
float weight = 1.0f;
float pdf = 1.0f;
float distance = medium->SampleDistance(sampler, ray.TFar, &weight, &pdf);
float3 transmission = medium->Transmission(distance);
throughput = throughput * weight * transmission;
if (distance < ray.TFar) {
// Create a scatter event
hitSurface = false;
ray.Origin = ray.Origin + ray.Direction * distance;
// Reset the other ray properties
float directionPdf;
float3a wo = normalize(ray.Direction);
ray.Direction = medium->SampleScatterDirection(sampler, wo, &directionPdf);
ray.TNear = 0.001f;
ray.TFar = infinity;
ray.GeomID = INVALID_GEOMETRY_ID;
ray.PrimID = INVALID_PRIMATIVE_ID;
ray.InstID = INVALID_INSTANCE_ID;
ray.Mask = 0xFFFFFFFF;
ray.Time = 0.0f;
}
}
if (hitSurface) {
// 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);
interaction.IORo = 0.0f;
// Calculate the direct lighting
color += throughput * SampleOneLight(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;
// Update the current IOR and medium if we refracted
if (interaction.SampledLobe == BSDFLobe::SpecularTransmission) {
interaction.IORi = interaction.IORo;
medium = material->medium;
}
// Shoot a new ray
// Set the origin at the intersection point
ray.Origin = interaction.Position;
// Reset the other ray properties
ray.Direction = interaction.InputDirection;
if (AnyNan(ray.Direction))
printf("bad");
ray.TNear = 0.001f;
ray.TFar = infinity;
ray.GeomID = INVALID_GEOMETRY_ID;
ray.PrimID = INVALID_PRIMATIVE_ID;
ray.InstID = INVALID_INSTANCE_ID;
ray.Mask = 0xFFFFFFFF;
ray.Time = 0.0f;
}
// 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;
}
}
if (bounces == maxBounces) {
printf("Over max bounces");
}
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 hiện đang ở trong một phương tiện (không phải trong chân không).
- Lấy mẫu trung bình cho một khoảng cách.
- Đánh giá truyền tải của phương tiện và tích lũy thông lượng.
- Nếu khoảng cách tán xạ nhỏ hơn khoảng cách từ gốc tia tới bề mặt tiếp theo, hãy tạo sự kiện tán xạ.
- Lấy mẫu môi trường cho hướng phân tán.
- Kiểm tra xem chúng tôi có chạm vào bề mặt không (nghĩa là chúng tôi không có sự kiện phân tán).
- Nếu đây là tia đầu tiên, hoặc chúng ta vừa bật ra khỏi bề mặt đầu cơ, hãy kiểm tra xem chúng ta có chạm đèn không. Nếu vậy, chúng tôi thêm sự phát xạ ánh sáng để tích lũy màu sắc của chúng tôi.
- Mẫu ánh sáng trực tiếp.
- 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ó.
- Nếu chúng tôi khúc xạ vào vật liệu, hãy cập nhật IOR và phương tiện cho lần nảy tiếp theo.
- 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 nếu chúng ta nên chấm dứt tia.
- Đi 1.