A procedurally generated city with the focus set on learning how OpenGL works.
Project information
- Category: OpenGL Project
- Project date: May, 2022
- Made by: Ramón Pérez Segarra and Jesús Royo Carabal.
Description
In this project we built a simple procedural city divided by districts. This city works as a playground for testing our OpenGL skills and learn how to create lighting, texturing, and programming specific shaders for different purposes.
The project also features a high level abstraction of the OpenGL library to ease the work with the library itself by creating classes that wrap around the base OpenGL features such as textures, shaders, programs, etc.
Project features
- Simple procedurally generated city with districts.
- High level abstraction over OpenGL features (programs, shaders, textures, etc).
- Light creation and handling on runtime (point lights, directional lights, spot lights).
- Fog shader.
- Frustum culling optimizations.
- Working geometry shaders.
- Post-processes systems with Blur and Chromatic Aberration.
- A water shader.
- A built-in first person camera.
What I did
- The procedural generation of the city.
- The OpenGL abstraction.
- Frustum culling.
- Geometry shaders.
- Worked with the frame buffers.
- Part of the lighting shaders (directional, point light and spot light).
How it was made
OpenGL abstraction
The project features a built in OpenGL abstraction to help in the creation of the procedural city and be able to develop bigger and better features for the Project for example, with the use of "Materials" made from OpenGL programs and shaders and with custom features that let the programmer define some behaviors don't have to worry about the implementation and left all the hard work to interfaces that must communicate your CPU logic with the GPU.
How does it work
All the abstraction is based on an academic graphics library that was created for the purpose of being reimplemented in order to learn how OpenGL works, this part of the project served perfectly as a solid base for the creation of our personal game engine built from scratch.
The different parts that I had to reimplement were:
- Shader wrapper
- Program wrapper
- Buffer wrapper
- Framebuffer wrapper
- Texture wrapper
- Rendering loop
Shader wrapper
With the shader wrapper you are able to create a shader, define it's source code and compile it, leaving to the user the only task of creating the shader class itself.
Program wrapper
The program needs a group of shaders to create a link between them and it also has methods for the program's use and the uniform setting based on the different data types that you can send to a shader.
Buffer wrapper
The buffers work as a way to upload different kind of data to the GPU by using array or element buffers.
Framebuffer
One of the most interesting parts of the abstraction itself are the framebuffers, a wrapper that lets you create these "buffers" in order to be able to render directly to a texture and then use it to create things like post-processing effects, drawing shadows or even deferred rendering.
In this case we are creating the buffers and dividing the rendering into a "Z Buffer" texture and the "Render Buffer" itself, after doing this we're creating simple post-processing effects such as blur and chromatic aberration.
Texture wrapper
The texture wrapper was built to let the programmer use the texture as a way of drawing something with the shaders or as a way to store information that the shaders could need. An example of the information storage using textures could be the shader receiving data about the fluctuations of a sound wave in order to create an equalizer that behaves differently depending on the sound wave that is receiving.
City's procedural generation
The procedural generation is a very simple one, as we wanted to create something that looked interesting by creating our city in an irregular grid of streets with different lengths and widths.
The depth of the procedural generation wasn't deep enough but I managed to use the resources that I had to prevent the buildings from clipping and to try and create a visually interesting looking city. This was one of my first attempts to create a procedural generation.
How did the grid worked
The generation was divided in different layers, that we could arrange from the smallest to the biggest:
- Flat
- Building
- District
Flat
A flat defines where must be a building, it has an specific size that can differ from the other flats in the grid (irregular grid)
Another thing important when creating the flat is to define it's orientation, as this will be the base and parent of the building that will have in top, the easiest way to rate the building and give a little bit of variation is to apply the rotation when creating the flat itself because we are not tracking of detecting relations between buildings.
The flat also has its own mesh and material, being able to change in case you wanted to have custom flats, even being different between them. In this example the flat used is just a cube scaled to look like a platform with a concrete texture.
Flat example.
Building
The building is just an entity with a material that has a specific albedo texture, specular map and mesh. All the buildings are pre-loaded before start generating the city.
When placing the buildings, we are setting them as "derived" from the flat where they're placed in order to keep the flat's rotation, position and even the scale, as the buildings should be scaled based on the needs of the flat itself but trying to keep the size ratio and not deforming the meshes too much.
Building examples.
District
The district is the main "piece" of the city, a city is created by grouping multiple districts and are formed by flats, streets and buildings.
You can create each district with different sizes and configurations as each one of them is built seperately from the rest and don't know each other, so the streets won't mess up with the twist that you're the one that must locate both districts aligned in order to create a sensation of a connected city.
Distrcits have a very specific structure, with two big avenues that connect in the very center of the district in order to be able to easily connect different districts. They also feature a "square shaped street" that goes around all the district and finishes connecting everything and avoid having dead end streets.
District example with a red fog.
Grid generation
Now that I've explained the different segments of our city I'm explaining the whole process of generating it.
The first step is to create a District by setting up its location, size, street width and avenue width, after that the district starts creating a series of structures called "TemporalStreet", which are the first approach to the streets of our district.
Circular and central avenues
Every district has in common a series of streets, the ones called "Circular avenues" and "central avenues", these ones will be created by using temporal streets and the layout would look like this:
District layout with temporal streets.
The purpose of the temporal streets is to mark off the boundings of the district and to have a way to communicate different districts,
Horizontal and vertical streets
The next step is to create temporal streets to delimitate the flats of our district using the street width that we selected when we created the district. In this step we're going to make a distinction between horizontal and vertical streets and we will create the horizontal streets first in order to define the height of our future flats.
District layout with temporal horizontal streets.
This is the layout with all the horizontal streets, and now we're going to repeat the same operation but with vertical streets, generating the next layout:
District layout with temporal horizontal streets.
And with this simple steps we have the layout for our districts with a fixed spaced for the flats where the buildings will be placed, but currently we only have a "data layout" and we need to convert it into a "physical" one by placing streets and flats.
The main problem with this approach is that we want to connect our streets smoothly with the different intersections that we have, and currently we only have a bunch of horizontal and vertical streets, and if we place our streets over those "temporal streets" our streets will collide and have "z fighting" and very ugly intersections, so, we are going to segment the streets using the intersections between the horizontal and vertical temporal streets and will create "physical" streets on runtime.
Street segmentation
For the segmentation process first we have to sort our arrays of temporal streets as we want to iterate through them from the left to the right and from the top to the bottom.
The segmentation process is very simple, we just need to check the intersections of the streets and have a control on where the street starts and of its size, and with that data we are ready to instantiate a street with wanted mesh.
Its important to know which street are we calculating, as we will need to differentiate between streets and intersections, but that's the next step, first we want to make sure that we can create the separate street segments.
To ease the process of segmentating the streets we are going to take advantage of the predictability of our grid by just calculating the intersections of two of our streets, the circular bottom and the circular left avenue as they also count as regular streets but wider.
District segmentation process.
As you can see the streets are being segmentd in order, first the horizontal and then the vertical one. You also may have noticed that the street is cut just before the intersection and that's because we need those spots to place a intersection depending on the number of connecting streets in that point.
The next step is to repeat the process for every street used the stored intersections and interpolating between them, and with that we achieve a full district ready to place flats and buildings.
The positioning of the flats is made during the segmentation process, as it's the optimal moment to do it because you're checking the bounds of the intersecting streets and you can find the central point and size of the flat easily.
Building placement
The building placement is the last thing we need to do and the process is very simple. First we need to arrange our buildings by size, with three or four different sizes. This is not a very accurte technique but it's easy and fast. After the sorted the buildings we can iterate through our flats, check it's bounds and then choose randomly a building that fits that size with a margin.