Cities are systems of high functional and visual complexity. They reflect the historical, cultural, economic and social changes over time in every aspect in which they are seen. Examining pictures of a large-scale city such as New York or movies like Blade Runner 2049 and games like Cyberpunk 2077 reveals a fantastic diversity of street patterns, buildings, forms and textures. For the Skylines: City Generator project, I was faced with the challenge of creating the rules that would define the city and decided to choose an extremely simple set of rules that form the foundation that the entire city is built on.
In this first part, I will dive into the simple set of rules that I chose to help generate the city. I was inspired by the clean grid based layouts that you can see in parts of Manhattan with the style and aesthetics of a city straight from the dystopian future, where the roads are evenly spaced and the buildings are tightly packed together like Lego blocks. For this implementation, I decided not to classify the different areas of the city into different districts and any building can be placed in any part of the city. In the future, I might look into how that can be integrated into this current implementation of the project or how that might change my approach.
Naïve beginnings
I started this project, by deciding that I would outsource the building models by purchasing premade assets online and then preparing and modifying them in Blender before bringing them into TouchDesigner. With that out of the way, I started work on prototyping the city generation in TouchDesigner. During the first iteration, I decided that instancing in TouchDesigner was going to be the way to generate hundreds of different kinds of buildings in real-time.
In this first iteration, I figured out a way to group the points of a gridSOP for the evenly spaced roads and then randomly assign the remaining points to the buildings. A big problem with this approach was that all the buildings were of the same sizes and if I tried to instance a larger building, it would overlap with either the roads or other buildings. I was now faced with the challenge of coming up with an updated solution that took into account the size of the buildings before a point was assigned for instancing.
Points, but with more data
After the first iteration of the project, I realised that I needed to come up with a new way to create the city grid. While I had figured out how to group points on a single object to be able to instance different kinds of buildings, I had not taken into account that the buildings are of different sizes. Since I had decided to work mostly in SOPs and use Python to assign the groups for the roads and the buildings, I had to figure out how to store additional information about the points to account for the different building sizes.
I decided to use the distance between two adjacent points of the gridSOP as the standard unit of measurement for the different building sizes. Apart from the smallest buildings, which are 1 x 1 units, all other buildings will occupy multiple points on the grid. Therefore, when we are evaluating whether a point on the grid can be potentially used for instancing a building, we have to check if it has already been allocated to a larger building previously.
To be able to store information about whether a point has been allocated or not, I created an interface for a Point object that stored the allocation status for a given point object. This interface is an integral part of how I was able to handle allocating the points to different buildings. The Point interface class is as follows.
Before we can start allocating any points, we created a Point object for every point on the grid. In our Allocator extension, to allocate the points for instancing, we need to identify the index of the point that has been selected, check whether the point satisfies our selection criteria and then add that index value to the corresponding groupSOPs which can further be used for instancing the buildings. The procedure to allocate points on the grid starts with the roads and then moves to the buildings in decreasing order of size. This is to ensure that the buildings do not intersect with roads or other buildings.
We also created a function to assign the points to the groupSOP to allow for repeatability. The function receives a dictionary with the lists holding the point indices and the name of the groupSOPs to reference. The groupSOPs are further sub grouped into smaller groups to accommodate for different buildings with same sizes inside their corresponding base components to finally create the instancing data. You can have a look at the AllocatorEXT here.
In the image above, we see a part of the TouchDesigner project in its current state, on the left you can see the allocation network with the two scripts in the top left and the groupSOPs branching from the gridSOP. On the right you can see the final render of the scene with different Cyberpunk buildings of varying sizes all placed on the grid without overlapping each other.
Points allocated, buildings instanced
Successfully implementing this point allocation algorithm was vital to the project as it allowed me to use a single gridSOP as the source for instancing 80+ different building models on the same grid. I just had to come to the realisation that I needed to treat all points on the grid as a bunch of memory that was being allocated to a building and different buildings would be allocated a different number of points. Once I had all the buildings instanced, I was faced with the urgent need of project optimization. In the next part you can read about how I undertook the process of making the project run smoothly.