Ray Tracing in One Weekend - OpenGL Compute
As an exercise for learning ray tracing I have implemented most of Ray Tracing in One Weekend, one of the most beloved ray tracing introduction books, in an OpenGL compute shader. The idea is to create an application that shoots multiple rays per pixel and checks for intersections with spheres. This way we procedurally generate a frame, avoiding traditional triangle rendering. I have built up quite a nice C++ framework over time for creating new OpenGL applications, loading shaders etc. so I could focus on the actual GLSL ray tracing program and only need a bit of C++ for dynamically adding, removing and altering the rendered objects.
Rasterization
Traditional rendering is called rasterization: It is the process of taking vertices (points in 3D space, called 'vectors' in math) that make up a triangle and converting it to monitor
pixels. We take the triangle and project it flat onto the screen. This technique is used the for pretty much every modern game; Everything you see on the screen is made up of millions of triangles all being rasterized every frame.
Image above: Doom Eternal (2020) id Tech 7, Image below: Rasterizing triangles
Ray Tracing
Modern graphics cards have had years of hardware development to optimize the rasterization pipeline and I believe that many decisions made were based on a clear direction. Though, the mathematical concept of ray tracing has been around since before I was born and it is good to see companies like Nvidia lead the way to a ray traced future. So how does it differ from rasterization?
As previously explained, rasterization fills every pixel by projecting triangles flat onto the screen. In ray tracing, we shoot a ray for every pixel on the screen and check for the closest triangle we intersect (or not). In an oversimplified example, we look up the intersected triangles' colour and write that to the pixel we shot from.
In pseudo-code it is something like this:
for pixel on screen:
ray = ray(pixel)
for object in world:
for triangle in object:
if ray.intersects(triangle):
// check if its the closest intersection
// if so, get the colour of the triangle
You will notice that this involves a triple nested for loop, and with objects being made up of millions of triangles, you end up shooting billions of rays. Nvidia attempts to speed up this process with dedicated hardware executing the intersection algorithms, but because of its recursive nature it will take years before we get to fully ray traced frames.
Now let's talk spheres. In this demo, we define the world objects to just be spheres and it just so happens that, with the power of linear algebra, we can directly check for intersections:
for pixel on screen:
ray = ray(pixel)
for sphere in world:
if ray.intersects(sphere):
// check if its the closest intersection
// if so, get the colour of the triangle
This cuts out the entire triangle loop. So how do we check a ray directly against a sphere? Isn't a sphere made up of thousands of triangles?
Oh god, math
Bounces
Using the previous techniques we can determine if a ray intersects a sphere and memorize the colour if we did. We then create a new ray, with its origin at the intersection point and shoot in the opposite direction. This way we recursively keep bouncing until we miss. In real life, light loses some of its 'power' every time it bounces off of a surface, so every time we bounce we add the colour of the sphere we just hit to the previous colour, but halving it each time:
rgba colour = rgba()
for pixel on screen:
ray = ray(pixel)
for sphere in world:
if ray.intersects(sphere):
if(closest intersection):
colour += sphere.colour * 0.5
This is a gross over simplification, so if you want to learn more about this concept look into Physically Based Rendering. It's basically a bunch of math to determine how much power is lost and how rays bounce off of surfaces to determine reflections. For example, glass is perfectly reflective, so rays bouncing off of glass shoot directly in the opposite direction. Rougher materials might bounce off in different angles.
What is OpenGL?
OpenGL is a way to interact with graphics cards/units in a
computer system through code.
In itself, its a giant API document that driver vendors like Nvidia and AMD are to implement for their graphics cards. The API has been around for
years and compared to newer alternatives like DirectX12 and Vulkan it is
much easier to learn and use, but comes at the cost of performance and flexibility.
What is Compute?
Rasterized rendering is done through various graphics card programs
called shaders. There are shaders for every stage of the rasterization
process, most common are per vertex (point in 3d space) shaders and per pixel shaders. For example, in the pixel shader you look up the colour of the rasterized triangle and the program will write that colour to the screen. Compute
shaders are programs that run outside of the rasterization process and
have nothing to do with rendering, they are called 'compute' because that is
what they are designed to do; perform work that only involves computation.
In newer APIs this means you can execute compute shaders in parallel with
vertex and pixel shaders. This way one can render a complex scene and
perform computation-only stuff like particles or animation alongside it.
Comments
Post a Comment