Computer Graphics II - Cubemaps & Reflections


Second Homework - Sky using cubemaps and reflective surfaces

Hello everyone, welcome to the second post in our series. We'll continue with our journey of implementing and investigating common computer graphics concepts. This time we'll take a look at the second homework of CENG469, Computer Graphics II course at METU of 2022-2023, which was about implementing skyboxes, and reflective surfaces.

 

For this homework, we were tasked with implementing reflections, and a skybox that samples from special textures which are 6 sided, just like a cube. Even though my main goal was to create something aesthetically pleasing from the beginning, you can see that it failed horribly by the end. But I have learned a lot, and now have a plan for the future, but that's a story for another time. For now, let's begin delving into this homework, shall we?

 

The Game

It wasn't just a plain old homework, was it? Even though the main goal was to create a sky, and shiny reflections, this homework was gamified, and it made so much sense. Unlike my previous assignments, this time we were free on how we implemented certain aspects. The homework only asked for two things:

  1. Implementing the sky using special 6 sided textures (called cubemaps), loaded statically. This means that the sky won't change throughout running the program.
  2. Implementing reflections using dynamic cubemaps. This time the "sky" is seen through the reflections, and they should be updated each frame.

That's it. The only other thing that was required was for this program to have some movement, to be able to inspect the reflections if they were working correctly.


First steps

 Before I begun implementing anything, I decided to work with the sample code we were given, which imported an armadillo and rendered it. Then, I quickly started working on the sky as my first task.

Rendering the Sky - Using Static Cubemaps

I have first tried to settle on which method I was going to use. Basically, there are two methods:

  1. Rendering an actual cube.
  2. Rendering only a quad (Two triangles) on the screen.

While the former sounds more intuitive, I went with the second since it seemed easier to me. Either way, we needed to have the direction we are facing (the gaze direction) for the pixel so that we can use the correct coordinates for the texture. The latter only meant that I had to do more math, but eventually they would render the same thing. So, I started with rendering the quad, and in order to know that I rendered it correctly, it was red:

I then started searching for a library to import images. nothings/stb did the trick. But, I needed to flip images vertically so they weren't upside down.

I used the ground texture as a temporary image for the background:


(CRT like texture bug. Seems pretty cool.)

But it didn't work the first time, so I had to specify an unpack alignment of 1 in OpenGL:

 

Then, next comes my headache for the next 24 hours. Nothing I did seemed to work, and textures were flipped. But upon further research, I found out that OpenGL followed the specifications of RenderMan, a rendering engine used by Pixar, and they used left handed coordinates, with UV coordinates starting from top left, so basically it meant that it was a nightmare to figure out the correct orientations for the 6 faces I needed to upload to the cubemap texture. In the end, I switched some of the textures of the sides of the cube and rotated the whole box by 180 degrees, so that orientations were correct:

   (120 degrees field of view so that other sides are visible)

The math behind the rendering of the sky is actually pretty cool, first you render two triangles to the screen so that it spans the whole screen. Then you pass the projection and view matrix to the shader as usual, but reverse it this time. Because each pixel has its own coordinate value for the vertices, we can use that and reverse it through a reverse view projection matrix, and get the gaze direction, which we can use to index the texture.

Object Model

At this point, I have decided to implement my own class schema, and it didn't seem like much overkill because there are already many existing libraries, so I thought they are all different in some way, I don't see why I cannot do the same thing. I also wanted to keep the code clean, since there were many changes coming along.

I have decided to have simple classes like RenderObject, Program, Geometry and SkyBox. SkyBox inherits from Geometry and has its own texture handling. Otherwise, all the common code is in RenderObject. Program holds the uniforms to set each frame, and necessary shaders.

Orbit Controls & Scroll wheel

To be able to inspect everything, I thought of adding orbit controls, but used euler rotations, which I payed off with many hours involving many frustrations as to why everything was rotating so incorrectly. But in the end, everything got connected to a single rotation angle, which rotated everything from scratch each frame, so that's how I got away with it. I will use quaternions next time.
 
Oh and there is a scrolling function that zooms in and out. Pretty neat.

Ground

Adding the ground was actually pretty straightforward. I just imported the ground object from the help assets, and rotated it so it was parallel to the ground. For it to repeat the texture multiple times, I set the UV coordinates to a high number, like 500. It meant that there were total 500x500 ground textures wrapped in the ground.


 

Then I implemented TeslaBody, TeslaWheels and TeslaWindows objects to have a car. It started with colors of the reflection direction (normals), and then I simply shaded it with the former skybox texture. It will be later switched to dynamic cubemaps.



 


Matcaps

So I didn't feel like implementing lights, but there needs to be a some kind of light to be able to see thing, so I have implemented matcaps. They are basically pre-lighted textures, so after you apply them to meshes, they look better. For this homework though, I had to tone down the effect so that reflections were visible.


Reflections - Using Dynamic Cubemaps

Switching the car reflections to dynamic reflections were surprisingly easy, I have just specified 6 camera directions and rendered them to another cubemap that was bind to a framebuffer. So when I rendered it from the center of the car, I drew everything except the car, so I had the skybox with all the objects around. I only needed to send that to the car shaders instead of the static one for the car to have proper reflections this time.




All in all, it was fun trying to solve these problems that arise when one tries to implement an OpenGL app. It was rather frustrating but a fun experience, and I feel like I have expanded my arsenal a lot.

TL;DR:

  • Sky is rendered using a quad in screen space. Then shaders do the hard work of inversing the projection * view matrix so that we know the gaze direction of the fragment.
  • In order to fix the cubemap orientation, we switch to a right-handed coordinate system by reversing y and z of the gaze direction and switch some of the textures of the cubemap surfaces.
  • There is an orbit control system for camera movement, to look around. It's implemented using euler rotations. Also you can use the scrolling wheel to zoom in/out.
  • Implemented matcaps because I didn't want to implement a lighting system.



Yorumlar

Bu blogdaki popüler yayınlar

Computer Graphics II - HDR & Physically Based Rendering & Image Based Lighting & Shadows

Computer Graphics II - Clouds

Computer Graphics II - Bezier Surfaces