#[SOLVED] Monte-Carlo Path-Tracer output too bright/saturated
145 messages · Page 1 of 1 (latest)
Sure. All I expect is to see the gradient transitioning from a sky color to white as defined in Environment(...).
inspect the RGB values to see if they are what you expect
It's very hard to debug stuff if you don't test assumptions in your code
They look fine to me. They are within [0,255] as expected and growing towards 255 as we move downwards in the picture, which makes sense as we're transitioning to pure white.
check the values are exactly what you'd expect not that they are brighter then expected
They are what I expect. like I said.
If I have values that are too bright within the range of 0-1 say because I did .sqrt() or something then you'll still get 0 to 255 transition
manually inspect pixel values (not at the ends) and do the calculation manually
I am not sure what you are trying to get at. The way I am handling colors is identical to that of the handling in the book. The book uses a linear color space during the path-tracing. When eventually saving the image, gamma-2 correction is done followed by a clamping to [0,1], and lastly a transformation to [0, 255].
The above image void of any objects essentially boils down to evaluating the below for each pixel.
static const glm::dvec3 SKY_COLOR{ 0.5, 0.7, 1.0 }; // same color as in the book
const double alpha = 0.5 * (glm::normalize(ray.direction).y + 1.0); // same alpha calculation
const glm::dvec3 skyColor = alpha * SKY_COLOR + (1.0 - alpha) * glm::dvec3{1.0}; // same linear-interpolation
return skyColor;
Nothing interesting is going on here and looking at color values at different stages of the program seemingly yield exactly what you'd expect.
So have you checked that if you have a pixel value at uv=(some value that isn't at a boundary) it is the correct value by opening it in GIMP or something?
I have printed out some color-related stuff at various points in the program, as well as inspected the individual pixel values in the resulting image using an appropriate tool.
so yes?
Yes.
Ok good we've at least narrowed it down a little bit
Set the sky to a solid value of (1, 1, 1) and the ground to (0.5, 0.5, 0.5) is the ground the colour you expect (remove all objects above it)
the ground should be exactly 0.5, 0.5, 0.5 before any gamma correction
I've already tried several such sanity tests, the one you suggested included. Yes, it does return vec3(0.5).
and if you try it with the same ground colour?
What do you mean?
If you try it with the same ground colour as the reference it should return a similar colour to the reference image
The ground color in the reference is vec3(0.5) too.
*with the sky
You mean to render the ground (with same color as reference) with sky included (as evaluated in reference)?
and no objects. I would stick with a constant colour that is close to what it is near the top
It should match the colour pretty closely
both yours and the reference should be ~gamma(SKY_COLOUR*Vec3(0.5)) no?
based on the images you sent one of them is clearly not
You will get the same color as in my image, the one that is much too bright/saturated.
I believe I've tried exactly that.
That's what is confusing me so much
I don't get how it becomes that way
what do you mean too bright
The evaluation of the sky color is identical and the ground color is identical
is it gamma(SKY_COLOUR*Vec3(0.5))?
that's not what I'm asking
How it produces the blueish ground color is not making sense
(your latest suggested test)
Is the pixel value the same as if y ou calculated gamma(SKY_COLOUR*Vec3(0.5)) manually?
I do not know what SKY_COLOR will evaluate to as it is the result of a linear interpolation that is dependent on the y-component of a given ray.
pick a value near the top it will be close enough
there is clearly a big enough difference between yours and the reference it should be clear if it's off
It should be around [0.474, 0.561, 0.671] roughly
did a colour picker on the reference image and it's around there in a lot of locations
which is what you'd expect
Yes, it is clearly off as we can see in the images. The gamma-corrected values as well as the final values in [0, 255] are much too high.
Yes so test it with a solid sky colour of around 0.9 * SKY_COLOUR and no objects it should match this
I just tested it? The results are much too high, e.g. (gamma-corrected) pixel values such as 0.774202 0.87157 1
ok then now you have something to work off
All of this was known prior to this hence why my hypothesis lies in TraceRay(...) incorrectly accumulating light.
The updating of throughput (which depends on e.g. BRDF, cosine term as well as the PDF with respect to a Lambertian material) is my main suspect.
As it is what is being multiplied by the sky color.
you should not have a cosine term
the sampling in RTOW is proportional to a cosine distribution so it cancels out
Correct, this is done in my case too.
BRDF * cosine / PDF is being evaluated where PDF = cosine / PI, so the cosine cancels out.
so this is too bright but the 0.5, 0.5, 0.5 test from before isn't???
I'm aware
Given that sky color is set to pure white, yes.
With a pure white sky color, the pre gamma-corrected value is vec(0.5), as expected.
that is very odd since you'd expect that to also be off is this is, the brightness of the sky should only be a mutiplicative factor before gamma correction which means either both should be off or neither
0.774202 0.87157 1 means you aren't losing any energy at all with the ground
just gamma correcting 0.9*sky gets you [0.671, 0.794, 0.949] which is smaller then the value you have
Hmm
for the sake of testing I would remove the gamma correction and then just do a solid SKY_COLOUR with 0.5, 0.5 ,0.5 and see if you get [0.250, 0.350, 0.500]
If that works try the same with 0.9 * SKY_COLOUR which should get [0.225, 0.315, 0.450]
What do you mean with "solid SKY_COLOUR"? Do you want it to remain as evaluated in the book, or do you want to set it to e.g. pure white again?
I mean set it to the constant value defined here:
static const glm::dvec3 SKY_COLOR{ 0.5, 0.7, 1.0 };
Now I get exactly this
after I removed the problem
The problem is the updating of throughput as a result of the way I am doing russian roulette...
Removing the russian roulette lead to me getting that ^ correct image.
You shouldn't be doing RR on such an early bounce
aaaaaaaaaaaaaah
right right
omg
it's a stupid logic error
I only start doing RR after 5 iterations... but I divide by 1/p already from first iteration...
no wonder
this is what your RR should look like:
if depth > RUSSIAN_ROULETTE_THRESHOLD {
let p = tp.component_max();
if rng.gen() > p {
break;
}
tp /= p;
}
yeah, I see the exact problem
it is a stupid mistake
static uint32_t ITERS_BEFORE_RR = 5;
const double p = glm::max(glm::max(throughput.x, throughput.y), throughput.z);
if (i >= ITERS_BEFORE_RR && Util::RandomDouble() > p) {
break;
}
throughput *= (1.0f / p);
that is INCORRECT
do you see why 😄
this should not be the case with your incorrect code though?
did you actually test it properly?
it should? I am not testing for early exit until i >= 5
yet I am dividing the throughput to account for the RR even before i >= 5
I am introducing bias
with your incorrect code you get a throughput of Vec3(0.5) on first bounce then multiple by (1.0 / 0.5) = 2 so you get Vec3(1) then exit because you hit the sky
if (i >= ITERS_BEFORE_RR) {
throughput *= (1.0f / p);
}
is the ugly and naíve (but clear) way of fixing it
is there a reason you are avoid nested if statements?
I am not avoiding them bro
I just found out about this mistake
I will now fix it in an appropriate way
likely by first checking the ITERS_BEFORE_RR condition, then if true inside of there do the RR
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);
}
I mean with this
which is probably what you have in mind.
exactly
that was my intended solution that I was just about to implement
you just seemed confused as to why the original code was wrong
so I just introduced that temporary fix to clarify
anyway
all should be well now
No I'm confused why you went straight for a seperate if statement
fresnel looks off
it is a pure mirror
a.k.a. not a realistic material
I'll be transitioning to using a microfacet based model later on
yes but even with a perfect mirror you should not have that darkening
a perfect mirror should not be losing energy
the reference does not have that darkening too
you have a good point
hmm
struct Mirror : Material {
Mirror(const glm::dvec3 color, const glm::dvec3 emission = glm::dvec3(0.0f))
: Material(color, emission) {}
[[nodiscard]] glm::dvec3 f(const glm::dvec3& wi, const glm::dvec3& wo, const glm::dvec3& normal) const {
const glm::dvec3 brdf = color;
return brdf;
}
[[nodiscard]] Sample sample(const glm::dvec3& wo, const glm::dvec3& normal) const {
const Sample sample{
.wi = glm::reflect(-wo, normal),//-wo - 2 * glm::dot(-wo, normal) * normal,
.pdf = 1.0f
};
return sample;
}
};
What are your initial thoughts on this?
My understanding is that the BRDF is simply the albedo, and since we're always choosing the perfect reflection direction the PDF is 1.
the brdf is not simply the albedo
the BRDF is dirac delta distribution multiplied by the albedo divided by cos(theta)
dirac delta distributions are typically not handled in the same way as everything else
for reference this is my naive integrator which should be similar to what you have:
impl Naive {
#[must_use]
pub fn rgb(mut ray: Ray, rng: &mut impl MinRng) -> (Vec3, u64) {
let mats = unsafe { MATERIALS.get().as_ref_unchecked() };
let envmap = unsafe { ENVMAP.get().as_ref_unchecked() };
let (mut tp, mut rgb) = (Vec3::ONE, Vec3::ZERO);
let mut depth = 0;
while depth < MAX_DEPTH {
depth += 1;
let sect = get_intersection(&ray, rng);
if sect.is_none() {
rgb += tp * envmap.sample_dir(ray.dir);
break;
}
let mat = &mats[sect.mat];
let wo = -ray.dir;
rgb += mat.le() * tp;
if mat
.scatter(§, &mut ray, rng)
.contains(ScatterStatus::EXIT)
{
break;
}
// by convention both wo and wi point away from the surface
tp *= mat.eval(§, wo, ray.dir);
if depth > RUSSIAN_ROULETTE_THRESHOLD {
let p = tp.component_max();
if rng.gen() > p {
break;
}
tp /= p;
}
}
if rgb.contains_nan() {
log::warn!("NAN encountered!");
return (Vec3::X, 0);
}
(rgb, depth)
}
}
eval is either BXDF * COS / PDF or albedo for dirac delta stuff
Right, I recognize this. The delta is not necessary to take into account in this case as in this basic material system the reflection will always be the perfect reflection direction. However, I have obviously forgotten to take into account the cosine factor.