2
$\begingroup$

This picture depicts my current immagination of the coordinate systems involved in ray tracing:

my current imagination of the coordinate systems involved

Explanation: So the green rectangle is the [-0.5, nx-0.5] x [-0.5, ny-0.5] coordinate system and you can translate that coordinate system to the {u, v, w} coordinate system's coordinates (the world coordinate system) w/ the equations near the green rectangle.
From the origin of the world coordinate system, you can cast rays into the [l, r] x [b, t] coordinate system w/ the equation near the red rectangle ($s = e + u_{s} * u + v_{s} * v + w_{s} * w$).

The [-0.5, nx-0.5] x [-0.5, ny-0.5] matches a classic windowing systems coordinate sytem while the [l, r] x [b, t] seems to be the "ray tracer's screen"/"ray tracer's view area".
Both apparently define the view frustum depicted above. This however is counterintuitive for me since I expect the screen directly in front of the world coordinate system. So the screen I am seeing is the green rectangle, the [-0.5, nx-0.5] x [-0.5, ny-0.5]. That means everything goes backwards.



The ray tracer algorithm in my own words (and with my current understanding):

"Cause" the world space to shoot a ray so that it goes through each pixel of the window screen [-0.5, nx-0.5] x [-0.5, ny-0.5] and check for an intersection within the frustum along the way. If there is an intersection color it, if not leave it at the background color.
IOW: I am a "commander" and as I go through each pixel of [-0.5, nx-0.5] x [-0.5, ny-0.5] I cause the world space to shoot/cast a ray so that it goes through the pixel that I am currently at as well while checking for intersections within the frustum.



With my current understanding I tried to implement a ray tracer on the basis of a set_pixel() function.



My implementation attempt (w/ a skewed green sphere as a result):

void game::Engine::run()

{

int nx = window_width; // 1366
int ny = window_heigth; // 768
// Construct the "ray tracer window"/"ray tracer screen"
int left = -200;
int right = 200;
int top = 100;
int bottom = -100;
int distance = 10;
// Create the {u, v, w} coordinate system
Vec3f e{ 0, 0, float(distance) };
Vec3f u{ 1, 0, 0 };
Vec3f v{ 0, 1, 0 };
Vec3f w{ 0, 0, float(distance) };
// Create the sphere
Vec3f center{ -50, -20, 11 };
float radius = 3.0f;

// Game loop
bool running{ true };
SDL_Event event{};
while (running)
{
    // Clear buffer
    SDL_SetRenderDrawColor(renderer, 153, 153, 153, 255);
    SDL_RenderClear(renderer);

    // Draw
    for (int i = 0; i <= window_width; ++i) {
        for (int j = 0; j < window_heigth; ++j) {
            // Calculate the scalar factors from the pixel position for each axis of the {u, v, w} coordinate system
            float u_s = (left + (right - left) * (i + 0.5)) / nx;
            float v_s = (bottom + (top - bottom) * (j + 0.5)) / ny;
            float w_s = -distance;
            // Cast a ray (in a perspective manner)
            Ray3f s{ e, u * u_s + v * v_s + w * w_s};
            Vec3f co = center - s.origin_vector;
            // Calculate the intersection between the sphere and the ray
            float a = Vec3f::dot_product(s.direction_vector, s.direction_vector);
            float b = 2 * Vec3f::dot_product(co, s.direction_vector);
            float c = Vec3f::dot_product(co, co) - radius * radius;
            float discriminant = b * b - 4 * a * c;
            Vec3f color_vec{ 0, 0, 0 }; // default background color
            if (discriminant > 0)
            {
                // Make the hit sphere green
                color_vec.x = 0;
                color_vec.y = 1;
                color_vec.z = 0;
            }
            set_pixel(renderer, i, j, 255 * color_vec.x, 255 * color_vec.y, 255 * color_vec.z, 255);
        }
    }

    // Swap buffers
    SDL_RenderPresent(renderer);
    // Process event queue
    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {
        case SDL_QUIT:
            running = false;
            break;
        }
    }
}

}



The result: result

My conclusion: I got the coordinate system totally wrong thus the skewed result. So the window 1366 x 768 and the ray tracer's screen 400 x 200 as well as the world coordinate system that is spanned with the standard basis in the code doesn't lead to the expected result the sphere beeing on the center of the screen and not looking skewed. Plus, the coordinate system acts weird and seems to be arbitrary.
Thus something is fundamentally wrong with my current understanding.

References:
Lecture on ray tracing
Peter Shirley's treatise on ray tracing

$\endgroup$
3
  • $\begingroup$ Your sphere intersection is somewhat wrong as it doesn't test, the case of spheres on the back side of camera. Second the formula is left + (right - left)*(i+0.5)/nx Notice that left doesn't get divided by nx $\endgroup$ Aug 28, 2018 at 13:21
  • $\begingroup$ Also always aim to normalize all your direction vectors unless you have a use for their magnitude. That simiplifies a lot of things such as intersection. You won't have to divde or multiply by a's $\endgroup$ Aug 28, 2018 at 13:30
  • $\begingroup$ Thanks for the input, gallickgunner! I normalized all the involved vectors and I corrected the formulas as you suggested. I don't know what exactly of my sphere intersection test is wrong. The result is now that the entire window gets painted green. Here is the code: privatebin.net/?080012578f6fb173#mvw2yi/… Here is the screenshot: s15.postimg.cc/x16akk6sb/Screenshot_RT_app_03.png $\endgroup$
    – user9266
    Aug 28, 2018 at 14:38

1 Answer 1

2
$\begingroup$

I gave it a quick look and I found several places which need attention. First of all you forgot transforming the view space to NDC space.

We are working with 3 spaces.

1) The world space, which is the global reference frame. You usually define spheres in this frame. Or if you are reading model data, you might want to multiply by a model matrix to bring them into world space.

2) The camera space. This is the part of the world the camera views. The correct equation for generating rays in the camera space as i mentioned in the comments are given by.

$ x = l + (r-l)*(pixel_i * 0.5)/Width$

$ y = b + (t-b)*(pixel_y * 0.5)/Height$

$ z = -View Plane Distance$

3) The third is the NDC space which is usually in the range $[-1,1]$ Some API's have this range in $[0,1]$ For ray tracing however, we are concerned with $[-1,1]$. The concept here is, imagine your whole image which is $Width \times Height$ to be mapped to a plane which is $2 \times 2$ units wide centered at the origin. This is somewhat a generalization. We don't need to mess with the width and height anymore. We introduced an intermediary space where everything is in the range [-1,1].

However this is true only for square images. What happens when width != height?

Then we have to account for it by doing,

$ x = [-a, a]$

$ y = [-1, 1]$

Where $a$ is Aspect Ratio given by.

$ a = Width/Height $

The NDC space is usually calcualted as,

$ x_{ndc} = [ (2 * ( i+ 0.5)/Width) - 1] * Aspect Ratio$

$ y_{ndc} = [ (2 * ( y+ 0.5)/Width) - 1]$

Note tho depending on the image coordinates, we might need to flip $y_{ndc}$. The calculation above assumes Image has the origin at the top left corner. If the origin is at the bottom left, we will need to flip.

However this assumes the camera is positioned at the origin. How do take into account for camera rotation and position. Normally this is done by the use of a camera matrix which transforms from world space to camera or vice versa. You calculate ray direction in the NDC space, multiply it by this matrix and then normalize it. I will not go into details about this one.

The way you are calculating the rays in camera space, haven't seen much like this but there is a simple fix to get a moving camera and transform it in NDC space. You divide by the width and height of the camera volume. So your equations become.

$ x = [l + (r-l)*(pixel_i * 0.5)/Width] / (r-l)$

$\therefore x = l/(r-l) + (pixel_i * 0.5)/Width $

$ y = [b + (t-b)*(pixel_y * 0.5)/Height] / (t-b)$

$\therefore y = b/(t-b) + (pixel_y * 0.5)/Height$

$ z = -View Plane Distance$

You don't need to account for aspect ratio in this scenario as it's already being done by $l/(r-l)$ and $b/(t-b)$ factors.

As for the proper way of intersecting spheres (if you are normalizing direction, which you should, would be)

 t = -1.0f;
 co = ray.origin - sphere.center // Note we subtract center FROM origin.
 b = dot(co, ray.dir);
 c = dot(co, co) - sphere.radius * sphere.radius;
 disc = b*b - c;
 if disc >= 0
   t = -b + sqrt(disc); // wrong
   t = -b - sqrt(disc); // correct
 if ( t >= 0)
 {
  ray.length = t;       /* you can add a length parameter in ray as 
                           direction is normalized
                           Set other parameters such as sphere id, or if 
                           there is only 1 sphere, set color here. */
  return true;
 }
 else
  return false;

Also forgot to mention your spheres $Z$ coordinates should be negative. Since rays are generating in the $-Z$ direction.

EDIT:- I corrected my sphere intersection. There was a mistake. To answer the questions in your comments.

1) About the spaces, scratchapixel seems to follow RenderMan's coordinate system. Since I'm more from an OpenGL background, I'll talk with that in mind. When I talk about NDC space that equals scratchapixels's definition of screen space.

Screen space usually means screen/window coordinates which range from 0 to image width/height and that's why scratchapixel can be confusing sometimes.

2 and 3) About the sphere intersection and why rays need a length parameter. First of all rays are defined by this formula.

$ R = e + t*\hat{d}$

Where $e$ is the origin, $\hat{d}$ is the direction vector of that ray and $t$ is the length of that ray. In raytracing we initialize all our rays to have the length infinity. Then whenever we intersect anything, we set this $t$ parameter if it's less than the previous value. For the first time, the $t$ value is set to the distance to the first object hit. After that whenever we find that the same ray hits another object we compare the $t$ values to see which one is closer.

We can't directly set color by checking discriminant only because when a Ray interseccts a sphere it can be the case that it intersects at two points (when ray isn't tangent to the sphere). Both of these are given by,

$ t = -b + sqrt(disc)$

$ t = -b - sqrt(disc)$

However in the case for spheres, the closest point is always given by the second one. I mistakenly provided you the first one.

However this doesn't mean we dont have a view frustum. If we want we can define near and far planes to get a closed view frustum and initialize all rays with the length = distance to far plane. Then any objects farther than this far plane will not get intersected. In other words, discriminant will be greater than zero but the t check would fail.

Also I don't see where you have declared window_width in your code but make sure when dividing by width or height the division should have a float value in it (cast width/height into float if declared as int) other wise aspect ratio would be rounded off to an integer.

And try placing your sphere further in the -Z direction. Let's say (0,0, -6).

$\endgroup$
10
  • $\begingroup$ Part 1/3: Thanks for your input, gallickgunner! As I tried to follow your answer and correct my mistakes in code, I also consulted the article on ray tracing from scratchapixel.com[1] since the different depictions/approaches of the involved coordinate systems confused me (they are different from Wolfgang Huerst's, Shirley's). As you pointed out, I have forgotten to map to the NDC space and from there to the screen space (as [1] puts it). [1]: scratchapixel.com/lessons/3d-basic-rendering/… $\endgroup$
    – user9266
    Aug 29, 2018 at 16:07
  • $\begingroup$ Part 2/3: So I did this in the for loop where the pixels are handled (privatebin.net/…). The result I got is a black screen. Unexpectedly, The green sphere isn't appearing at all. Furthermore I am not sure why the length parameter is needed. Also, what does t = -b + sqrt(disc) do? Why does the discrimant alone not suffice (the "intersection detector")? $\endgroup$
    – user9266
    Aug 29, 2018 at 16:08
  • $\begingroup$ Part 3/3: So I need 3 spaces: world, camera, screen space. The camera "sits" at the origin and shoots rays into the view plane. Since I converted from NDC space to screen space already all I need to do is to construct a primary ray w/ the distance from the view plane and the x and y coordinates of the screen space. Thus apparently there is no view frustum and the rays go to infinity. Are the screen space and the camera space the same? What's the difference? $\endgroup$
    – user9266
    Aug 29, 2018 at 16:08
  • $\begingroup$ Sorry, I forgott to disable the expiration: privatebin.net/?1db98377b85f2b0f#AOhpWXLhw5O9/… $\endgroup$
    – user9266
    Aug 29, 2018 at 16:13
  • $\begingroup$ @user9266 - edited my answer to add some details. brief ans is use t = -b - sqrt(disc) (Notice the negative) and when dividing by window_width/height cast it into float if saved as integer. $\endgroup$ Aug 29, 2018 at 17:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.