Breakdown of the features, don't forget to turn on the audio~
Go thorough beautiful scenes of the game
This is the final project of the upenn course CIS-5600: Interactive Computer Graphics.
Team members:
Jiangxu Xing (Game Engine; Logic layer; Render)
Zhen Ren (Render)
Yueyang Li (Terrain Generation)
Participated in the construction of the G-buffer
Include the calculation vertices' position, normal, and save them into a buffer
Helps me get enough data for the following screen-spaced effects
Bilateral Filter
Implemented the optimized blur algorithm based on both the fragment's position and intensity
Helps in denoising shadow maps, SSAO, and SSMS
SSAO is intended to simulate ambient occlusion in screen space, the main logic behind it is to sample in the positive hemisphere of every fragment to sense surrounding depth.
If surrounding fragments are closer to the player, the target fragment will be darker.
Reference: https://lettier.github.io/3d-game-shaders-for-beginners/ssao.html
The data needed for generating such an effect:
The fragments' position map in view space
The norm map in view space
Random sample points and rotation factor
With different rotation factors in every fragment, I could generate different sampling using a very small number of uniform sample points
How it works for every fragment:
Map uniform sample points to a hemisphere of the fragment based on its norm
Here is how the rotation factor makes difference: generating different sampling by rotating the hemisphere
Sampling surrounding fragments, count their depth
To make the occlusion more realistic, I introduced weighted factors based on the difference of depth
Output the final intensity into a one-channel texture. This texture will be used in the composite shading process to decide the final color of one fragment.
Mix the color of the fragment with the color of the pure sky (no clouds)
This helps us make a smooth blend for the newly loaded terrain and the sky
How it works
The logic behind distance fog is intuitive: use the depth of the fragment as the factor of intensity.
To generate height fog, I need to find the height of one fragment and compare it with the camera's height
Notice that height fog will only appear in the lower latitude.
Animate fog based on time, the fog intensity will reach the maximum when day and night are interleaving.
Note: In my solution, I output the fog intensity as one one-channel texture such that it could be used in the next SSMC step.
SSMC powerfully simulates the scattered light in fogs under screen space, helps generate realistic and immersive vision under fogs
Data I need:
The albedo map from the previous render output (as it is a post-process)
The color map of the pure sky helps get color
The fog intensity map generated in the previous step
How it works:
Get fog intensity data from the intensity map, and calculate the fragment's tint color
Based on the magnitude of the fog intensity, sample its neighbor.
The higher the intensity, the more neighbors it sampled
Mix the tint color of the neighbor to the current position to simulate scattering
I use a weighted bilateral filter to mix the color.
The engine is build based on the insipration of Unity.
Define the critical time for the game to call awake, start, and update together
enum class GameInitStateEnums : int{
NULL_STATE = 0,
GAME_MGR_CONSTRUCT = 1,
GL_INIT = 1 << 1,
TERRAIN_GENERATE = 1 << 2,
// Add more states here, also update the COMPLETE flag
ENGINE_START = 1 << 10, // Update start
GAME_START = 1 << 11, // Game start, able to control
INIT_COMPLETE = GAME_MGR_CONSTRUCT | GL_INIT,
READY_FOR_PLAY = INIT_COMPLETE | TERRAIN_GENERATE
};
Use std::thread, std::bind, std::forward to create a threadpool
Application: Multi-thread creation of the terrain and its VBO data
/// MultiThread/ThreadPool
/// Same interface as std::thread, but will push the task to the queue
/// Accept a void function and several of its arguments
template <typename F, typename... Args>
void thread(F &&f, Args &&...args)
{
std::function<void()> func =
std::bind(std::forward<F>(f), std::forward<Args>(args)...);
m_queue.push(func);
}
// Worker's function
// Always ask the pool to get the task
void ThreadPool::doThread(ThreadPool *pool, int id){
if(pool == nullptr){
throw std::invalid_argument("Invalid pool");
}
std::optional<std::function<void()>> myTask;
while(1){
ThreadPool::getTask(pool, myTask);
if(myTask.has_value()){
myTask.value()(); // Call the task function
}
}
}
The checking of collision is achieved by ray-marching
Define the player as a cube with 1 * 1 * 2 dimision, and other cube in the mc-world with 1 * 1 * 1 dimension
Perform raycast from the player's camera position, the length of the ray is defined by the expected displacement at current frame
// This function will be called every tick to make the velocity closer and closer to the target
void RigidBody::calculateVelocity()
{
/// The larger it is, the longer it takes to change the velocity in the axis
glm::vec3 dampFactor {0.1, 0.3, 0.1};
m_velocity = glm::mix(m_velocity,
m_velocityMax * m_targetVelocityFactor, // The target velocity
m_dt / dampFactor);
}
// Here's an example to let the player cast a ray to find the interactable cube
#include <RigidBody/raycastor.h>
void Player::castLookRay()
{
RayCastor::generateRay(mcr_camera.mcr_position, m_forward, INTERACTION_LENGTH);
m_interactivableCubeHit = RayCastor::castRayToValidTarrain(mcr_terrain);
}