How I'm Implementing Procedural Architecture
In my previous post "Citybound as a Truly Moddable and Educational Simulation", I laid out how I'm planning to use domain-specific languages to make key parts of Citybound easier to develop. So easy, in fact, that they can trivially be modded. I mentioned two aspects, economic household behaviour and procedural architecture, as examples of things that could be described using domain-specific languages.
Today, I wanted to give you an update on my progress with one of those areas: my work on a domain-specific language for procedural architecture. For shortness' sake, let's call it the architecture language.
What is the job of the architecture language? It has to support two things:
- It should allow you to describe a particular architectural style in a recipe - with enough detail and flexibility that you could imagine several buildings that have that style in diverse suitable locations and settings
- It should consist of a vocabulary (from which you build recipes), which is precise enough that a computer can generate exact 3D geometry of random building variants for any given recipe and target location. The generated geometry should look very similar to what you imagine when you read the recipe yourself.
I believe I have come up with at least a good first try of a language that does both. Here is how it works.
- The idea of a building is decomposed into specialised rules.
- Each rule describes one aspect of how a building with a certain architectural style looks.
- These rules are composed, can be nested and can delegate work between each other.
- Rules have variables, that represent all parameters that can vary in a particular aspect of a building.
- Each rule internally knows how to generate 3D geometry for the building aspect it represents, or it can delegate geometry generation to nested child rules.
- A building style recipe is just a particular composition of rules with a particular assignment of values to rule variables.
- A recipe doesn't need to assign absolute values to rule variables, it might also assign a certain range of possible values from which a random value will be picked upon building generation. This allows for natural variation among even buildings of the same style.
This all probably sounds very abstract, so let me show you which rules there actually are, and how they work together.
My Current Rules of The Architecture Language
At the highest level, we have the building rule which is composed of one lot rule, describing how a building's lot will look, and one or several corpus rules, each describing one distinct building corpus on the lot.
Let's first look at the lot rule in more detail:
The lot rule still doesn't have any variables, but is wholly composed of a ground rule, a boundary rule, and one or more pavement rules.
A ground rule describes how the lot's ground should be covered by a material, such as asphalt (that differs from the natural underground material present, such as grass). The shrink variable allows you to define an offset from the lot boundary that the covered shape should be shrunk by.
The boundary rule can be used to add fences or surrounding walls to a lot. You can set height, material and the start and end point of a gap - a simple approximation of a gate or entrance to the lot.
Pavement rules let you add foot or car paths on your lot. You simply define where on a corpus or the lot boundary the path should start and end and it's shape will automatically be determined based on that.
Now that we covered all the ways that the building lot can be designed, let's look at the building corpora.
The corpus rule consists of a fundament rule, which positions and shapes the fundament of the corpus, one or several floor rules and one roof rule.
An interesting detail here is that the corpus rule has a number of floors variable, but the number of nested floor rules might differ from that. The algorithm for distributing which floor rules apply to which building floors works as follows:
- The first floor rule will always be applied to the ground floor
- All following rules will be evenly distributed onto remaining floors, bottom-up, repeating as necessary
This makes it possible to have a distinct look for the ground floor, lower floors and higher floors, without requiring too much configuration.
As mentioned, the fundament rule is responsible for positioning and sizing the building fundament on the lot.
This is quite complex, since in Citybound lot shapes can be very diverse, have more than four sides and even have curved edges.
To bring order into this large possibility space and to mimic what I assume architects do in real life, we orient things along the main lot edge - the one that goes along the road. If there are several, the longest road-adjacent lot edge is chosen.
Relative to that edge, a "major axis" is given by an angle offset from the road direction at the center of this lot edge. In most cases, you will want the major axis to run orthogonally to the road. The building corpus will then be oriented on a coordinate system given by this major axis.
The next problem is again due to the possibly irregular lot shapes: you can't really define absolute positions and sizes for the fundaments that will work on all possible lots and look good.
Instead, you define a rough offset to the side of the main axis, a width and a maximum fundament length. Together with a minimum padding from the lot boundary, the generation algorithm will try to come up with concrete values for building position and size that fulfill these constraints. If they can't be fulfilled, the whole building rule will raise failure, leaving the lot unoccupied until a more suitable building (with a different style) will be attempted to be built there.
The floor rule is also quite interesting, and pictured here with a more complex building corpus than in the overview pictures earlier.
Each floor has its own height and can then define how the footprint of the next floor should extend or shrink on its two axes (width and length). This lets you create an interesting 3D contour of the building, like in the example. The highest floor in the same way defines the footprint for the roof.
What actually goes on the walls of each floor is defined by four potentially distinct facade rules, one for the front, left, back and right sides of the floor, where "front" means "facing the road".
There is actually two kinds of facade rules: the simple case, which defines a material and decoration rules to apply to a whole wall - and the subdivision case, which distributes several other facade rules horizontally onto subsets of a wall.
While the distribution of floor rules mentioned earlier allows you to vary the look of the whole building along the vertical axis, the subdivision facade rules allow you to vary the look of the facade along the horizontal axis, on a per-floor basis. Together, these can give rise to all kinds of complex facades encountered in real buildings, while delegating to reusable rules for each distinct look that exists somewhere on that facade.
Supplementing the facade material, the decoration rules are responsible for distributing props along a wall. A prop is a 3D model and can be anything from windows, doors, drainpipes, ornamental columns, etc. Right now, each prop is just distributed evenly according to a defined minimum spacing, but this rule is bound to become more complex.
Finally, the roof rule right now allows you to define simple cases of gable and hip roofs, and even flat roofs as a degenerate-case-hack. I'm sure I will soon introduce several specialised kinds of roof rules to take better care of these distinct cases, and more. I will also introduce roof decoration rules that will work similarly to the wall decoration rules shown above. They will be used to distribute things like chimneys, roof windows, AC units, antennas and what-have-you onto angled and flat roofs.
I hope that by contemplating my explained rules above, both their general power as well as current limitations have become obvious. I started rewriting my hardcoded building generation code into using rules from this language. To define a new building style now takes a fraction of code compared to the previous manual process, takes an order of magnitude less time to write and results in snippets of rules and sub-rules that can be reused across building styles! I even almost got real-time rule reloading working, so I can change the rules without recompiling or even restarting the game, seeing the regenerated buildings immediately.
The language as presented here is actually already the second iteration I'm doing, based on the learnings from the first attempt to convert some of the hardcoded building styles. I'm sure that as I implement more and more building styles, this will inform further improvements to the language itself, but I expect those to be minor changes. I'm quite happy with the overall framework as laid out here.
I obviously can't wait to share the actual generation results of this language when they're ready - with both improved existing and completely new building styles. And even more so, I can't wait to see you play around with the language and create your own building styles. This would be the first really moddable aspect of Citybound. The part that exposes this language in the Citybound UI and thus allows player modding is almost ready as well. First, it will just be a simple text editor where you can edit the JSON representation of each rule - but this could be developed further later into an actual visual building style editor, making this kind of modding even more approachable.
In the end you might of course complain that this work is all about purely cosmetic aspects of Citybound. And while I think that beautiful and interesting procedural buildings will be at the core of Citybound's look and feel, what makes this really interesting to me is the general idea behind it.
Because this approach of moddable rule-sets can be applied to the simulation logic aspects of Citybound as well! And in the same way as for buildings, it will make my development and iteration faster and it will open up experimentation with the main game dynamics to our whole community.
If you can't tell, I'm really excited about this new direction.
I'm looking forward to hear what you think about it. And let me know how you liked this in-depth exploration of what I'm working on!