Solo Project Application 3D Graphics Programming

Basic Rendering Engine

Timeframe: 3 Months
Role: Feature Developer
Tools: Visual Studio, Dear ImGui, DirectX 11
Skills: C++, HLSL

A custom rendering engine built with C++ and utilizing DirectX 11. This was a semester-long class project where every new feature was added one assignment at a time. Features individually worked on and implemented are: a virtual camera, camera controls, skyboxes, Dear ImGui UI for runtime modification, custom pixel and vertex shaders written in hlsl, game entities with associated transforms, meshes, and PBR materials, multiple light types, and shadow maps. The project also makes use of the professor's own SimpleShader library which makes the process of implementing and integrating shaders into a DirectX application a lot more convenient. I really enjoyed working on this project and put all my effort into making the engine as functional as possible but also structuring the code in a organized and understandable way. As such, this project serves as the best example of my skill with C++.

The project started as a barebones DirectX 11 application provided by the professor. The underlying functionality of creating a new application window and using DirectX to display a frame of pixels with a swap chain was already implemented at the start. From there I built it up to be something that could display 3D objects in a virtual environment with fairly accurate materials and lighting.

Every object that is dislpayed within the engine is stored as a Game Entity class. This is an object-oriented class that includes a transform, mesh, and material. These properties all come together to determine what pixels in the frame are representative of the object and what color those pixels are. They are also everything that can be manipulated and changed with the UI menus at runtime. The transform contains the objects position, rotation, and scale. The position, rotation, and scale of objects are normally accessed in a 4x4 matrix containing each field, but multiple additional ways to assign and retrieve these variables are also provided. For the transform's rotation, I spent a good amount of time trying to use quaternions as the main data type to store the rotation. This was working fine until I added functionality to manipulate an object's rotation in the application's UI using euler angles. I tried multiple formulas to convert euler angles back to quaternions but could never find one that worked flawlessly. Once I started changing the rotation around the z-axis (forward and back), the conversion no longer produced accurate results. So, I made the decision to store rotations as euler angles instead of a quaternion, but created functions the convert that representation to either a matrix or a quaternion to be used for something else.

For lighting and shadow maps, I chose to support 3 types of lights: directional lights, point lights, and spot lights. While performing lighting calculations with all 3 types were fairly similar and straightforward, calculating the shadows formed from each light type was a much more complicated endeavor. The method of displaying shadows that I chose was the simplest one. Build depth buffers from the perspective of the light and only allows light to shine on objects that are the closest depth for each pixel of the depth buffer. For directional and spot lights, this process only needed to happen once, but for point lights, since they shine in every direction, this process needed to be done 6 times to create a depth buffer cube and analyse every surrounding direction.

I also wanted to make the light objects as dynamic as possible so that they could be moved in real time and change the lighting and shadowing conditions. I was able to get the lighting to update with movements at every frame but I could not get the shadow maps to do the same. From there I pivoted recalculating and updating the shadow maps from a per frame operation to a per request operation. I locked the movement and rotation of light objects when the lights are set to cast shadows. The user can then turn off that light's shadows, reposition it, and turn the shadows back on and the shadow maps will update at that moment to the new light direction.

    Challenges:
  • Learning and implementing all the intricacies of pixel shader lighting calculations and BDRFs
  • Learning to work with the DirectX 11 library and learning to code in HLSL
  • Making as many properties as possible available to be manipulated with runtime UI menus
  • Attempting to use quaternions as the sole data type for storing rotations, exept in the UI
  • Ensuring that the lighting and shadows in the engine are able to be dynamically changed at runtime

    What I did well:
  • Made the application easy to use with simple camera controls and user-friendly UI menus
  • Simply structured and well documented code with many useful function overloads
  • Integrated tinyobj loader into the mesh class to support loading a wider variety of 3D model types
  • Implemented a creative way to allow light positions to change and shadow maps to update dynamically

    What I would change/improve:
  • Improve ease of use when setting up a 3D environment to be rendered
  • Add tools in the UI for creating and adding objects to the environment
  • Continue to add more advanced graphics features such as: light baking, material swapping, animated textures, etc.
  • Improve the performance with optimizations on both the GPU and CPU side