Mini Minecraft

December 2023
  • C++
  • GLSL

Overview

This is a custom implemntation of a simplifide Minecraft as the final project for CIS 4600 and was done in collaboration with Christina Qiu and Aaron Jiang. The goal was to get a working game including terrain, player physics, and an alternate world that could be traveled to and from by building portals. The majority of features added were for asstetics and to increace performance. I was responsable for implementing efficient terain generation via chunking, texturing (blocks and bill board assets), ambient occlusion, and shadow mapping.

Full Project Video Demo

Implemented Features

Chunking

In order to speed up the game play, I designed a chunking system similar to the one found in Minecraft. Each chunk was 16x256x16 blocks in size. Chunks would be generated based on proximity to the player so that you could never reach the end of the generated world before the ground had loaded in. Additionally, while generating each chunk, only block faces that didn't have blocks in front of them were created and buffered to the GPU for rendering. Using a chunking system also allowed for multi threading later on, as each chunk could be generated in parallel.

To keep track of which faces to include, I created a new struct which represents an entire block face keyed by the direction it is pointing. I used this in combination with several unordered maps to construct the vertex buffer objects (VBO) for each block. For faces on the border between two chunks, I used a mod system to check the next chunk over for blocks. Rather than updating the model matrix for each face, I kept the world space positions of the current chunk which I used with the chunk space position of each point to directly draw the faces in world space. The main dfficulty was getting the chunk border to work correctly when checkin for empty blocks.

Textures

Implementing textures was fairly strait forward. Since I had a sprite sheet for the different block textures, I created a large map of uv choordinates keyed on the block type. To distinguish animated textures (water and lava) from static textures, I repurposed every part of the color buffer rather than creating a seperate attribute. The first two slots were used to pass in the UVs from my texture map, the third slot was used as a boolean (0 or 1) to represent if the texture is animated or not, and finaly the fourth slot was used for the ambient occlusion value (see bellow). Animating the textures was done by adding an offset based on a time variable to the UV coordinates of animated faces. For rendering transparent textures like the water I just created two sepperate buffers and functions that processed the VBOs. I would used each one based on if the current block was opaque or not, then during the rendering stage I would do two render passes, one with just the opaque blocks then one with the transparent blocks on top.

Voxel Ambient Occlusion

The implementation was similar to how I did chunk generation, but on a per vertex basis. For each vertex, you check all the neighboring blocks. The difficulty came with getting the right blocks for each vertex on the face, due to the fact that they could be in different chunks. In the most extreme case, the three possible occluding blocks could be in three different chunks. The way I got around this was with a somewhat overly extensive check for each position value I would use. I then passed on the number of occluders to the shaders, and used that number to calculate a darkening scalar for the final color within the vertex shader.

Flowers / Grass / Mushrooms

These types of geometry were implemented as to crossing bilboard textures, which required a new form of VBOs to be generated. Becuase these features are just two planes set diagonaly across a block, the block position system wouldn't work. However, I could still reuse some of the values. Because of the order of the point's positions I had previously hardcoded in, there wasn't some niece patter I could take advantage of. Thus I hard coded in the X shape. I added these blocks to the transparend VBOs so that the blocks behind them would be correctly rendered. I also slightly adjusted the fragment shader used to render them to discard any fragments with an alpha of 0. This was to solve an issue with fully transparent fragments blocking the opaque fragments behind them when they should have been rendered. This set of blocks was also excluded when taking lighting into account.

Shadow Map

Shadow mpaing, in theory, is a pretty simple idea. Just render a depth map from the lights point of view and do a bit of geometry in the fragment shader to check for occlusion. The tough part is the slight adjustments you need to make for it to fit with your geometry well. To make the shadow map, I reused the frame buffer code used to make the post process effects along with a new shader that rendered the fragments z coordinate as the color. Getting the right position for this light camera took a bit of work to get it to both see all the terain but to also smoothly follow the player's movement. With the shadow map texture created, I passed it into final shader to comput which fragments were in shadow. The hard part was adjusting the bias to remove all atrifacts, which even after hours of adjusting values still resulted in some slight shadow bands when the sun is at a certain angle. Part of the problem is also with the resolution of the light's depth map. Even after upscalling it, the shadow map still resulted in some minor artifacting. The other issue I had was that the shadows buzzed around alot as the sun moved. A partial fix for this was to clamp part of the cameras MVP values, althoguh that only eliminated jittering along one axis.

Shadow Map and Ambient Occlusion

World 1

World 2