Hello.
Quick thing before we start. Since I am quite new to the "field" of ray tracing, especially material systems, be mindful that my theoretical foundation may or may not be structurally sound... so to speak :) In other words, do not assume that I know small details; rather, share them with me and we'll all learn together.
Furthermore, note that this question does not necessarily ask about specifically programming stuff. It is more theoretical in nature and the supplied code is only to act as complementary material for how the topic could be applied in practice. You will not need to run any code to help out in this case.
I am trying to implement the glTF 2.0 material system in my (monte carlo) path tracer.
A diagram of the system can be seen below. Note, I have not implemented the mix
box... yet!
I am stuck on a few details, and need assistance:
- In the specification, the vectors
L
andV
are used which typically refer to the vector from the intersection point towards a light source and the vector from the intersection point towards the camera respectively.
I do not have direct illumination implemented yet, thus there are no dedicated light sources in my scenes. The only "light source" is a background color which could be seen as "ambient lighting". Thus, I have chosen to interpret L
as wi
in my implementation where wi
is the sampled bounce ray. My motivation for this is that I am bouncing in that direction in order to find out what the light contribution from that angle is. Thus, in some way L
and wi
are the same thing(ish). I do not know if this is a correct thing to do.
Furthermore, since there are many rays bouncing around there isn't a "given viewing direction"; rather, I am choosing to interpret V
as wo
where wo
is the incoming ray direction. Again, I do not know if this is a correct thing to do.
- Figuring out how to sample a new bounce direction in
SpecularBRDF::sample
, as well as what the PDF is.
The specification uses a microfacet BRDF that uses some complicated functions. Evaluating the BRDF is easy enough; however, it is not trivial to figure out how to sample a new direction as well as what the PDF is. At least not for me.
- Figuring out how to sample a new bounce direction in
Dielectric::sample
, as well as what the PDF is.
Similar problem as with 2. above. A Dielectric
material has a SpecularBRDF
and a DiffuseBRDF
. I have no clue how I am supposed to take into account both of them when wanting to sample a new direction as well as defining the PDF.
- How to introduce the concept of "emissive materials" into this material system.
In my previous very rudimentary material system that simply features a Lambertian
material, I simply included a glm::dvec3 emission
variable and a double emission_strength
variable corresponding to the color of the emitted light and emission strength respectively. However, I don't really see how to introduce something like this in the glTF 2.0 material system.
Below follows my work in progress(!!!) implementation of the material system mentioned above.
#pragma once
#include <memory>
#include <glm/glm.hpp>
#include <glm/vec3.hpp>
#include "../Util.hpp"
struct Sample {
glm::dvec3 wi;
double pdf;
};
struct BxDF {
BxDF() = default;
virtual ~BxDF() = default;
[[nodiscard]] virtual glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& normal) const = 0;
[[nodiscard]] virtual Sample sample(const glm::dvec3& wo, const glm::dvec3& normal) const = 0;
};
struct SpecularBRDF : BxDF {
double alpha{0.25};
double alpha2{0.0625};
SpecularBRDF() = default;
SpecularBRDF(const double roughness)
: alpha(roughness * roughness), alpha2(alpha * alpha) {}
[[nodiscard]] glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& normal) const override {
const auto H = glm::normalize(wi + wo);
const auto brdf = glm::dvec3(V(wi, wo, normal) * D(normal, H));
return brdf;
}
[[nodiscard]] Sample sample(const glm::dvec3& wo, const glm::dvec3& normal) const override {
const Sample sample {
.wi = ..............................TODO
.pdf = ..............................TODO
};
return sample;
}
private:
double Chi(const double x) const {
if (std::fabs(x) <= 0.0) {
return 1.0;
}
return 0.0;
};
double G(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& N) const {
const glm::dvec3 H = glm::normalize(wi + wo);
const double NdotWI = glm::max(glm::dot(N, wi), 0.0001);
const double NdotWO = glm::max(glm::dot(N, wo), 0.0001);
const double HdotWI = glm::dot(H, wi);
const double HdotWO = glm::dot(H, wo);
const double numeratorL = 2.0 * NdotWI * Chi(HdotWI);
const double denomL = NdotWI + glm::sqrt(alpha2 + (1 - alpha2) * NdotWI * NdotWI);
const double quotientL = numeratorL / denomL;
const double numeratorR = 2.0 * NdotWO * Chi(HdotWO);
const double denomR = NdotWO + glm::sqrt(alpha2 + (1 - alpha2) * NdotWO * NdotWO);
const double quotientR = numeratorR / denomR;
return quotientL * quotientR;
}
double D(const glm::dvec3 N, const glm::dvec3 H) const {
const double NdotH = glm::max(glm::dot(N, H), 0.0001);
const double numerator = alpha2 * Chi(NdotH);
double temp = (NdotH * NdotH * (alpha2 - 1) + 1);
const double denom = Util::PI * temp * temp;
return numerator / denom;
}
double V(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& N) const {
const double numerator = G(wi, wo, N);
const double NdotWI = glm::max(glm::dot(N, wi), 0.0001);
const double NdotWO = glm::max(glm::dot(N, wo), 0.0001);
const double denom = 4.0 * NdotWI * NdotWO;
return numerator / denom;
}
};
struct DiffuseBRDF : BxDF {
glm::dvec3 baseColor{1.0f};
DiffuseBRDF() = default;
DiffuseBRDF(const glm::dvec3 baseColor) : baseColor(baseColor) {}
[[nodiscard]] glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& normal) const override {
const auto brdf = (1.0 / Util::PI) * baseColor;
return brdf;
}
[[nodiscard]] Sample sample(const glm::dvec3& wo, const glm::dvec3& normal) const override {
const Sample sample{
.wi = Util::CosineSampleHemisphere(normal),
.pdf = glm::max(glm::dot(sample.wi, normal), 0.0) / Util::PI,
};
return sample;
}
};
struct NewMaterial {
NewMaterial() = default;
virtual ~NewMaterial() = default;
[[nodiscard]] virtual glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& normal) const = 0;
[[nodiscard]] virtual Sample sample(const glm::dvec3& wo, const glm::dvec3& normal) const = 0;
};
struct Dielectric : NewMaterial {
std::shared_ptr<SpecularBRDF> specular{nullptr};
std::shared_ptr<DiffuseBRDF> diffuse{nullptr};
double ior{1.0};
Dielectric() = default;
Dielectric(const std::shared_ptr<SpecularBRDF>& specular, const std::shared_ptr<DiffuseBRDF>& diffuse, const double& ior)
: specular(specular), diffuse(diffuse), ior(ior) {}
[[nodiscard]] glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& N) const {
const double f0 = glm::pow(((1.0 - ior)) / (1.0 + ior), 2.0);
const glm::dvec3 H = glm::normalize(wi + wo);
const double WOdotH = glm::dot(wo, H);
const double fr = f0 + (1 - f0) * glm::pow(glm::abs(WOdotH), 5);
const glm::dvec3 base = diffuse->f(wi, wo, N);
const glm::dvec3 layer = specular->f(wi, wo, N);
return glm::mix(base, layer, fr);
}
[[nodiscard]] Sample sample(const glm::dvec3& wo, const glm::dvec3& N) const {
const Sample sample {
.wi = ..............................TODO
.pdf = ..............................TODO
};
return sample;
}
};
struct Metal : NewMaterial {
std::shared_ptr<SpecularBRDF> specular{nullptr};
glm::dvec3 f0{1.0};
Metal() = default;
Metal(const std::shared_ptr<SpecularBRDF>& specular, const glm::dvec3& f0)
: specular(specular), f0(f0) {}
[[nodiscard]] glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& N) const {
const glm::dvec3 H = glm::normalize(wi + wo);
const double WOdotH = glm::dot(wo, H);
return specular->f(wi, wo, N) * (f0 + (1.0 - f0) * glm::pow(1 - glm::abs(WOdotH), 5));
}
[[nodiscard]] Sample sample(const glm::dvec3& wo, const glm::dvec3& N) const {
return specular->sample(wo, N);
}
};
Perhaps the TraceRay(...)
code below may be of interest, I am not sure. I'll put it here in case we need it.
glm::dvec3 TraceRay(Ray& ray) {
glm::dvec3 Lo{0.0f};
glm::dvec3 throughput{1.0};
for (uint32_t i = 0; i <= MAX_BOUNCES; i++) {
HitPayload hp{};
if (!scene.intersect(ray, hp)) {
Lo += throughput * Environment(ray);
break;
}
const std::shared_ptr<NewMaterial> material = matsNew[hp.mat_idx];
// TODO: Direct illumination
// Add emitted radiance from intersection
// !!! Not supported with new glTF 2.0 material system... yet!
//Lo += throughput * material->emission;
const glm::dvec3 wo = glm::normalize(-ray.direction);
const Sample sample = material->sample(wo, hp.normal);
// PDF near zero => extremely unlikely path
if (sample.pdf < Util::EPSILON)
break;
const double cosine = glm::dot(sample.wi, hp.normal);
const glm::dvec3 brdf = material->f(sample.wi, wo, hp.normal);
throughput *= (brdf * cosine) / sample.pdf;
// throughput has decreased so much => not much point in continuing
if (throughput.x < Util::EPSILON && throughput.y < Util::EPSILON && throughput.z < Util::EPSILON)
break;
// Russian roulette
static uint32_t ITERS_BEFORE_RR = 5;
if (i >= ITERS_BEFORE_RR) {
const double p = glm::max(glm::max(throughput.x, throughput.y), throughput.z);
if (Util::RandomDouble() > p) {
break;
}
throughput *= (1.0f / p);
}
// Bounce ray
ray.origin = hp.position + Util::EPSILON * sample.wi;
ray.direction = sample.wi;
}
return Lo;
}