Closed KnewHow closed 3 years ago
Hi KnewHow,
Regarding the call to material.setDiffuse(), the key feature to achieve is a checkerboard pattern. One of the tools ssloy used for the job is the bitwise AND (&) operator. Let's isolate that code here:
(int(.5 * hit.x + 1000) + int(.5 * hit.z)) & 1
When you AND any integer by 1 you're effectively asking "is this number odd or even?". This works because the least significant bit of an integer represents the value 0 or 1, and therefore also indicates an even or odd value respectively.
The next tool used is the ternary operator ?:
. Passing this operator the result of the bitwise AND allows the code to choose either of the two colours, based on whether the result is even or odd.
If we supply this formula with a series of values, say, 1,2,3,4,5,... etc, you can see we achieve the on/off pattern of a checkboard due to the nature of sequential numbers being even and odd.
This is why the hit.x and hit.z are being cast to integers and used as input to this formula -- we can guarantee that the numbers are a changing series due to knowing the application's core loop is walking across the rows and columns of the output image.
So why the +1000? Good question, I'm glad you asked. The gotcha here is the checkboard rectangle spans the 0 value on the x axis. In fact, ssloy has the code written to go from -10 on the x axis to +10 on the x axis. In terms of passing integers from -10 to +10 through the above formula, you arrive at a problem because there is no 0 value, 0 is the absence of a value. When traversing from -x to +x you go from -1, to +1 and obviously they're both odd numbers. So you ruin the nice the checkboard pattern when you cross the 0 coord.
The easiest way out is to shift the numbers away from the origin just for this formula, hence, ssloy adds 1000. Chances are high in this case we won't come anywhere near 0 on the x axis with such a large value.
So why doesn't he add +1000 to the z value too? You're asking some great questions. Because the checkerboard is on the -z axis. It doesn't span 0 like the x axis does. ssloy placed the checkboard at -10 to -30 on the z axis.
To your other question, let's consider this code:
int a = std::max(0, std::min(env.getWidth() -1, static_cast<int>((atan2(d.z, d.x) / (2 * M_PI) + .5)* env.getWidth())));
int b = std::max(0, std::min(env.getHeight() -1, static_cast<int>(acos(d.y) / M_PI * env.getHeight())));
return env.getPixelColor(a, b); // background color
In a nutshell, this is spherical environment mapping. ssloy has thrown in boundary checks for the image so that env.getPixelColor()
doesn't access an invalid pixel. Let's remove those and concentrate on the guts of it.
a = (int)(atan2(d.z, d.x) / (2 * M_PI) + .5) * env.getWidth()
b = (int)acos(d.y) / M_PI * env.getHeight()
Spherical environment mapping is all about converting a unit vector (that is, a vector whose length is 1) to UV texture coords. This is ideal for the case where your camera is inside a sphere and looking around. That's what ssloy is simulating in this tinyraytracer.
This works when you map an image to the sphere that has already been distorted beforehand. Either with an expensive digital camera, or modified afterwards using Photoshop. The image that ssloy has supplied is one such image. That is what's going on in the above formulae -- the code has made the assumption that the input data (the image) have already been distorted and now it's time to convert back to sphere.
To make the formulae more clear, let's rewrite them in terms of UV texture coords.
float texCoordU = atan2(d.z, d.x) / (2 * M_PI) + .5;
float texCoordV = acos(d.y) / M_PI;
int pixelX = (int)(texCoordU * (float)env.getWidth());
int pixelY = (int)(texCoordV * (float)env.getHeight());
Typically the values texCoordU and texCoordV are in the range (0,1) so that when we multiply them with the width and height of the image we're choosing a value within the bounds of the pixel data. The std::min/std::max handle this case above.
Let's look at the Y coord first as it's easier to explain. We know that d.y is a unit vector, therefore it's value will be in the range (-1,1). We want our tex coord to be in the range (0,1). We need a simple way to linearly map a value from the former range to the latter. The formula used here is
acos(d.y) / M_PI
This works because acos accepts values in the range (-1,1) and returns a value (θ) that is 0 ≤ θ ≤ π. This is why ssloy divides by PI afterwards -- it converts the return value into the range (0,1). Great! So we're done, if the value passed is 1 then we get a 0 back. If the value passed in is -1, we get PI back. Divide by PI and we've got a suitable value for a tex coord.
On to the other bit:
atan2(d.z, d.x) / (2 * M_PI) + .5
The idea is similar here. We need to produce a valid tex coord in the range (0,1). The trick is we have two input variables: x and z. This formula achieves the conversion using atan2(). The neat thing about atan2() is that it essentially tells you which way a vector is facing. If it helps, you could think of it as passing atan2 a 2D vector and it telling you which way it's facing from 0° up to 359°.
Of course atan2 actually uses radians, and instead of atan2 returning values in the range 0-359, it returns values in the −π < θ ≤ π range. That is why ssloy divides by 2*PI, because it converts the range of possible return values from -0.5 to +0.5. And that is why he adds 0.5 to the result -- it shifts the range of possible values nicely into (0,1). Once again, we have a nice tex coord.
Hopefully this helped you understand what is going on in the code :-)
Thank you very much, I've got it!
Thank you so much @DamianCoventry for your help!
afterwards
Your explanation is very professional! Now, I have completely understand! Thank you very much!
You're welcome. I'm glad you've got it now. Happy to have helped :-)
I have read whole part one article of you tiny ray tracer, some code I'm still not understanding. Can you give me a solution. the one is checkerboard color with white and light yellow, why you use
material.setDiffuse((int(.5 * hit.x + 1000) + int(.5 * hit.z)) & 1 ? vec3f(.3, .3, .3) : vec3f(.3, .2, .1)); // TODO understand
to get? the whole code is flowing:another problem is calculate the background with environment map with following code, I don't understand this formulate. I if have time, I hope you can do some explain for me. Thank you very much!